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>
232 lines
5.9 KiB
C#
232 lines
5.9 KiB
C#
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)
|
|
{
|
|
// Determine who gets tagged: farther from their own base loses
|
|
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);
|
|
|
|
if (myDistance > theirDistance)
|
|
{
|
|
TagOut();
|
|
}
|
|
else if (theirDistance > myDistance)
|
|
{
|
|
other.TagOut();
|
|
}
|
|
else
|
|
{
|
|
// Equal distance - both get tagged (more chaos!)
|
|
TagOut();
|
|
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
|
|
Transform baseTransform = Game.Instance.GetBase(team);
|
|
Vector3 offset = new Vector3(Random.Range(-2f, 2f), team == Team.Player ? -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;
|
|
}
|