539 lines
18 KiB
C#
539 lines
18 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using TMPro;
|
|
|
|
public class Game : MonoBehaviour
|
|
{
|
|
// Auto-bootstrap when game starts - no Editor setup needed
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
|
static void Bootstrap()
|
|
{
|
|
if (Instance != null) return;
|
|
|
|
var gameGO = new GameObject("Game");
|
|
gameGO.AddComponent<Game>();
|
|
}
|
|
|
|
// ===========================================
|
|
// CONFIGURATION - All tunable constants here
|
|
// ===========================================
|
|
|
|
// Gameplay
|
|
public const int UnitsPerTeam = 3;
|
|
public const float UnitSpeed = 5f;
|
|
public const float UnitSize = 1f;
|
|
public const float UnitColliderRadius = 0.5f;
|
|
public const float MinGapSize = 2f; // Minimum gap between obstacles
|
|
|
|
// Vision & Detection
|
|
public const float VisionRadius = 6f;
|
|
public const float TagRadius = 0.75f;
|
|
public const float FlagPickupRadius = 0.75f;
|
|
|
|
// Timing
|
|
public const float RespawnDelay = 5f;
|
|
public const float FlagReturnDelay = 5f;
|
|
|
|
// Scoring
|
|
public const int WinScore = 3;
|
|
|
|
// Map - Large neighborhood (19:9 landscape, requires zoom)
|
|
public const float MapWidth = 80f;
|
|
public const float MapHeight = 40f;
|
|
|
|
// Camera
|
|
public const float CameraMinZoom = 8f;
|
|
public const float CameraMaxZoom = 22f;
|
|
public const float CameraStartZoom = 15f;
|
|
|
|
// Bases
|
|
public const float BaseSize = 6f;
|
|
public const float BaseInset = 6f;
|
|
|
|
// Neighborhood layout
|
|
public const float StreetWidth = 4f;
|
|
public const float HouseWidth = 5f;
|
|
public const float HouseDepth = 4f;
|
|
public const float YardDepth = 3f;
|
|
|
|
// References set during setup
|
|
public static Game Instance { get; private set; }
|
|
|
|
public List<Unit> playerUnits = new();
|
|
public List<Unit> enemyUnits = new();
|
|
public Transform playerBase;
|
|
public Transform enemyBase;
|
|
public Flag playerFlag;
|
|
public Flag enemyFlag;
|
|
|
|
int playerScore;
|
|
int enemyScore;
|
|
bool gameOver;
|
|
|
|
TextMeshProUGUI scoreText;
|
|
TextMeshProUGUI gameOverText;
|
|
|
|
void Awake()
|
|
{
|
|
Instance = this;
|
|
}
|
|
|
|
void Start()
|
|
{
|
|
// Seed random for consistent neighborhood layout
|
|
Random.InitState(42);
|
|
|
|
SetupCamera();
|
|
CreateGround();
|
|
CreateObstacles();
|
|
CreateBases();
|
|
CreateFlags();
|
|
SpawnUnits();
|
|
CreateUI();
|
|
SetupSystems();
|
|
}
|
|
|
|
void SetupCamera()
|
|
{
|
|
var cam = Camera.main;
|
|
if (cam != null)
|
|
{
|
|
cam.orthographic = true;
|
|
cam.orthographicSize = CameraStartZoom;
|
|
cam.transform.position = new Vector3(0, 0, -10);
|
|
|
|
// Add camera controller for zoom/pan
|
|
var controller = cam.gameObject.AddComponent<CameraController>();
|
|
controller.minZoom = CameraMinZoom;
|
|
controller.maxZoom = CameraMaxZoom;
|
|
}
|
|
}
|
|
|
|
void CreateGround()
|
|
{
|
|
// Grass background
|
|
var ground = CreateSprite("Ground", new Color(0.2f, 0.35f, 0.15f), MapWidth, MapHeight);
|
|
ground.transform.position = Vector3.zero;
|
|
var sr = ground.GetComponent<SpriteRenderer>();
|
|
sr.sortingOrder = -10;
|
|
|
|
// Create streets (darker gray paths)
|
|
Color streetColor = new Color(0.3f, 0.3f, 0.32f);
|
|
|
|
// Main horizontal street (center)
|
|
var mainStreet = CreateSprite("MainStreet", streetColor, MapWidth - 20f, StreetWidth);
|
|
mainStreet.transform.position = new Vector3(0, 0, 0);
|
|
mainStreet.GetComponent<SpriteRenderer>().sortingOrder = -9;
|
|
|
|
// Upper horizontal street
|
|
var upperStreet = CreateSprite("UpperStreet", streetColor, MapWidth - 30f, StreetWidth);
|
|
upperStreet.transform.position = new Vector3(0, 10f, 0);
|
|
upperStreet.GetComponent<SpriteRenderer>().sortingOrder = -9;
|
|
|
|
// Lower horizontal street
|
|
var lowerStreet = CreateSprite("LowerStreet", streetColor, MapWidth - 30f, StreetWidth);
|
|
lowerStreet.transform.position = new Vector3(0, -10f, 0);
|
|
lowerStreet.GetComponent<SpriteRenderer>().sortingOrder = -9;
|
|
|
|
// Vertical cross streets
|
|
float[] crossStreetX = { -20f, -8f, 8f, 20f };
|
|
int streetIndex = 0;
|
|
foreach (float x in crossStreetX)
|
|
{
|
|
var crossStreet = CreateSprite($"CrossStreet_{streetIndex++}", streetColor, StreetWidth, MapHeight - 10f);
|
|
crossStreet.transform.position = new Vector3(x, 0, 0);
|
|
crossStreet.GetComponent<SpriteRenderer>().sortingOrder = -9;
|
|
}
|
|
}
|
|
|
|
void CreateObstacles()
|
|
{
|
|
// Create realistic neighborhood layout for 80x40 map
|
|
// Pattern: horizontal streets with house rows, vertical cross-streets
|
|
// All gaps minimum 2.5 units for unit passage
|
|
|
|
var obstacles = new List<(Vector2 pos, Vector2 size, Color color)>();
|
|
|
|
// Street grid creates the main routes
|
|
// Houses line the streets with backyards creating sneaky routes
|
|
|
|
// === ROW 1 (Top) - y around 12-16 ===
|
|
CreateHouseRow(obstacles, y: 14f, xStart: -32f, xEnd: 32f, houseWidth: 5f, gapWidth: 3f, yardSide: -1);
|
|
|
|
// === ROW 2 (Upper-mid) - y around 4-8 ===
|
|
CreateHouseRow(obstacles, y: 6f, xStart: -28f, xEnd: 28f, houseWidth: 6f, gapWidth: 3.5f, yardSide: 1);
|
|
|
|
// === ROW 3 (Center) - y around -2 to 2 ===
|
|
// Staggered houses creating interesting chokepoints
|
|
CreateHouseRow(obstacles, y: -1f, xStart: -25f, xEnd: 25f, houseWidth: 5f, gapWidth: 4f, yardSide: -1);
|
|
|
|
// === ROW 4 (Lower-mid) - y around -8 to -4 ===
|
|
CreateHouseRow(obstacles, y: -8f, xStart: -28f, xEnd: 28f, houseWidth: 6f, gapWidth: 3.5f, yardSide: 1);
|
|
|
|
// === ROW 5 (Bottom) - y around -12 to -16 ===
|
|
CreateHouseRow(obstacles, y: -14f, xStart: -32f, xEnd: 32f, houseWidth: 5f, gapWidth: 3f, yardSide: 1);
|
|
|
|
// === Add some fences/hedges in backyards for extra cover ===
|
|
// These create the sneaky backyard routes
|
|
AddBackyardObstacles(obstacles);
|
|
|
|
// Create all obstacles
|
|
int houseIndex = 0;
|
|
foreach (var (pos, size, color) in obstacles)
|
|
{
|
|
var house = CreateSprite($"House_{houseIndex++}", color, size.x, size.y);
|
|
house.transform.position = new Vector3(pos.x, pos.y, 0);
|
|
|
|
var collider = house.AddComponent<BoxCollider2D>();
|
|
collider.size = Vector2.one;
|
|
|
|
var sr = house.GetComponent<SpriteRenderer>();
|
|
sr.sortingOrder = -5;
|
|
}
|
|
}
|
|
|
|
void CreateHouseRow(List<(Vector2, Vector2, Color)> obstacles, float y, float xStart, float xEnd,
|
|
float houseWidth, float gapWidth, int yardSide)
|
|
{
|
|
Color houseColor = new Color(0.4f, 0.35f, 0.3f); // Brown-ish houses
|
|
float houseDepth = 4f;
|
|
float x = xStart;
|
|
|
|
while (x < xEnd - houseWidth)
|
|
{
|
|
// Vary house sizes slightly for visual interest
|
|
float w = houseWidth + Random.Range(-0.5f, 0.5f);
|
|
float h = houseDepth + Random.Range(-0.3f, 0.3f);
|
|
|
|
// Skip houses near bases (leave clear zones)
|
|
if (Mathf.Abs(x) > 12f || Mathf.Abs(y) < 10f)
|
|
{
|
|
obstacles.Add((new Vector2(x + w / 2f, y), new Vector2(w, h), houseColor));
|
|
}
|
|
|
|
x += w + gapWidth;
|
|
}
|
|
}
|
|
|
|
void AddBackyardObstacles(List<(Vector2, Vector2, Color)> obstacles)
|
|
{
|
|
Color fenceColor = new Color(0.5f, 0.4f, 0.3f); // Fence brown
|
|
Color hedgeColor = new Color(0.15f, 0.35f, 0.15f); // Hedge green
|
|
|
|
// Fences between yards (vertical) - create backyard maze
|
|
float[] fenceXPositions = { -22f, -14f, -6f, 6f, 14f, 22f };
|
|
foreach (float fx in fenceXPositions)
|
|
{
|
|
// Upper backyard fences
|
|
if (Random.value > 0.3f)
|
|
obstacles.Add((new Vector2(fx, 10f), new Vector2(0.5f, 3f), fenceColor));
|
|
// Lower backyard fences
|
|
if (Random.value > 0.3f)
|
|
obstacles.Add((new Vector2(fx, -10f), new Vector2(0.5f, 3f), fenceColor));
|
|
}
|
|
|
|
// Hedges along some property lines (horizontal)
|
|
float[] hedgeYPositions = { 10f, 2.5f, -4f, -11f };
|
|
foreach (float hy in hedgeYPositions)
|
|
{
|
|
float hx = Random.Range(-20f, 20f);
|
|
if (Mathf.Abs(hx) > 8f) // Not too close to center
|
|
{
|
|
obstacles.Add((new Vector2(hx, hy), new Vector2(4f, 1f), hedgeColor));
|
|
}
|
|
}
|
|
|
|
// Some sheds/garages in backyards
|
|
Color shedColor = new Color(0.45f, 0.4f, 0.35f);
|
|
Vector2[] shedPositions = {
|
|
new(-18f, 10f), new(-10f, -10f), new(8f, 10f), new(16f, -10f),
|
|
new(-26f, 2f), new(26f, -3f)
|
|
};
|
|
foreach (var pos in shedPositions)
|
|
{
|
|
if (Random.value > 0.4f)
|
|
obstacles.Add((pos, new Vector2(2.5f, 2.5f), shedColor));
|
|
}
|
|
}
|
|
|
|
void CreateBases()
|
|
{
|
|
// Player base - left side
|
|
var playerBaseGO = CreateSprite("PlayerBase", new Color(0.2f, 0.3f, 0.8f, 0.5f), BaseSize, BaseSize);
|
|
playerBaseGO.transform.position = new Vector3(-MapWidth / 2f + BaseInset, 0, 0);
|
|
playerBase = playerBaseGO.transform;
|
|
var sr1 = playerBaseGO.GetComponent<SpriteRenderer>();
|
|
sr1.sortingOrder = -8;
|
|
|
|
var playerBaseTrigger = playerBaseGO.AddComponent<BoxCollider2D>();
|
|
playerBaseTrigger.isTrigger = true;
|
|
playerBaseTrigger.size = Vector2.one;
|
|
|
|
var playerBaseZone = playerBaseGO.AddComponent<BaseZone>();
|
|
playerBaseZone.team = Unit.Team.Player;
|
|
|
|
// Enemy base - right side
|
|
var enemyBaseGO = CreateSprite("EnemyBase", new Color(0.8f, 0.2f, 0.2f, 0.5f), BaseSize, BaseSize);
|
|
enemyBaseGO.transform.position = new Vector3(MapWidth / 2f - BaseInset, 0, 0);
|
|
enemyBase = enemyBaseGO.transform;
|
|
var sr2 = enemyBaseGO.GetComponent<SpriteRenderer>();
|
|
sr2.sortingOrder = -8;
|
|
|
|
var enemyBaseTrigger = enemyBaseGO.AddComponent<BoxCollider2D>();
|
|
enemyBaseTrigger.isTrigger = true;
|
|
enemyBaseTrigger.size = Vector2.one;
|
|
|
|
var enemyBaseZone = enemyBaseGO.AddComponent<BaseZone>();
|
|
enemyBaseZone.team = Unit.Team.Enemy;
|
|
}
|
|
|
|
void CreateFlags()
|
|
{
|
|
// Player flag at player base
|
|
var playerFlagGO = CreateSprite("PlayerFlag", new Color(0.3f, 0.5f, 1f), 1f, 1.5f);
|
|
playerFlagGO.transform.position = playerBase.position;
|
|
playerFlag = playerFlagGO.AddComponent<Flag>();
|
|
playerFlag.team = Unit.Team.Player;
|
|
playerFlag.homePosition = playerBase;
|
|
|
|
var flagCollider1 = playerFlagGO.AddComponent<CircleCollider2D>();
|
|
flagCollider1.isTrigger = true;
|
|
flagCollider1.radius = FlagPickupRadius;
|
|
|
|
var rb1 = playerFlagGO.AddComponent<Rigidbody2D>();
|
|
rb1.bodyType = RigidbodyType2D.Kinematic;
|
|
|
|
var sr1 = playerFlagGO.GetComponent<SpriteRenderer>();
|
|
sr1.sortingOrder = 5;
|
|
|
|
// Enemy flag at enemy base
|
|
var enemyFlagGO = CreateSprite("EnemyFlag", new Color(1f, 0.3f, 0.3f), 1f, 1.5f);
|
|
enemyFlagGO.transform.position = enemyBase.position;
|
|
enemyFlag = enemyFlagGO.AddComponent<Flag>();
|
|
enemyFlag.team = Unit.Team.Enemy;
|
|
enemyFlag.homePosition = enemyBase;
|
|
|
|
var flagCollider2 = enemyFlagGO.AddComponent<CircleCollider2D>();
|
|
flagCollider2.isTrigger = true;
|
|
flagCollider2.radius = FlagPickupRadius;
|
|
|
|
var rb2 = enemyFlagGO.AddComponent<Rigidbody2D>();
|
|
rb2.bodyType = RigidbodyType2D.Kinematic;
|
|
|
|
var sr2 = enemyFlagGO.GetComponent<SpriteRenderer>();
|
|
sr2.sortingOrder = 5;
|
|
}
|
|
|
|
void SpawnUnits()
|
|
{
|
|
// Spawn player units near player base
|
|
for (int i = 0; i < UnitsPerTeam; i++)
|
|
{
|
|
var offset = GetUnitSpawnOffset(i, UnitsPerTeam, isPlayer: true);
|
|
var unit = CreateUnit($"PlayerUnit_{i}", Unit.Team.Player, playerBase.position + offset);
|
|
playerUnits.Add(unit);
|
|
}
|
|
|
|
// Spawn enemy units near enemy base
|
|
for (int i = 0; i < UnitsPerTeam; i++)
|
|
{
|
|
var offset = GetUnitSpawnOffset(i, UnitsPerTeam, isPlayer: false);
|
|
var unit = CreateUnit($"EnemyUnit_{i}", Unit.Team.Enemy, enemyBase.position + offset);
|
|
enemyUnits.Add(unit);
|
|
}
|
|
}
|
|
|
|
Vector3 GetUnitSpawnOffset(int index, int total, bool isPlayer)
|
|
{
|
|
float spacing = 1.8f;
|
|
float yOffset = (index - (total - 1) / 2f) * spacing;
|
|
float xOffset = isPlayer ? 2f : -2f; // Offset toward center of map
|
|
return new Vector3(xOffset, yOffset, 0);
|
|
}
|
|
|
|
Unit CreateUnit(string name, Unit.Team team, Vector3 position)
|
|
{
|
|
Color color = team == Unit.Team.Player ? new Color(0.3f, 0.5f, 1f) : new Color(1f, 0.3f, 0.3f);
|
|
var unitGO = CreateSprite(name, color, UnitSize, UnitSize);
|
|
unitGO.transform.position = position;
|
|
|
|
var unit = unitGO.AddComponent<Unit>();
|
|
unit.team = team;
|
|
|
|
var collider = unitGO.AddComponent<CircleCollider2D>();
|
|
collider.isTrigger = true;
|
|
collider.radius = UnitColliderRadius;
|
|
|
|
var rb = unitGO.AddComponent<Rigidbody2D>();
|
|
rb.bodyType = RigidbodyType2D.Kinematic;
|
|
|
|
var sr = unitGO.GetComponent<SpriteRenderer>();
|
|
sr.sortingOrder = 10;
|
|
|
|
return unit;
|
|
}
|
|
|
|
void CreateUI()
|
|
{
|
|
// Create Canvas
|
|
var canvasGO = new GameObject("Canvas");
|
|
var canvas = canvasGO.AddComponent<Canvas>();
|
|
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
|
canvasGO.AddComponent<UnityEngine.UI.CanvasScaler>();
|
|
canvasGO.AddComponent<UnityEngine.UI.GraphicRaycaster>();
|
|
|
|
// Score text
|
|
var scoreGO = new GameObject("ScoreText");
|
|
scoreGO.transform.SetParent(canvasGO.transform, false);
|
|
scoreText = scoreGO.AddComponent<TextMeshProUGUI>();
|
|
scoreText.text = "0 - 0";
|
|
scoreText.fontSize = 48;
|
|
scoreText.alignment = TextAlignmentOptions.Center;
|
|
scoreText.color = Color.white;
|
|
|
|
var scoreRect = scoreGO.GetComponent<RectTransform>();
|
|
scoreRect.anchorMin = new Vector2(0.5f, 1f);
|
|
scoreRect.anchorMax = new Vector2(0.5f, 1f);
|
|
scoreRect.pivot = new Vector2(0.5f, 1f);
|
|
scoreRect.anchoredPosition = new Vector2(0, -20);
|
|
scoreRect.sizeDelta = new Vector2(200, 60);
|
|
|
|
// Game over text (hidden initially)
|
|
var gameOverGO = new GameObject("GameOverText");
|
|
gameOverGO.transform.SetParent(canvasGO.transform, false);
|
|
gameOverText = gameOverGO.AddComponent<TextMeshProUGUI>();
|
|
gameOverText.text = "";
|
|
gameOverText.fontSize = 72;
|
|
gameOverText.alignment = TextAlignmentOptions.Center;
|
|
gameOverText.color = Color.white;
|
|
gameOverText.gameObject.SetActive(false);
|
|
|
|
var gameOverRect = gameOverGO.GetComponent<RectTransform>();
|
|
gameOverRect.anchorMin = new Vector2(0.5f, 0.5f);
|
|
gameOverRect.anchorMax = new Vector2(0.5f, 0.5f);
|
|
gameOverRect.pivot = new Vector2(0.5f, 0.5f);
|
|
gameOverRect.anchoredPosition = Vector2.zero;
|
|
gameOverRect.sizeDelta = new Vector2(400, 100);
|
|
}
|
|
|
|
void SetupSystems()
|
|
{
|
|
// Add RouteDrawer
|
|
gameObject.AddComponent<RouteDrawer>();
|
|
|
|
// Add Visibility system
|
|
var visibility = gameObject.AddComponent<Visibility>();
|
|
visibility.playerUnits = playerUnits.ToArray();
|
|
visibility.enemyUnits = enemyUnits.ToArray();
|
|
visibility.enemyFlag = enemyFlag;
|
|
|
|
// Add SimpleAI
|
|
var ai = gameObject.AddComponent<SimpleAI>();
|
|
ai.aiUnits = enemyUnits.ToArray();
|
|
ai.playerFlag = playerFlag;
|
|
ai.aiBase = enemyBase;
|
|
}
|
|
|
|
public void Score(Unit.Team team)
|
|
{
|
|
if (gameOver) return;
|
|
|
|
if (team == Unit.Team.Player)
|
|
playerScore++;
|
|
else
|
|
enemyScore++;
|
|
|
|
UpdateScoreUI();
|
|
Debug.Log($"Score: {playerScore} - {enemyScore}");
|
|
|
|
if (playerScore >= WinScore)
|
|
EndGame(true);
|
|
else if (enemyScore >= WinScore)
|
|
EndGame(false);
|
|
else
|
|
ResetRound();
|
|
}
|
|
|
|
void UpdateScoreUI()
|
|
{
|
|
if (scoreText != null)
|
|
scoreText.text = $"{playerScore} - {enemyScore}";
|
|
}
|
|
|
|
void ResetRound()
|
|
{
|
|
// Return flags to home
|
|
playerFlag.ReturnHome();
|
|
enemyFlag.ReturnHome();
|
|
|
|
// Respawn all units at bases
|
|
for (int i = 0; i < playerUnits.Count; i++)
|
|
{
|
|
var offset = GetUnitSpawnOffset(i, playerUnits.Count, isPlayer: true);
|
|
playerUnits[i].ForceRespawn(playerBase.position + offset);
|
|
}
|
|
|
|
for (int i = 0; i < enemyUnits.Count; i++)
|
|
{
|
|
var offset = GetUnitSpawnOffset(i, enemyUnits.Count, isPlayer: false);
|
|
enemyUnits[i].ForceRespawn(enemyBase.position + offset);
|
|
}
|
|
}
|
|
|
|
void EndGame(bool playerWon)
|
|
{
|
|
gameOver = true;
|
|
|
|
if (gameOverText != null)
|
|
{
|
|
gameOverText.text = playerWon ? "YOU WIN!" : "YOU LOSE!";
|
|
gameOverText.color = playerWon ? Color.green : Color.red;
|
|
gameOverText.gameObject.SetActive(true);
|
|
}
|
|
|
|
Debug.Log(playerWon ? "Player wins!" : "Enemy wins!");
|
|
}
|
|
|
|
public Transform GetBase(Unit.Team team)
|
|
{
|
|
return team == Unit.Team.Player ? playerBase : enemyBase;
|
|
}
|
|
|
|
public Flag GetFlag(Unit.Team team)
|
|
{
|
|
return team == Unit.Team.Player ? playerFlag : enemyFlag;
|
|
}
|
|
|
|
// 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 = CreateRectSprite();
|
|
sr.color = color;
|
|
go.transform.localScale = new Vector3(width, height, 1);
|
|
return go;
|
|
}
|
|
|
|
// Create a simple white 1x1 sprite
|
|
static Sprite cachedSprite;
|
|
static Sprite CreateRectSprite()
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Simple component to identify base zones
|
|
public class BaseZone : MonoBehaviour
|
|
{
|
|
public Unit.Team team;
|
|
}
|