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(); } // =========================================== // 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 playerUnits = new(); public List 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(); 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(); 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().sortingOrder = -9; // Upper horizontal street var upperStreet = CreateSprite("UpperStreet", streetColor, MapWidth - 30f, StreetWidth); upperStreet.transform.position = new Vector3(0, 10f, 0); upperStreet.GetComponent().sortingOrder = -9; // Lower horizontal street var lowerStreet = CreateSprite("LowerStreet", streetColor, MapWidth - 30f, StreetWidth); lowerStreet.transform.position = new Vector3(0, -10f, 0); lowerStreet.GetComponent().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().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(); collider.size = Vector2.one; var sr = house.GetComponent(); 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(); sr1.sortingOrder = -8; var playerBaseTrigger = playerBaseGO.AddComponent(); playerBaseTrigger.isTrigger = true; playerBaseTrigger.size = Vector2.one; var playerBaseZone = playerBaseGO.AddComponent(); 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(); sr2.sortingOrder = -8; var enemyBaseTrigger = enemyBaseGO.AddComponent(); enemyBaseTrigger.isTrigger = true; enemyBaseTrigger.size = Vector2.one; var enemyBaseZone = enemyBaseGO.AddComponent(); 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(); playerFlag.team = Unit.Team.Player; playerFlag.homePosition = playerBase; var flagCollider1 = playerFlagGO.AddComponent(); flagCollider1.isTrigger = true; flagCollider1.radius = FlagPickupRadius; var rb1 = playerFlagGO.AddComponent(); rb1.bodyType = RigidbodyType2D.Kinematic; var sr1 = playerFlagGO.GetComponent(); 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(); enemyFlag.team = Unit.Team.Enemy; enemyFlag.homePosition = enemyBase; var flagCollider2 = enemyFlagGO.AddComponent(); flagCollider2.isTrigger = true; flagCollider2.radius = FlagPickupRadius; var rb2 = enemyFlagGO.AddComponent(); rb2.bodyType = RigidbodyType2D.Kinematic; var sr2 = enemyFlagGO.GetComponent(); 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.team = team; var collider = unitGO.AddComponent(); collider.isTrigger = true; collider.radius = UnitColliderRadius; var rb = unitGO.AddComponent(); rb.bodyType = RigidbodyType2D.Kinematic; var sr = unitGO.GetComponent(); sr.sortingOrder = 10; return unit; } void CreateUI() { // Create Canvas var canvasGO = new GameObject("Canvas"); var canvas = canvasGO.AddComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvasGO.AddComponent(); canvasGO.AddComponent(); // Score text var scoreGO = new GameObject("ScoreText"); scoreGO.transform.SetParent(canvasGO.transform, false); scoreText = scoreGO.AddComponent(); scoreText.text = "0 - 0"; scoreText.fontSize = 48; scoreText.alignment = TextAlignmentOptions.Center; scoreText.color = Color.white; var scoreRect = scoreGO.GetComponent(); 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(); gameOverText.text = ""; gameOverText.fontSize = 72; gameOverText.alignment = TextAlignmentOptions.Center; gameOverText.color = Color.white; gameOverText.gameObject.SetActive(false); var gameOverRect = gameOverGO.GetComponent(); 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(); // Add Visibility system var visibility = gameObject.AddComponent(); visibility.playerUnits = playerUnits.ToArray(); visibility.enemyUnits = enemyUnits.ToArray(); visibility.enemyFlag = enemyFlag; // Add SimpleAI var ai = gameObject.AddComponent(); 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(); 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; }