Files
Backyard-CTF/Backyard CTF/Assets/Scripts/Unit.cs
John Lamb 1911da1e33 feat(gameplay): large zoomable map, neighborhood layout, tagging fix
Major changes:
- Map: 80x40 (was 38x18) - requires zoom/pan to navigate
- CameraController: pinch-to-zoom, drag-to-pan, scroll wheel zoom
- Neighborhood layout: streets grid with house rows, backyards, fences
- All gaps minimum 2.5 units wide for unit passage
- Streets visible on ground (gray paths)

Bug fixes:
- Tagging: only captured unit goes to jail, never the instigator
- Collision handled once (by lower instance ID)
- Respawn offset fixed for landscape mode

New constants:
- CameraMinZoom/MaxZoom/StartZoom
- MinGapSize, StreetWidth
- VisionRadius increased to 6 (larger map)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-01 21:45:28 -06:00

233 lines
6.2 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)
{
// 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;
}