Pacxon (#12)
* Add GridMovementAbility and PacXonGridInteractor for grid-based movement; integrate with PlayerController and PacXonLevel * Add GhostMovementComponent and PacXonTrailComponent; integrate ghost movement and trail features in PacXonLevel * Update main menu button focus and add new movement abilities; adjust player and ghost initialization in PacXonLevel
This commit is contained in:
59
scripts/PacXonLevel.cs
Normal file
59
scripts/PacXonLevel.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System.Linq;
|
||||
using Godot;
|
||||
using Mr.BrickAdventures.scripts.components;
|
||||
|
||||
namespace Mr.BrickAdventures.scripts;
|
||||
|
||||
[GlobalClass]
|
||||
public partial class PacXonLevel : Node
|
||||
{
|
||||
[Export] public PlayerController Player { get; set; }
|
||||
[Export] public PacXonGridManager GridManager { get; set; }
|
||||
[Export] public Node GhostContainer { get; set; }
|
||||
[Export] public Label PercentageLabel { get; set; }
|
||||
|
||||
private const float WinPercentage = 0.90f;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
var ghosts = GhostContainer.GetChildren().OfType<Node2D>().ToList();
|
||||
Player.ClearMovementAbilities();
|
||||
Player.SetGridMovement();
|
||||
|
||||
foreach (var ghost in ghosts)
|
||||
{
|
||||
var movement = ghost.GetNode<GhostMovementComponent>("GhostMovementComponent");
|
||||
movement?.Initialize(GridManager, Player);
|
||||
}
|
||||
|
||||
var gridMovement = Player.GetNodeOrNull<GridMovementAbility>("Movements/GridMovementAbility");
|
||||
var gridInteractor = Player.GetNodeOrNull<PacXonGridInteractor>("PacXonGridInteractor");
|
||||
var trailComponent = Player.GetNodeOrNull<PacXonTrailComponent>("PacXonTrailComponent");
|
||||
|
||||
if (gridMovement != null && gridInteractor != null)
|
||||
{
|
||||
gridInteractor.Initialize(GridManager, gridMovement, ghosts);
|
||||
trailComponent?.Initialize(gridInteractor);
|
||||
}
|
||||
else
|
||||
{
|
||||
GD.PushError("Could not find GridMovementAbility or PacXonGridInteractor on Player.");
|
||||
}
|
||||
|
||||
GridManager.FillPercentageChanged += OnFillPercentageChanged;
|
||||
OnFillPercentageChanged(GridManager.GetFillPercentage());
|
||||
|
||||
var playerMapPos = GridManager.LocalToMap(Player.Position);
|
||||
Player.GlobalPosition = GridManager.MapToLocal(playerMapPos);
|
||||
}
|
||||
|
||||
private void OnFillPercentageChanged(float percentage)
|
||||
{
|
||||
PercentageLabel.Text = $"Fill: {percentage:P0}";
|
||||
if (percentage >= WinPercentage)
|
||||
{
|
||||
GD.Print("YOU WIN!");
|
||||
GetTree().Paused = true;
|
||||
}
|
||||
}
|
||||
}
|
1
scripts/PacXonLevel.cs.uid
Normal file
1
scripts/PacXonLevel.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b8lu5pdufiy37
|
69
scripts/components/GhostMovementComponent.cs
Normal file
69
scripts/components/GhostMovementComponent.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using Godot;
|
||||
|
||||
namespace Mr.BrickAdventures.scripts.components;
|
||||
|
||||
[GlobalClass]
|
||||
public partial class GhostMovementComponent : Node2D
|
||||
{
|
||||
[Export] public float MoveSpeed { get; set; } = 0.2f;
|
||||
[Export] public int GridSize { get; set; } = 16;
|
||||
|
||||
private CharacterBody2D _body;
|
||||
private Timer _moveTimer;
|
||||
private PacXonGridManager _gridManager;
|
||||
private HealthComponent _playerHealth;
|
||||
private Vector2 _direction;
|
||||
private readonly Vector2[] _directions = { Vector2.Up, Vector2.Down, Vector2.Left, Vector2.Right };
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_body = Owner.GetNode<CharacterBody2D>(".");
|
||||
_moveTimer = new Timer { WaitTime = MoveSpeed, OneShot = false, Autostart = true };
|
||||
AddChild(_moveTimer);
|
||||
_moveTimer.Timeout += OnMoveTimerTimeout;
|
||||
|
||||
var rng = new RandomNumberGenerator();
|
||||
_direction = _directions[rng.RandiRange(0, 3)];
|
||||
}
|
||||
|
||||
public void Initialize(PacXonGridManager gridManager, PlayerController player)
|
||||
{
|
||||
_gridManager = gridManager;
|
||||
_playerHealth = player.GetNode<HealthComponent>("HealthComponent");
|
||||
}
|
||||
|
||||
private void OnMoveTimerTimeout()
|
||||
{
|
||||
if (_gridManager == null || _body == null) return;
|
||||
|
||||
var nextMapPos = _gridManager.LocalToMap(_body.Position + (_direction * GridSize));
|
||||
var cellState = _gridManager.GetCellState(nextMapPos);
|
||||
|
||||
switch (cellState)
|
||||
{
|
||||
case CellState.Solid:
|
||||
PickNewDirection();
|
||||
break;
|
||||
|
||||
case CellState.Trail:
|
||||
_playerHealth?.DecreaseHealth(9999);
|
||||
_moveTimer.Stop();
|
||||
break;
|
||||
|
||||
case CellState.Empty:
|
||||
_body.Position += _direction * GridSize;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void PickNewDirection()
|
||||
{
|
||||
var rng = new RandomNumberGenerator();
|
||||
Vector2 newDir;
|
||||
do
|
||||
{
|
||||
newDir = _directions[rng.RandiRange(0, 3)];
|
||||
} while (newDir == _direction || newDir == -_direction);
|
||||
_direction = newDir;
|
||||
}
|
||||
}
|
1
scripts/components/GhostMovementComponent.cs.uid
Normal file
1
scripts/components/GhostMovementComponent.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://7i20oc4cyabl
|
59
scripts/components/Movement/GridMovementAbility.cs
Normal file
59
scripts/components/Movement/GridMovementAbility.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using Godot;
|
||||
|
||||
namespace Mr.BrickAdventures.scripts.components;
|
||||
|
||||
[GlobalClass]
|
||||
public partial class GridMovementAbility : MovementAbility
|
||||
{
|
||||
[Export] public float MoveSpeed { get; set; } = 0.15f; // Time in seconds between moves
|
||||
[Export] public int GridSize { get; set; } = 16; // Size of one grid cell in pixels
|
||||
|
||||
private Vector2 _currentDirection = Vector2.Zero;
|
||||
private Vector2 _nextDirection = Vector2.Zero;
|
||||
private Timer _moveTimer;
|
||||
|
||||
[Signal]
|
||||
public delegate void MovedEventHandler(Vector2 newPosition);
|
||||
|
||||
public override void Initialize(PlayerController controller)
|
||||
{
|
||||
base.Initialize(controller);
|
||||
_moveTimer = new Timer { WaitTime = MoveSpeed, OneShot = false };
|
||||
AddChild(_moveTimer);
|
||||
_moveTimer.Timeout += OnMoveTimerTimeout;
|
||||
_moveTimer.Start();
|
||||
}
|
||||
|
||||
public override Vector2 ProcessMovement(Vector2 currentVelocity, double delta)
|
||||
{
|
||||
GD.Print($"Player position: {_body.Position}, {_body.GlobalPosition}");
|
||||
|
||||
var inputDirection = _input.MoveDirection;
|
||||
var newDirection = Vector2.Zero;
|
||||
|
||||
if (Mathf.Abs(inputDirection.Y) > 0.1f)
|
||||
{
|
||||
newDirection = new Vector2(0, Mathf.Sign(inputDirection.Y));
|
||||
}
|
||||
else if (Mathf.Abs(inputDirection.X) > 0.1f)
|
||||
{
|
||||
newDirection = new Vector2(Mathf.Sign(inputDirection.X), 0);
|
||||
}
|
||||
|
||||
if (newDirection != Vector2.Zero && newDirection != -_currentDirection)
|
||||
{
|
||||
_nextDirection = newDirection;
|
||||
}
|
||||
|
||||
return Vector2.Zero;
|
||||
}
|
||||
|
||||
private void OnMoveTimerTimeout()
|
||||
{
|
||||
_currentDirection = _nextDirection;
|
||||
if (_currentDirection == Vector2.Zero) return;
|
||||
|
||||
_body.Position += _currentDirection * GridSize;
|
||||
EmitSignal(SignalName.Moved, _body.GlobalPosition);
|
||||
}
|
||||
}
|
1
scripts/components/Movement/GridMovementAbility.cs.uid
Normal file
1
scripts/components/Movement/GridMovementAbility.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ctm5glmeu502l
|
91
scripts/components/PacXonGridInteractor.cs
Normal file
91
scripts/components/PacXonGridInteractor.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using System.Collections.Generic;
|
||||
using Godot;
|
||||
|
||||
namespace Mr.BrickAdventures.scripts.components;
|
||||
|
||||
[GlobalClass]
|
||||
public partial class PacXonGridInteractor : Node
|
||||
{
|
||||
private enum PlayerGridState { OnSolid, DrawingTrail }
|
||||
|
||||
private PacXonGridManager _gridManager;
|
||||
private HealthComponent _healthComponent;
|
||||
private GridMovementAbility _gridMovement;
|
||||
|
||||
private PlayerGridState _currentState = PlayerGridState.OnSolid;
|
||||
private readonly List<Vector2I> _currentTrail = [];
|
||||
private List<Node2D> _ghosts = [];
|
||||
|
||||
[Signal] public delegate void TrailStartedEventHandler(Vector2 startPosition);
|
||||
[Signal] public delegate void TrailExtendedEventHandler(Vector2 newPosition);
|
||||
[Signal] public delegate void TrailClearedEventHandler();
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_healthComponent = Owner.GetNodeOrNull<HealthComponent>("HealthComponent");
|
||||
}
|
||||
|
||||
public void Initialize(PacXonGridManager gridManager, GridMovementAbility gridMovement, List<Node2D> ghosts)
|
||||
{
|
||||
_gridManager = gridManager;
|
||||
_gridMovement = gridMovement;
|
||||
_ghosts = ghosts;
|
||||
_gridMovement.Moved += OnPlayerMoved;
|
||||
}
|
||||
|
||||
private void OnPlayerMoved(Vector2 newPosition)
|
||||
{
|
||||
if (_gridManager == null) return;
|
||||
|
||||
var mapCoords = _gridManager.LocalToMap(newPosition);
|
||||
var destinationState = _gridManager.GetCellState(mapCoords);
|
||||
|
||||
if (_currentState == PlayerGridState.DrawingTrail) EmitSignalTrailExtended(newPosition);
|
||||
|
||||
if (destinationState == CellState.Trail)
|
||||
{
|
||||
EmitSignalTrailCleared();
|
||||
_healthComponent?.DecreaseHealth(9999);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_currentState == PlayerGridState.OnSolid)
|
||||
{
|
||||
if (destinationState == CellState.Empty)
|
||||
{
|
||||
// Moved from solid ground to an empty space, start drawing.
|
||||
_currentState = PlayerGridState.DrawingTrail;
|
||||
_currentTrail.Clear();
|
||||
_currentTrail.Add(mapCoords);
|
||||
_gridManager.SetCellState(mapCoords, CellState.Trail);
|
||||
EmitSignalTrailStarted(newPosition);
|
||||
}
|
||||
}
|
||||
else if (_currentState == PlayerGridState.DrawingTrail)
|
||||
{
|
||||
if (destinationState == CellState.Empty)
|
||||
{
|
||||
// Continue drawing the trail
|
||||
_currentTrail.Add(mapCoords);
|
||||
_gridManager.SetCellState(mapCoords, CellState.Trail);
|
||||
}
|
||||
else if (destinationState == CellState.Solid)
|
||||
{
|
||||
_gridManager.PerformFloodFill(_ghosts);
|
||||
GD.Print("Fill logic triggered!");
|
||||
_currentState = PlayerGridState.OnSolid;
|
||||
SolidifyTrail();
|
||||
_currentTrail.Clear();
|
||||
EmitSignalTrailCleared();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SolidifyTrail()
|
||||
{
|
||||
foreach (var pos in _currentTrail)
|
||||
{
|
||||
_gridManager.SetCellState(pos, CellState.Solid);
|
||||
}
|
||||
}
|
||||
}
|
1
scripts/components/PacXonGridInteractor.cs.uid
Normal file
1
scripts/components/PacXonGridInteractor.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c00siqtssccr6
|
121
scripts/components/PacXonGridManager.cs
Normal file
121
scripts/components/PacXonGridManager.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Godot;
|
||||
|
||||
namespace Mr.BrickAdventures.scripts.components;
|
||||
|
||||
public enum CellState
|
||||
{
|
||||
Empty = -1,
|
||||
Solid = 0,
|
||||
Trail = 1,
|
||||
Hunted = 2
|
||||
}
|
||||
|
||||
[GlobalClass]
|
||||
public partial class PacXonGridManager : TileMapLayer
|
||||
{
|
||||
[Export] public Rect2I PlayArea { get; set; } = new Rect2I(1, 1, 38, 28);
|
||||
|
||||
private int _solidCellCount = 0;
|
||||
private int _totalPlayableCells = 0;
|
||||
|
||||
[Signal] public delegate void FillPercentageChangedEventHandler(float percentage);
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_totalPlayableCells = PlayArea.Size.X * PlayArea.Size.Y;
|
||||
InitializeGrid();
|
||||
}
|
||||
|
||||
private void InitializeGrid()
|
||||
{
|
||||
Clear();
|
||||
|
||||
for (var x = PlayArea.Position.X - 1; x <= PlayArea.End.X + 1; x++)
|
||||
{
|
||||
for (var y = PlayArea.Position.Y - 1; y <= PlayArea.End.Y + 1; y++)
|
||||
{
|
||||
if (x < PlayArea.Position.X || x > PlayArea.End.X || y < PlayArea.Position.Y || y > PlayArea.End.Y)
|
||||
{
|
||||
SetCell(new Vector2I(x, y), (int)CellState.Solid, Vector2I.Zero);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public CellState GetCellState(Vector2I mapCoords)
|
||||
{
|
||||
var tileId = GetCellSourceId(mapCoords);
|
||||
return (CellState)tileId;
|
||||
}
|
||||
|
||||
public void SetCellState(Vector2I mapCoords, CellState state)
|
||||
{
|
||||
if (GetCellSourceId(mapCoords) != (int)CellState.Solid && state == CellState.Solid) _solidCellCount++;
|
||||
SetCell(mapCoords, (int)state, Vector2I.Zero);
|
||||
}
|
||||
|
||||
public float GetFillPercentage()
|
||||
{
|
||||
return _totalPlayableCells > 0 ? (float)_solidCellCount / _totalPlayableCells : 0;
|
||||
}
|
||||
|
||||
public void PerformFloodFill(List<Node2D> ghosts)
|
||||
{
|
||||
var unsafeCells = new HashSet<Vector2I>();
|
||||
|
||||
foreach (var ghost in ghosts.Where(IsInstanceValid))
|
||||
{
|
||||
var ghostPos = LocalToMap(ghost.Position);
|
||||
FloodFillScan(ghostPos, unsafeCells);
|
||||
}
|
||||
|
||||
var filledCount = 0;
|
||||
for (var x = PlayArea.Position.X; x <= PlayArea.End.X; x++)
|
||||
{
|
||||
for (var y = PlayArea.Position.Y; y <= PlayArea.End.Y; y++)
|
||||
{
|
||||
var currentPos = new Vector2I(x, y);
|
||||
if (GetCellState(currentPos) == CellState.Empty && !unsafeCells.Contains(currentPos))
|
||||
{
|
||||
SetCellState(currentPos, CellState.Solid);
|
||||
filledCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filledCount > 0)
|
||||
{
|
||||
EmitSignal(SignalName.FillPercentageChanged, GetFillPercentage());
|
||||
}
|
||||
}
|
||||
|
||||
private void FloodFillScan(Vector2I startPos, HashSet<Vector2I> visited)
|
||||
{
|
||||
if (!PlayArea.HasPoint(startPos) || visited.Contains(startPos)) return;
|
||||
|
||||
var q = new Queue<Vector2I>();
|
||||
q.Enqueue(startPos);
|
||||
visited.Add(startPos);
|
||||
|
||||
while (q.Count > 0)
|
||||
{
|
||||
var pos = q.Dequeue();
|
||||
|
||||
var neighbors = new[]
|
||||
{
|
||||
pos + Vector2I.Up, pos + Vector2I.Down, pos + Vector2I.Left, pos + Vector2I.Right
|
||||
};
|
||||
|
||||
foreach (var neighbor in neighbors)
|
||||
{
|
||||
if (PlayArea.HasPoint(neighbor) && !visited.Contains(neighbor) && GetCellState(neighbor) == CellState.Empty)
|
||||
{
|
||||
visited.Add(neighbor);
|
||||
q.Enqueue(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
scripts/components/PacXonGridManager.cs.uid
Normal file
1
scripts/components/PacXonGridManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dx4m2ouyvwkir
|
67
scripts/components/PacXonTrailComponent.cs
Normal file
67
scripts/components/PacXonTrailComponent.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System.Collections.Generic;
|
||||
using Godot;
|
||||
|
||||
namespace Mr.BrickAdventures.scripts.components;
|
||||
|
||||
[GlobalClass]
|
||||
public partial class PacXonTrailComponent : Line2D
|
||||
{
|
||||
private PacXonGridInteractor _gridInteractor;
|
||||
private readonly List<Vector2> _trailPoints = [];
|
||||
|
||||
public void Initialize(PacXonGridInteractor interactor)
|
||||
{
|
||||
_gridInteractor = interactor;
|
||||
_gridInteractor.TrailStarted += OnTrailStarted;
|
||||
_gridInteractor.TrailExtended += OnTrailExtended;
|
||||
_gridInteractor.TrailCleared += OnTrailCleared;
|
||||
|
||||
Width = 8;
|
||||
DefaultColor = new Color("#a6f684");
|
||||
JointMode = LineJointMode.Round;
|
||||
BeginCapMode = LineCapMode.Round;
|
||||
EndCapMode = LineCapMode.Round;
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
if (_gridInteractor != null)
|
||||
{
|
||||
_gridInteractor.TrailStarted -= OnTrailStarted;
|
||||
_gridInteractor.TrailExtended -= OnTrailExtended;
|
||||
_gridInteractor.TrailCleared -= OnTrailCleared;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTrailStarted(Vector2 startPosition)
|
||||
{
|
||||
_trailPoints.Clear();
|
||||
_trailPoints.Add(ToLocal(startPosition));
|
||||
_trailPoints.Add(ToLocal(startPosition));
|
||||
UpdateTrail();
|
||||
}
|
||||
|
||||
private void OnTrailExtended(Vector2 newPosition)
|
||||
{
|
||||
if (_trailPoints.Count > 0)
|
||||
{
|
||||
_trailPoints[^1] = ToLocal(newPosition);
|
||||
}
|
||||
UpdateTrail();
|
||||
}
|
||||
|
||||
private void OnTrailCleared()
|
||||
{
|
||||
_trailPoints.Clear();
|
||||
UpdateTrail();
|
||||
}
|
||||
|
||||
private void UpdateTrail()
|
||||
{
|
||||
ClearPoints();
|
||||
foreach (var point in _trailPoints)
|
||||
{
|
||||
AddPoint(point);
|
||||
}
|
||||
}
|
||||
}
|
1
scripts/components/PacXonTrailComponent.cs.uid
Normal file
1
scripts/components/PacXonTrailComponent.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cmk4m7mplqnrm
|
@@ -18,6 +18,7 @@ public partial class PlayerController : CharacterBody2D
|
||||
[Export] public PackedScene OneWayPlatformScene { get; set; }
|
||||
[Export] public PackedScene SpaceshipMovementScene { get; set; }
|
||||
[Export] public PackedScene WallJumpScene { get; set; }
|
||||
[Export] public PackedScene GridMovementScene { get; set; }
|
||||
|
||||
[Signal] public delegate void JumpInitiatedEventHandler();
|
||||
[Signal] public delegate void MovementAbilitiesChangedEventHandler();
|
||||
@@ -75,7 +76,7 @@ public partial class PlayerController : CharacterBody2D
|
||||
_abilities.Add(ability);
|
||||
}
|
||||
|
||||
private void ClearMovementAbilities()
|
||||
public void ClearMovementAbilities()
|
||||
{
|
||||
foreach (var ability in _abilities)
|
||||
{
|
||||
@@ -118,7 +119,14 @@ public partial class PlayerController : CharacterBody2D
|
||||
if (SpaceshipMovementScene != null) AddAbility(SpaceshipMovementScene.Instantiate<MovementAbility>());
|
||||
EmitSignalMovementAbilitiesChanged();
|
||||
}
|
||||
|
||||
|
||||
public void SetGridMovement()
|
||||
{
|
||||
ClearMovementAbilities();
|
||||
if (GridMovementScene != null) AddAbility(GridMovementScene.Instantiate<MovementAbility>());
|
||||
EmitSignalMovementAbilitiesChanged();
|
||||
}
|
||||
|
||||
private async Task ConnectJumpAndGravityAbilities()
|
||||
{
|
||||
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
|
||||
|
Reference in New Issue
Block a user