feat/teaser-prototype-playable-core (#1)

Co-authored-by: John Lamb <j.lamb13@gmail.com>
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2026-02-05 02:30:45 +00:00
parent b1b3e4d0b3
commit 0de174eb1a
21 changed files with 1780 additions and 14 deletions

View File

@@ -0,0 +1,187 @@
using UnityEngine;
using UnityEngine.InputSystem;
public class CameraController : MonoBehaviour
{
// Zoom settings
public float minZoom = 5f;
public float maxZoom = 25f;
public float zoomSpeed = 2f;
public float pinchZoomSpeed = 0.1f;
// Pan settings
public float panSpeed = 1f;
Camera cam;
Vector2 lastPanPosition;
bool isPanning;
float lastPinchDistance;
bool isPinching;
// Track if we started on a unit (don't pan if drawing route)
bool startedOnUnit;
void Start()
{
cam = Camera.main;
}
void Update()
{
HandleMouseInput();
HandleTouchInput();
ClampCameraPosition();
}
void HandleMouseInput()
{
var mouse = Mouse.current;
if (mouse == null) return;
// Scroll wheel zoom
float scroll = mouse.scroll.ReadValue().y;
if (Mathf.Abs(scroll) > 0.01f)
{
Zoom(-scroll * zoomSpeed * Time.deltaTime * 10f);
}
// Right-click pan (left-click is for route drawing)
if (mouse.rightButton.wasPressedThisFrame)
{
lastPanPosition = mouse.position.ReadValue();
isPanning = true;
}
else if (mouse.rightButton.wasReleasedThisFrame)
{
isPanning = false;
}
if (isPanning && mouse.rightButton.isPressed)
{
Vector2 currentPos = mouse.position.ReadValue();
Vector2 delta = currentPos - lastPanPosition;
Pan(-delta);
lastPanPosition = currentPos;
}
}
void HandleTouchInput()
{
var touch = Touchscreen.current;
if (touch == null) return;
int touchCount = 0;
foreach (var t in touch.touches)
{
if (t.press.isPressed) touchCount++;
}
if (touchCount == 2)
{
// Two finger pinch zoom and pan
var touch0 = touch.touches[0];
var touch1 = touch.touches[1];
Vector2 pos0 = touch0.position.ReadValue();
Vector2 pos1 = touch1.position.ReadValue();
float currentDistance = Vector2.Distance(pos0, pos1);
if (!isPinching)
{
isPinching = true;
lastPinchDistance = currentDistance;
lastPanPosition = (pos0 + pos1) / 2f;
}
else
{
// Pinch zoom
float deltaDistance = lastPinchDistance - currentDistance;
Zoom(deltaDistance * pinchZoomSpeed);
lastPinchDistance = currentDistance;
// Two-finger pan
Vector2 currentCenter = (pos0 + pos1) / 2f;
Vector2 delta = currentCenter - lastPanPosition;
Pan(-delta);
lastPanPosition = currentCenter;
}
isPanning = false; // Don't single-finger pan while pinching
}
else if (touchCount == 1)
{
isPinching = false;
var primaryTouch = touch.primaryTouch;
Vector2 touchPos = primaryTouch.position.ReadValue();
// Check if touch started on a unit
if (primaryTouch.press.wasPressedThisFrame)
{
Vector2 worldPos = cam.ScreenToWorldPoint(touchPos);
var hit = Physics2D.OverlapPoint(worldPos);
startedOnUnit = hit != null && hit.GetComponent<Unit>() != null;
if (!startedOnUnit)
{
lastPanPosition = touchPos;
isPanning = true;
}
}
else if (primaryTouch.press.wasReleasedThisFrame)
{
isPanning = false;
startedOnUnit = false;
}
// Single finger pan (only if not drawing route)
if (isPanning && !startedOnUnit && primaryTouch.press.isPressed)
{
Vector2 delta = touchPos - lastPanPosition;
Pan(-delta);
lastPanPosition = touchPos;
}
}
else
{
isPinching = false;
isPanning = false;
}
}
void Zoom(float delta)
{
float newSize = cam.orthographicSize + delta;
cam.orthographicSize = Mathf.Clamp(newSize, minZoom, maxZoom);
}
void Pan(Vector2 screenDelta)
{
// Convert screen delta to world delta
float worldUnitsPerPixel = cam.orthographicSize * 2f / Screen.height;
Vector3 worldDelta = new Vector3(
screenDelta.x * worldUnitsPerPixel * panSpeed,
screenDelta.y * worldUnitsPerPixel * panSpeed,
0
);
cam.transform.position += worldDelta;
}
void ClampCameraPosition()
{
// Keep camera within map bounds (with some padding for zoom)
float halfHeight = cam.orthographicSize;
float halfWidth = halfHeight * cam.aspect;
float minX = -Game.MapWidth / 2f + halfWidth;
float maxX = Game.MapWidth / 2f - halfWidth;
float minY = -Game.MapHeight / 2f + halfHeight;
float maxY = Game.MapHeight / 2f - halfHeight;
Vector3 pos = cam.transform.position;
pos.x = Mathf.Clamp(pos.x, minX, maxX);
pos.y = Mathf.Clamp(pos.y, minY, maxY);
cam.transform.position = pos;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a481d18ae14b94a098fb1659b13c68a5

View File

@@ -0,0 +1,97 @@
using System.Collections;
using UnityEngine;
public class Flag : MonoBehaviour
{
public Unit.Team team;
public Transform homePosition;
public Unit carriedBy;
float dropTimer;
bool isDropped;
Coroutine returnCoroutine;
void Update()
{
if (carriedBy != null)
{
// Follow carrier
transform.position = carriedBy.transform.position + new Vector3(0.3f, 0.3f, 0);
}
}
public void Pickup(Unit unit)
{
if (carriedBy != null) return;
// Cancel return timer if picking up dropped flag
if (returnCoroutine != null)
{
StopCoroutine(returnCoroutine);
returnCoroutine = null;
}
carriedBy = unit;
unit.hasFlag = true;
isDropped = false;
Debug.Log($"{unit.team} picked up {team} flag!");
}
public void Drop()
{
if (carriedBy == null) return;
carriedBy.hasFlag = false;
carriedBy = null;
isDropped = true;
Debug.Log($"{team} flag dropped!");
// Start return timer
returnCoroutine = StartCoroutine(ReturnAfterDelay());
}
IEnumerator ReturnAfterDelay()
{
yield return new WaitForSeconds(Game.FlagReturnDelay);
if (isDropped && carriedBy == null)
{
ReturnHome();
}
}
public void ReturnHome()
{
if (returnCoroutine != null)
{
StopCoroutine(returnCoroutine);
returnCoroutine = null;
}
if (carriedBy != null)
{
carriedBy.hasFlag = false;
carriedBy = null;
}
transform.position = homePosition.position;
isDropped = false;
Debug.Log($"{team} flag returned home!");
}
void OnTriggerEnter2D(Collider2D other)
{
// If flag is dropped and a friendly unit touches it, return it home immediately
if (isDropped && carriedBy == null)
{
var unit = other.GetComponent<Unit>();
if (unit != null && unit.team == team && !unit.isTaggedOut)
{
ReturnHome();
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 95e5070ee05d6462bbee9fa1ca4917d9

View File

@@ -0,0 +1,538 @@
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;
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f62c6569cd5544a03b7d7d1cee7da3f6

View File

@@ -0,0 +1,147 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class RouteDrawer : MonoBehaviour
{
Unit selectedUnit;
List<Vector2> currentRoute = new();
LineRenderer lineRenderer;
bool isDrawing;
const float MinPointDistance = 0.5f;
void Start()
{
CreateLineRenderer();
}
void CreateLineRenderer()
{
var lineGO = new GameObject("RouteLine");
lineRenderer = lineGO.AddComponent<LineRenderer>();
lineRenderer.startWidth = 0.15f;
lineRenderer.endWidth = 0.15f;
lineRenderer.material = new Material(Shader.Find("Sprites/Default"));
lineRenderer.startColor = new Color(1f, 1f, 1f, 0.5f);
lineRenderer.endColor = new Color(1f, 1f, 1f, 0.5f);
lineRenderer.sortingOrder = 20;
lineRenderer.positionCount = 0;
}
void Update()
{
var mouse = Mouse.current;
var touch = Touchscreen.current;
// Handle mouse input
if (mouse != null)
{
HandlePointerInput(
mouse.leftButton.wasPressedThisFrame,
mouse.leftButton.wasReleasedThisFrame,
mouse.leftButton.isPressed,
mouse.position.ReadValue()
);
}
// Handle touch input
else if (touch != null && touch.primaryTouch.press.isPressed)
{
var primaryTouch = touch.primaryTouch;
HandlePointerInput(
primaryTouch.press.wasPressedThisFrame,
primaryTouch.press.wasReleasedThisFrame,
primaryTouch.press.isPressed,
primaryTouch.position.ReadValue()
);
}
}
void HandlePointerInput(bool pressed, bool released, bool held, Vector2 screenPos)
{
Vector2 worldPos = Camera.main.ScreenToWorldPoint(screenPos);
if (pressed)
{
OnPointerDown(worldPos);
}
else if (released)
{
OnPointerUp();
}
else if (held && isDrawing)
{
OnPointerDrag(worldPos);
}
}
void OnPointerDown(Vector2 worldPos)
{
// Check if we clicked on a player unit
var hit = Physics2D.OverlapPoint(worldPos);
if (hit != null)
{
var unit = hit.GetComponent<Unit>();
if (unit != null && unit.team == Unit.Team.Player && !unit.isTaggedOut)
{
selectedUnit = unit;
isDrawing = true;
currentRoute.Clear();
currentRoute.Add(worldPos);
UpdateLineRenderer();
}
}
}
void OnPointerDrag(Vector2 worldPos)
{
if (!isDrawing || selectedUnit == null) return;
// Only add point if far enough from last point
if (currentRoute.Count > 0)
{
float dist = Vector2.Distance(currentRoute[currentRoute.Count - 1], worldPos);
if (dist >= MinPointDistance)
{
currentRoute.Add(worldPos);
UpdateLineRenderer();
}
}
}
void OnPointerUp()
{
if (isDrawing && selectedUnit != null && currentRoute.Count > 0)
{
// Remove the first point (unit's current position) and apply route
if (currentRoute.Count > 1)
{
currentRoute.RemoveAt(0);
}
selectedUnit.SetRoute(currentRoute);
}
// Clear drawing state
isDrawing = false;
selectedUnit = null;
currentRoute.Clear();
ClearLineRenderer();
}
void UpdateLineRenderer()
{
if (lineRenderer == null) return;
lineRenderer.positionCount = currentRoute.Count;
for (int i = 0; i < currentRoute.Count; i++)
{
lineRenderer.SetPosition(i, new Vector3(currentRoute[i].x, currentRoute[i].y, 0));
}
}
void ClearLineRenderer()
{
if (lineRenderer == null) return;
lineRenderer.positionCount = 0;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8c4467d56dec84c76a24b128afc73a3f

View File

@@ -0,0 +1,70 @@
using System.Collections.Generic;
using UnityEngine;
public class SimpleAI : MonoBehaviour
{
public Unit[] aiUnits;
public Flag playerFlag;
public Transform aiBase;
float decisionTimer;
const float DecisionInterval = 0.5f;
const float RandomOffset = 1.5f;
void Update()
{
decisionTimer -= Time.deltaTime;
if (decisionTimer <= 0)
{
MakeDecisions();
decisionTimer = DecisionInterval;
}
}
void MakeDecisions()
{
if (playerFlag == null || aiBase == null) return;
for (int i = 0; i < aiUnits.Length; i++)
{
var unit = aiUnits[i];
if (unit == null || unit.isTaggedOut) continue;
Vector2 target;
if (unit.hasFlag)
{
// Has flag - return to base
target = aiBase.position;
}
else if (playerFlag.carriedBy != null && playerFlag.carriedBy.team == Unit.Team.Enemy)
{
// Friendly unit has flag - escort or find something else to do
// For now, just patrol near own base
target = (Vector2)aiBase.position + Random.insideUnitCircle * 5f;
}
else if (playerFlag.carriedBy != null)
{
// Player has our flag - chase the carrier
target = playerFlag.carriedBy.transform.position;
}
else
{
// Flag is at home or dropped - go for it
target = playerFlag.transform.position;
}
// Add slight randomness to prevent all units clumping
Vector2 randomOffset = Random.insideUnitCircle * RandomOffset;
// Offset based on unit index for some spread
float angle = (i / (float)aiUnits.Length) * Mathf.PI * 2f;
Vector2 spreadOffset = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * 1f;
Vector2 finalTarget = target + randomOffset * 0.5f + spreadOffset;
// Simple route: straight line to target
unit.SetRoute(new List<Vector2> { finalTarget });
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: faa2f6c6b185d4351829086d17055949

View File

@@ -0,0 +1,232 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Unit : MonoBehaviour
{
public enum Team { Player, Enemy }
public Team team;
public bool isTaggedOut;
public bool hasFlag;
List<Vector2> route = new();
int routeIndex;
bool isMoving;
SpriteRenderer spriteRenderer;
CircleCollider2D circleCollider;
void Awake()
{
spriteRenderer = GetComponent<SpriteRenderer>();
circleCollider = GetComponent<CircleCollider2D>();
}
void Update()
{
if (isTaggedOut || !isMoving || route.Count == 0) return;
FollowRoute();
}
void FollowRoute()
{
if (routeIndex >= route.Count)
{
isMoving = false;
return;
}
Vector2 target = route[routeIndex];
Vector2 current = transform.position;
Vector2 direction = (target - current).normalized;
float distance = Vector2.Distance(current, target);
// Check for obstacles ahead
if (IsObstacleAhead(direction))
{
isMoving = false;
return;
}
float moveDistance = Game.UnitSpeed * Time.deltaTime;
if (moveDistance >= distance)
{
// Reached waypoint
transform.position = new Vector3(target.x, target.y, 0);
routeIndex++;
if (routeIndex >= route.Count)
{
isMoving = false;
}
}
else
{
// Move toward waypoint
Vector2 newPos = current + direction * moveDistance;
transform.position = new Vector3(newPos.x, newPos.y, 0);
}
}
bool IsObstacleAhead(Vector2 direction)
{
// Raycast to check for obstacles (non-trigger colliders)
var hits = Physics2D.RaycastAll(transform.position, direction, 0.6f);
foreach (var hit in hits)
{
if (hit.collider != null && hit.collider.gameObject != gameObject && !hit.collider.isTrigger)
{
return true;
}
}
return false;
}
public void SetRoute(List<Vector2> waypoints)
{
if (isTaggedOut) return;
route = new List<Vector2>(waypoints);
routeIndex = 0;
isMoving = route.Count > 0;
}
public void ClearRoute()
{
route.Clear();
routeIndex = 0;
isMoving = false;
}
void OnTriggerEnter2D(Collider2D other)
{
if (isTaggedOut) return;
// Check for enemy unit collision (tagging)
var otherUnit = other.GetComponent<Unit>();
if (otherUnit != null && otherUnit.team != team && !otherUnit.isTaggedOut)
{
HandleTagCollision(otherUnit);
return;
}
// Check for flag pickup
var flag = other.GetComponent<Flag>();
if (flag != null && flag.team != team && flag.carriedBy == null)
{
flag.Pickup(this);
return;
}
// Check for scoring (reaching own base with enemy flag)
var baseZone = other.GetComponent<BaseZone>();
if (baseZone != null && baseZone.team == team && hasFlag)
{
// Get the flag we're carrying
var carriedFlag = team == Team.Player ? Game.Instance.enemyFlag : Game.Instance.playerFlag;
if (carriedFlag != null && carriedFlag.carriedBy == this)
{
carriedFlag.Drop();
Game.Instance.Score(team);
}
}
}
void HandleTagCollision(Unit other)
{
// Only process collision once - lower instance ID handles it
if (GetInstanceID() > other.GetInstanceID()) return;
// Determine who gets tagged: farther from their own base loses
// The instigator (closer to their base) is NOT captured
Transform myBase = Game.Instance.GetBase(team);
Transform theirBase = Game.Instance.GetBase(other.team);
float myDistance = Vector2.Distance(transform.position, myBase.position);
float theirDistance = Vector2.Distance(other.transform.position, theirBase.position);
// Only the one farther from their base gets tagged
if (myDistance > theirDistance)
{
TagOut();
}
else
{
// They are farther or equal - they get tagged
other.TagOut();
}
}
public void TagOut()
{
if (isTaggedOut) return;
isTaggedOut = true;
isMoving = false;
route.Clear();
// Drop flag if carrying
if (hasFlag)
{
var flag = team == Team.Player ? Game.Instance.enemyFlag : Game.Instance.playerFlag;
if (flag != null && flag.carriedBy == this)
{
flag.Drop();
}
}
// Visual feedback - fade out
if (spriteRenderer != null)
{
var color = spriteRenderer.color;
color.a = 0.3f;
spriteRenderer.color = color;
}
StartCoroutine(RespawnCoroutine());
}
IEnumerator RespawnCoroutine()
{
yield return new WaitForSeconds(Game.RespawnDelay);
// Respawn at base (landscape - horizontal offset toward center)
Transform baseTransform = Game.Instance.GetBase(team);
float xOffset = team == Team.Player ? 2f : -2f;
Vector3 offset = new Vector3(xOffset, Random.Range(-2f, 2f), 0);
transform.position = baseTransform.position + offset;
isTaggedOut = false;
// Restore visual
if (spriteRenderer != null)
{
var color = spriteRenderer.color;
color.a = 1f;
spriteRenderer.color = color;
}
}
public void ForceRespawn(Vector3 position)
{
StopAllCoroutines();
transform.position = position;
isTaggedOut = false;
isMoving = false;
route.Clear();
hasFlag = false;
if (spriteRenderer != null)
{
var color = spriteRenderer.color;
color.a = 1f;
spriteRenderer.color = color;
}
}
public bool IsMoving => isMoving;
public List<Vector2> CurrentRoute => route;
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6ea1fe45ed12847fdbf61f14ae451c47

View File

@@ -0,0 +1,59 @@
using UnityEngine;
public class Visibility : MonoBehaviour
{
public Unit[] playerUnits;
public Unit[] enemyUnits;
public Flag enemyFlag;
void Update()
{
// Update visibility of enemy units
foreach (var enemy in enemyUnits)
{
if (enemy == null) continue;
bool visible = IsVisibleToAnyPlayerUnit(enemy.transform.position);
var sr = enemy.GetComponent<SpriteRenderer>();
if (sr != null)
{
sr.enabled = visible || enemy.isTaggedOut; // Show tagged out units (they're faded anyway)
}
}
// Update visibility of enemy flag (only when not carried)
if (enemyFlag != null && enemyFlag.carriedBy == null)
{
bool flagVisible = IsVisibleToAnyPlayerUnit(enemyFlag.transform.position);
var flagSr = enemyFlag.GetComponent<SpriteRenderer>();
if (flagSr != null)
{
flagSr.enabled = flagVisible;
}
}
else if (enemyFlag != null && enemyFlag.carriedBy != null)
{
// Flag is carried - always show it (it's attached to a visible unit)
var flagSr = enemyFlag.GetComponent<SpriteRenderer>();
if (flagSr != null)
{
flagSr.enabled = true;
}
}
}
bool IsVisibleToAnyPlayerUnit(Vector2 position)
{
foreach (var unit in playerUnits)
{
if (unit == null || unit.isTaggedOut) continue;
float distance = Vector2.Distance(unit.transform.position, position);
if (distance < Game.VisionRadius)
{
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 02e237c0633a948708044095c3dc90c5