feat(core): implement playable CTF prototype with 6 core scripts

Implements the minimal playable core for the teaser prototype:
- Game.cs: Bootstrap, scene setup, scoring, round reset, win condition
- Unit.cs: Movement, route following, tagging, respawn
- RouteDrawer.cs: Click-drag route input with LineRenderer preview
- Flag.cs: Pickup, drop, return mechanics
- Visibility.cs: Fog of war via SpriteRenderer visibility
- SimpleAI.cs: Enemy AI that chases player flag

All game objects created programmatically (no Editor setup required).
Uses RuntimeInitializeOnLoadMethod for automatic bootstrap.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
John Lamb
2026-02-01 21:03:11 -06:00
parent b1b3e4d0b3
commit 95b37a1606
8 changed files with 1405 additions and 0 deletions

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;
}
}