feat(grid): Replace real-time CTF with turn-based grid system

Replace continuous free-form movement with discrete 20x50 grid-based
gameplay featuring asymmetric movement mechanics:

- Blue/Red teams with 3 units each
- Zone-based movement: orthogonal (4 dir) in offense, diagonal (8 dir) in defense
- Alternating turns with click-to-select, click-to-move input
- Fog of war: 3-cell Chebyshev vision radius per unit
- Defense speed nerf: skip every 4th move in own zone
- AI opponent that chases flag carriers and advances toward enemy flag
- Collision resolution: defender wins in their zone, lower ID wins in neutral

Implements all 3 phases from the plan:
- Phase 1: Playable grid with hot-seat two-player
- Phase 2: Fog of war + defense speed nerf
- Phase 3: AI opponent

Deleted obsolete files: Flag.cs, Unit.cs, RouteDrawer.cs, SimpleAI.cs, Visibility.cs

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
John Lamb
2026-02-07 17:05:44 -06:00
parent 0de174eb1a
commit e4ac24f989
17 changed files with 1658 additions and 1092 deletions

View File

@@ -0,0 +1,905 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public enum ZoneOwner { Blue, Neutral, Red }
public static class ZoneBoundaries
{
public const int TeamBlueDefenseEnd = 20; // Y < 20 is Blue defense
public const int NeutralEnd = 30; // Y < 30 is neutral (if >= TeamBlueDefenseEnd)
public const int BoardWidth = 20;
public const int BoardHeight = 50;
}
public class GridBoard : MonoBehaviour
{
// Configuration
const float CellSize = 1f;
const float CellPadding = 0.05f;
const int UnitsPerTeam = 3;
const int WinScore = 3;
const int RespawnDelay = 2; // Turns until respawn
const int VisionRadius = 3; // Chebyshev distance for fog of war
// Colors
static readonly Color BlueZoneColor = new Color(0.2f, 0.3f, 0.6f, 0.4f);
static readonly Color NeutralZoneColor = new Color(0.4f, 0.4f, 0.4f, 0.4f);
static readonly Color RedZoneColor = new Color(0.6f, 0.2f, 0.2f, 0.4f);
static readonly Color BlueUnitColor = new Color(0.3f, 0.5f, 1f);
static readonly Color RedUnitColor = new Color(1f, 0.3f, 0.3f);
static readonly Color ValidMoveColor = new Color(0.3f, 1f, 0.3f, 0.5f);
static readonly Color SelectedColor = new Color(1f, 1f, 0.3f, 0.8f);
static readonly Color FlagBlueColor = new Color(0.3f, 0.5f, 1f);
static readonly Color FlagRedColor = new Color(1f, 0.3f, 0.3f);
// Board state
GridUnit[,] cellOccupants;
Dictionary<GridUnit, Vector2Int> unitPositions = new();
List<GridUnit> blueUnits = new();
List<GridUnit> redUnits = new();
// Flags
Vector2Int blueFlagPosition;
Vector2Int redFlagPosition;
Vector2Int blueFlagHome;
Vector2Int redFlagHome;
GridUnit blueFlagCarrier;
GridUnit redFlagCarrier;
GameObject blueFlagGO;
GameObject redFlagGO;
// Turn state
Team currentTeam = Team.Blue;
int turnNumber = 0;
// Scores
int blueScore = 0;
int redScore = 0;
bool gameOver = false;
// UI state
GridUnit selectedUnit;
List<Vector2Int> validMoves = new();
List<GameObject> validMoveHighlights = new();
GameObject selectionHighlight;
// Visual objects
GameObject[,] cellVisuals;
// Visibility
HashSet<Vector2Int> visibleToBlue = new();
HashSet<Vector2Int> visibleToRed = new();
// AI
GridAI ai;
// UI references (set by Game.cs)
public TMPro.TextMeshProUGUI scoreText;
public TMPro.TextMeshProUGUI gameOverText;
public TMPro.TextMeshProUGUI turnText;
int nextUnitId = 0;
void Start()
{
InitializeBoard();
CreateBoardVisuals();
SpawnUnits();
CreateFlags();
RecalculateVisibility();
UpdateUI();
// Initialize AI for red team
ai = gameObject.AddComponent<GridAI>();
ai.Initialize(this, Team.Red);
}
void InitializeBoard()
{
cellOccupants = new GridUnit[ZoneBoundaries.BoardWidth, ZoneBoundaries.BoardHeight];
cellVisuals = new GameObject[ZoneBoundaries.BoardWidth, ZoneBoundaries.BoardHeight];
}
void CreateBoardVisuals()
{
// Create parent for organization
var boardParent = new GameObject("Board");
boardParent.transform.SetParent(transform);
for (int x = 0; x < ZoneBoundaries.BoardWidth; x++)
{
for (int y = 0; y < ZoneBoundaries.BoardHeight; y++)
{
Color zoneColor = GetZoneColor(y);
var cell = CreateSprite($"Cell_{x}_{y}", zoneColor, CellSize - CellPadding, CellSize - CellPadding);
cell.transform.SetParent(boardParent.transform);
cell.transform.position = GridToWorld(new Vector2Int(x, y));
cell.GetComponent<SpriteRenderer>().sortingOrder = -10;
cellVisuals[x, y] = cell;
}
}
}
Color GetZoneColor(int y)
{
var zone = GetZoneOwner(y);
return zone switch
{
ZoneOwner.Blue => BlueZoneColor,
ZoneOwner.Neutral => NeutralZoneColor,
ZoneOwner.Red => RedZoneColor,
_ => NeutralZoneColor
};
}
void SpawnUnits()
{
// Blue units spawn at bottom of blue zone
for (int i = 0; i < UnitsPerTeam; i++)
{
int x = ZoneBoundaries.BoardWidth / 2 - 1 + (i % 3);
int y = 2 + (i / 3);
SpawnUnit(Team.Blue, new Vector2Int(x, y));
}
// Red units spawn at top of red zone
for (int i = 0; i < UnitsPerTeam; i++)
{
int x = ZoneBoundaries.BoardWidth / 2 - 1 + (i % 3);
int y = ZoneBoundaries.BoardHeight - 3 - (i / 3);
SpawnUnit(Team.Red, new Vector2Int(x, y));
}
}
GridUnit SpawnUnit(Team team, Vector2Int position)
{
Color color = team == Team.Blue ? BlueUnitColor : RedUnitColor;
var go = CreateSprite($"{team}Unit_{nextUnitId}", color, CellSize * 0.8f, CellSize * 0.8f);
go.GetComponent<SpriteRenderer>().sortingOrder = 10;
var unit = new GridUnit(nextUnitId++, team, position, go);
unit.SetWorldPosition(GridToWorld(position));
cellOccupants[position.x, position.y] = unit;
unitPositions[unit] = position;
if (team == Team.Blue)
blueUnits.Add(unit);
else
redUnits.Add(unit);
return unit;
}
void CreateFlags()
{
// Blue flag at center bottom of blue zone
blueFlagHome = new Vector2Int(ZoneBoundaries.BoardWidth / 2, 1);
blueFlagPosition = blueFlagHome;
blueFlagGO = CreateSprite("BlueFlag", FlagBlueColor, CellSize * 0.5f, CellSize * 0.8f);
blueFlagGO.GetComponent<SpriteRenderer>().sortingOrder = 5;
blueFlagGO.transform.position = GridToWorld(blueFlagPosition);
// Red flag at center top of red zone
redFlagHome = new Vector2Int(ZoneBoundaries.BoardWidth / 2, ZoneBoundaries.BoardHeight - 2);
redFlagPosition = redFlagHome;
redFlagGO = CreateSprite("RedFlag", FlagRedColor, CellSize * 0.5f, CellSize * 0.8f);
redFlagGO.GetComponent<SpriteRenderer>().sortingOrder = 5;
redFlagGO.transform.position = GridToWorld(redFlagPosition);
}
void Update()
{
if (gameOver) return;
// AI handles red team
if (currentTeam == Team.Red)
{
return; // AI takes control in GridAI.Update()
}
HandleInput();
}
void HandleInput()
{
var mouse = Mouse.current;
if (mouse == null) return;
if (mouse.leftButton.wasPressedThisFrame)
{
Vector2 worldPos = Camera.main.ScreenToWorldPoint(mouse.position.ReadValue());
Vector2Int gridPos = WorldToGrid(worldPos);
if (!IsInBounds(gridPos)) return;
// If we have a unit selected and clicked on a valid move, execute it
if (selectedUnit != null && validMoves.Contains(gridPos))
{
ExecutePlayerMove(selectedUnit, gridPos);
return;
}
// Check if we clicked on a friendly unit to select it
var unitAtCell = cellOccupants[gridPos.x, gridPos.y];
if (unitAtCell != null && unitAtCell.Team == currentTeam && !unitAtCell.IsTaggedOut)
{
// Check if unit can move this turn (defense speed nerf)
if (IsDefending(unitAtCell) && unitAtCell.ConsecutiveDefenseMoves % 4 == 3)
{
// This unit must skip their move
Debug.Log($"Unit {unitAtCell.UnitId} must skip move (defense speed nerf)");
SelectUnit(null);
return;
}
SelectUnit(unitAtCell);
}
else
{
// Clicked on empty cell or enemy - deselect
SelectUnit(null);
}
}
// Right click to deselect
if (mouse.rightButton.wasPressedThisFrame)
{
SelectUnit(null);
}
}
void SelectUnit(GridUnit unit)
{
selectedUnit = unit;
ClearValidMoveHighlights();
if (unit == null)
{
if (selectionHighlight != null)
selectionHighlight.SetActive(false);
validMoves.Clear();
return;
}
// Show selection highlight
if (selectionHighlight == null)
{
selectionHighlight = CreateSprite("SelectionHighlight", SelectedColor, CellSize * 0.95f, CellSize * 0.95f);
selectionHighlight.GetComponent<SpriteRenderer>().sortingOrder = 1;
}
selectionHighlight.SetActive(true);
selectionHighlight.transform.position = GridToWorld(unit.GridPosition);
// Calculate and show valid moves
validMoves = GetValidMoves(unit);
foreach (var move in validMoves)
{
var highlight = CreateSprite("ValidMove", ValidMoveColor, CellSize * 0.9f, CellSize * 0.9f);
highlight.GetComponent<SpriteRenderer>().sortingOrder = 0;
highlight.transform.position = GridToWorld(move);
validMoveHighlights.Add(highlight);
}
}
void ClearValidMoveHighlights()
{
foreach (var highlight in validMoveHighlights)
{
Destroy(highlight);
}
validMoveHighlights.Clear();
}
public List<Vector2Int> GetValidMoves(GridUnit unit)
{
var moves = new List<Vector2Int>();
if (unit.IsTaggedOut) return moves;
var pos = unit.GridPosition;
bool isDefending = IsDefending(unit);
// Determine valid directions based on zone
Vector2Int[] directions;
if (isDefending)
{
// Diagonal movement (8 directions) in own defense zone
directions = new Vector2Int[]
{
new(-1, -1), new(0, -1), new(1, -1),
new(-1, 0), new(1, 0),
new(-1, 1), new(0, 1), new(1, 1)
};
}
else
{
// Orthogonal movement (4 directions) in offense/neutral zones
directions = new Vector2Int[]
{
new(0, -1), new(-1, 0), new(1, 0), new(0, 1)
};
}
foreach (var dir in directions)
{
var newPos = pos + dir;
if (IsInBounds(newPos))
{
// Can move to empty cells or cells with enemies (will trigger collision)
var occupant = cellOccupants[newPos.x, newPos.y];
if (occupant == null || occupant.Team != unit.Team)
{
moves.Add(newPos);
}
}
}
return moves;
}
void ExecutePlayerMove(GridUnit unit, Vector2Int destination)
{
ExecuteMove(unit, destination);
EndTurn();
}
void ExecuteMove(GridUnit unit, Vector2Int destination)
{
var startPos = unit.GridPosition;
bool wasDefending = IsDefending(unit);
// Move unit
MoveUnit(unit, destination);
// Update defense move counter
bool nowDefending = IsDefending(unit);
if (nowDefending)
{
unit.ConsecutiveDefenseMoves++;
}
else
{
unit.ConsecutiveDefenseMoves = 0;
}
// Check collision
CheckCollision(destination);
// Check flag pickup
CheckFlagPickup(unit);
// Check scoring
CheckScoring(unit);
// Update visual
unit.SetWorldPosition(GridToWorld(destination));
UpdateFlagVisuals();
}
public void MoveUnit(GridUnit unit, Vector2Int to)
{
var from = unitPositions[unit];
if (cellOccupants[from.x, from.y] == unit)
{
cellOccupants[from.x, from.y] = null;
}
cellOccupants[to.x, to.y] = unit;
unitPositions[unit] = to;
unit.GridPosition = to;
}
void CheckCollision(Vector2Int cell)
{
// Find all units at this cell
var unitsHere = new List<GridUnit>();
foreach (var unit in blueUnits)
{
if (!unit.IsTaggedOut && unit.GridPosition == cell)
unitsHere.Add(unit);
}
foreach (var unit in redUnits)
{
if (!unit.IsTaggedOut && unit.GridPosition == cell)
unitsHere.Add(unit);
}
if (unitsHere.Count < 2) return;
// Check for enemy collisions
for (int i = 0; i < unitsHere.Count; i++)
{
for (int j = i + 1; j < unitsHere.Count; j++)
{
var a = unitsHere[i];
var b = unitsHere[j];
if (a.Team != b.Team)
{
ResolveCollision(a, b, cell);
}
}
}
}
void ResolveCollision(GridUnit unitA, GridUnit unitB, Vector2Int cell)
{
if (unitA.IsTaggedOut || unitB.IsTaggedOut) return;
var zoneOwner = GetZoneOwner(cell.y);
// Defender in their zone always wins
if (unitA.Team == Team.Blue && zoneOwner == ZoneOwner.Blue)
TagOut(unitB);
else if (unitB.Team == Team.Blue && zoneOwner == ZoneOwner.Blue)
TagOut(unitA);
else if (unitA.Team == Team.Red && zoneOwner == ZoneOwner.Red)
TagOut(unitB);
else if (unitB.Team == Team.Red && zoneOwner == ZoneOwner.Red)
TagOut(unitA);
else
{
// Neutral zone: lower UnitId wins (deterministic)
if (unitA.UnitId < unitB.UnitId)
TagOut(unitB);
else
TagOut(unitA);
}
}
void TagOut(GridUnit unit)
{
if (unit.IsTaggedOut) return;
unit.IsTaggedOut = true;
unit.RespawnTurnsRemaining = RespawnDelay;
unit.ConsecutiveDefenseMoves = 0;
// Drop flag if carrying
if (blueFlagCarrier == unit)
{
blueFlagCarrier = null;
// Flag stays at current position
Debug.Log("Blue flag dropped!");
}
if (redFlagCarrier == unit)
{
redFlagCarrier = null;
Debug.Log("Red flag dropped!");
}
// Remove from cell
var pos = unit.GridPosition;
if (cellOccupants[pos.x, pos.y] == unit)
{
cellOccupants[pos.x, pos.y] = null;
}
// Fade out visual
if (unit.SpriteRenderer != null)
{
var color = unit.SpriteRenderer.color;
color.a = 0.3f;
unit.SpriteRenderer.color = color;
}
Debug.Log($"{unit.Team} unit {unit.UnitId} tagged out!");
}
void CheckFlagPickup(GridUnit unit)
{
if (unit.IsTaggedOut) return;
var pos = unit.GridPosition;
// Blue unit can pick up red flag
if (unit.Team == Team.Blue && pos == redFlagPosition && redFlagCarrier == null)
{
redFlagCarrier = unit;
Debug.Log($"Blue unit picked up red flag!");
}
// Red unit can pick up blue flag
else if (unit.Team == Team.Red && pos == blueFlagPosition && blueFlagCarrier == null)
{
blueFlagCarrier = unit;
Debug.Log($"Red unit picked up blue flag!");
}
// Return own flag if it's dropped and friendly unit touches it
if (unit.Team == Team.Blue && pos == blueFlagPosition && blueFlagCarrier == null && blueFlagPosition != blueFlagHome)
{
blueFlagPosition = blueFlagHome;
Debug.Log("Blue flag returned home!");
}
if (unit.Team == Team.Red && pos == redFlagPosition && redFlagCarrier == null && redFlagPosition != redFlagHome)
{
redFlagPosition = redFlagHome;
Debug.Log("Red flag returned home!");
}
}
void CheckScoring(GridUnit unit)
{
if (unit.IsTaggedOut) return;
var pos = unit.GridPosition;
var zone = GetZoneOwner(pos.y);
// Blue scores by bringing red flag to blue zone
if (unit.Team == Team.Blue && redFlagCarrier == unit && zone == ZoneOwner.Blue)
{
Score(Team.Blue);
}
// Red scores by bringing blue flag to red zone
else if (unit.Team == Team.Red && blueFlagCarrier == unit && zone == ZoneOwner.Red)
{
Score(Team.Red);
}
}
void Score(Team team)
{
if (team == Team.Blue)
{
blueScore++;
Debug.Log($"Blue scores! {blueScore} - {redScore}");
}
else
{
redScore++;
Debug.Log($"Red scores! {blueScore} - {redScore}");
}
UpdateUI();
if (blueScore >= WinScore)
{
EndGame(Team.Blue);
}
else if (redScore >= WinScore)
{
EndGame(Team.Red);
}
else
{
ResetRound();
}
}
void ResetRound()
{
// Return flags
blueFlagPosition = blueFlagHome;
redFlagPosition = redFlagHome;
blueFlagCarrier = null;
redFlagCarrier = null;
// Respawn all units
for (int i = 0; i < blueUnits.Count; i++)
{
var unit = blueUnits[i];
int x = ZoneBoundaries.BoardWidth / 2 - 1 + (i % 3);
int y = 2 + (i / 3);
RespawnUnit(unit, new Vector2Int(x, y));
}
for (int i = 0; i < redUnits.Count; i++)
{
var unit = redUnits[i];
int x = ZoneBoundaries.BoardWidth / 2 - 1 + (i % 3);
int y = ZoneBoundaries.BoardHeight - 3 - (i / 3);
RespawnUnit(unit, new Vector2Int(x, y));
}
UpdateFlagVisuals();
RecalculateVisibility();
}
void RespawnUnit(GridUnit unit, Vector2Int position)
{
unit.IsTaggedOut = false;
unit.RespawnTurnsRemaining = 0;
unit.ConsecutiveDefenseMoves = 0;
// Clear old position
var oldPos = unit.GridPosition;
if (cellOccupants[oldPos.x, oldPos.y] == unit)
{
cellOccupants[oldPos.x, oldPos.y] = null;
}
// Find nearest empty cell if position is occupied
if (cellOccupants[position.x, position.y] != null)
{
position = FindNearestEmptyCell(position);
}
unit.GridPosition = position;
unitPositions[unit] = position;
cellOccupants[position.x, position.y] = unit;
unit.SetWorldPosition(GridToWorld(position));
// Restore visual
if (unit.SpriteRenderer != null)
{
var color = unit.SpriteRenderer.color;
color.a = 1f;
unit.SpriteRenderer.color = color;
}
}
Vector2Int FindNearestEmptyCell(Vector2Int center)
{
for (int radius = 1; radius < 10; radius++)
{
for (int dx = -radius; dx <= radius; dx++)
{
for (int dy = -radius; dy <= radius; dy++)
{
var pos = center + new Vector2Int(dx, dy);
if (IsInBounds(pos) && cellOccupants[pos.x, pos.y] == null)
{
return pos;
}
}
}
}
return center;
}
void EndTurn()
{
SelectUnit(null);
turnNumber++;
// Process respawn timers
ProcessRespawns();
// Recalculate visibility
RecalculateVisibility();
// Switch teams
currentTeam = currentTeam == Team.Blue ? Team.Red : Team.Blue;
UpdateUI();
Debug.Log($"Turn {turnNumber}: {currentTeam}'s turn");
}
void ProcessRespawns()
{
foreach (var unit in blueUnits)
{
if (unit.IsTaggedOut)
{
unit.RespawnTurnsRemaining--;
if (unit.RespawnTurnsRemaining <= 0)
{
int idx = blueUnits.IndexOf(unit);
int x = ZoneBoundaries.BoardWidth / 2 - 1 + (idx % 3);
int y = 2 + (idx / 3);
RespawnUnit(unit, new Vector2Int(x, y));
Debug.Log($"Blue unit {unit.UnitId} respawned!");
}
}
}
foreach (var unit in redUnits)
{
if (unit.IsTaggedOut)
{
unit.RespawnTurnsRemaining--;
if (unit.RespawnTurnsRemaining <= 0)
{
int idx = redUnits.IndexOf(unit);
int x = ZoneBoundaries.BoardWidth / 2 - 1 + (idx % 3);
int y = ZoneBoundaries.BoardHeight - 3 - (idx / 3);
RespawnUnit(unit, new Vector2Int(x, y));
Debug.Log($"Red unit {unit.UnitId} respawned!");
}
}
}
}
void EndGame(Team winner)
{
gameOver = true;
if (gameOverText != null)
{
gameOverText.text = winner == Team.Blue ? "BLUE WINS!" : "RED WINS!";
gameOverText.color = winner == Team.Blue ? Color.blue : Color.red;
gameOverText.gameObject.SetActive(true);
}
Debug.Log($"{winner} wins!");
}
void UpdateUI()
{
if (scoreText != null)
{
scoreText.text = $"{blueScore} - {redScore}";
}
if (turnText != null)
{
string turnIndicator = currentTeam == Team.Blue ? "BLUE'S TURN" : "RED'S TURN";
turnText.text = turnIndicator;
turnText.color = currentTeam == Team.Blue ? Color.blue : Color.red;
}
}
void UpdateFlagVisuals()
{
// Blue flag follows carrier or stays at position
if (blueFlagCarrier != null && !blueFlagCarrier.IsTaggedOut)
{
blueFlagPosition = blueFlagCarrier.GridPosition;
blueFlagGO.transform.position = GridToWorld(blueFlagPosition) + new Vector2(0.2f, 0.2f);
}
else
{
blueFlagGO.transform.position = GridToWorld(blueFlagPosition);
}
// Red flag follows carrier or stays at position
if (redFlagCarrier != null && !redFlagCarrier.IsTaggedOut)
{
redFlagPosition = redFlagCarrier.GridPosition;
redFlagGO.transform.position = GridToWorld(redFlagPosition) + new Vector2(0.2f, 0.2f);
}
else
{
redFlagGO.transform.position = GridToWorld(redFlagPosition);
}
// Apply fog of war visibility to flags
bool blueFlagVisible = visibleToBlue.Contains(blueFlagPosition) || blueFlagCarrier != null;
bool redFlagVisible = visibleToBlue.Contains(redFlagPosition) || redFlagCarrier != null;
// For now, in single-player, Blue is the human player, so we apply Blue's visibility
redFlagGO.GetComponent<SpriteRenderer>().enabled = redFlagVisible;
// Blue flag is always visible to blue player
blueFlagGO.GetComponent<SpriteRenderer>().enabled = true;
}
// Fog of War
void RecalculateVisibility()
{
visibleToBlue.Clear();
visibleToRed.Clear();
foreach (var unit in blueUnits)
{
if (unit.IsTaggedOut) continue;
AddVisibleCells(unit.GridPosition, visibleToBlue);
}
foreach (var unit in redUnits)
{
if (unit.IsTaggedOut) continue;
AddVisibleCells(unit.GridPosition, visibleToRed);
}
// Update enemy visibility based on Blue's vision (human player)
foreach (var enemy in redUnits)
{
bool visible = visibleToBlue.Contains(enemy.GridPosition) || enemy.IsTaggedOut;
if (enemy.SpriteRenderer != null)
{
enemy.SpriteRenderer.enabled = visible;
}
}
UpdateFlagVisuals();
}
void AddVisibleCells(Vector2Int center, HashSet<Vector2Int> visibleSet)
{
for (int dx = -VisionRadius; dx <= VisionRadius; dx++)
{
for (int dy = -VisionRadius; dy <= VisionRadius; dy++)
{
var cell = center + new Vector2Int(dx, dy);
if (IsInBounds(cell))
{
visibleSet.Add(cell);
}
}
}
}
// AI interface
public void ExecuteAIMove(GridUnit unit, Vector2Int destination)
{
if (gameOver) return;
if (unit.Team != currentTeam) return;
// Check speed nerf
if (IsDefending(unit) && unit.ConsecutiveDefenseMoves % 4 == 3)
{
Debug.Log($"AI unit {unit.UnitId} skips move (defense speed nerf)");
EndTurn();
return;
}
ExecuteMove(unit, destination);
EndTurn();
}
public void AISkipTurn()
{
if (gameOver) return;
EndTurn();
}
public Team GetCurrentTeam() => currentTeam;
public bool IsGameOver() => gameOver;
public List<GridUnit> GetUnits(Team team) => team == Team.Blue ? blueUnits : redUnits;
public HashSet<Vector2Int> GetVisibleCells(Team team) => team == Team.Blue ? visibleToBlue : visibleToRed;
public Vector2Int GetEnemyFlagPosition(Team team) => team == Team.Blue ? redFlagPosition : blueFlagPosition;
public GridUnit GetFlagCarrier(Team flagTeam) => flagTeam == Team.Blue ? blueFlagCarrier : redFlagCarrier;
// Utility functions
public ZoneOwner GetZoneOwner(int y) => y switch
{
< ZoneBoundaries.TeamBlueDefenseEnd => ZoneOwner.Blue,
< ZoneBoundaries.NeutralEnd => ZoneOwner.Neutral,
_ => ZoneOwner.Red
};
public bool IsDefending(GridUnit unit)
{
var zone = GetZoneOwner(unit.GridPosition.y);
return (unit.Team == Team.Blue && zone == ZoneOwner.Blue) ||
(unit.Team == Team.Red && zone == ZoneOwner.Red);
}
public bool IsInBounds(Vector2Int pos) =>
pos.x >= 0 && pos.x < ZoneBoundaries.BoardWidth &&
pos.y >= 0 && pos.y < ZoneBoundaries.BoardHeight;
public Vector2 GridToWorld(Vector2Int gridPos)
{
// Center the board
float offsetX = -ZoneBoundaries.BoardWidth * CellSize / 2f + CellSize / 2f;
float offsetY = -ZoneBoundaries.BoardHeight * CellSize / 2f + CellSize / 2f;
return new Vector2(
gridPos.x * CellSize + offsetX,
gridPos.y * CellSize + offsetY
);
}
public Vector2Int WorldToGrid(Vector2 worldPos)
{
float offsetX = -ZoneBoundaries.BoardWidth * CellSize / 2f;
float offsetY = -ZoneBoundaries.BoardHeight * CellSize / 2f;
int x = Mathf.FloorToInt((worldPos.x - offsetX) / CellSize);
int y = Mathf.FloorToInt((worldPos.y - offsetY) / CellSize);
return new Vector2Int(x, y);
}
// Helper to create a colored sprite
GameObject CreateSprite(string name, Color color, float width, float height)
{
var go = new GameObject(name);
var sr = go.AddComponent<SpriteRenderer>();
sr.sprite = GetRectSprite();
sr.color = color;
go.transform.localScale = new Vector3(width, height, 1);
return go;
}
static Sprite cachedSprite;
static Sprite GetRectSprite()
{
if (cachedSprite != null) return cachedSprite;
var tex = new Texture2D(1, 1);
tex.SetPixel(0, 0, Color.white);
tex.Apply();
cachedSprite = Sprite.Create(tex, new Rect(0, 0, 1, 1), new Vector2(0.5f, 0.5f), 1);
return cachedSprite;
}
}