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>
906 lines
27 KiB
C#
906 lines
27 KiB
C#
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;
|
|
}
|
|
}
|