diff --git a/Backyard CTF/Assets/Scripts/CameraController.cs b/Backyard CTF/Assets/Scripts/CameraController.cs new file mode 100644 index 0000000..dd05d3b --- /dev/null +++ b/Backyard CTF/Assets/Scripts/CameraController.cs @@ -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() != 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; + } +} diff --git a/Backyard CTF/Assets/Scripts/Flag.cs.meta b/Backyard CTF/Assets/Scripts/Flag.cs.meta new file mode 100644 index 0000000..2400d90 --- /dev/null +++ b/Backyard CTF/Assets/Scripts/Flag.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 95e5070ee05d6462bbee9fa1ca4917d9 \ No newline at end of file diff --git a/Backyard CTF/Assets/Scripts/Game.cs b/Backyard CTF/Assets/Scripts/Game.cs index c4fcff0..540448f 100644 --- a/Backyard CTF/Assets/Scripts/Game.cs +++ b/Backyard CTF/Assets/Scripts/Game.cs @@ -24,9 +24,10 @@ public class Game : MonoBehaviour 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 = 4f; + public const float VisionRadius = 6f; public const float TagRadius = 0.75f; public const float FlagPickupRadius = 0.75f; @@ -37,17 +38,24 @@ public class Game : MonoBehaviour // Scoring public const int WinScore = 3; - // Map - 19:9 aspect ratio for smartphone (landscape) - public const float MapWidth = 38f; - public const float MapHeight = 18f; + // 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 = 5f; - public const float BaseInset = 4f; // Distance from map edge + public const float BaseSize = 6f; + public const float BaseInset = 6f; - // Houses - public const float HouseMinSize = 2f; - public const float HouseMaxSize = 4f; + // 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; } @@ -73,6 +81,9 @@ public class Game : MonoBehaviour void Start() { + // Seed random for consistent neighborhood layout + Random.InitState(42); + SetupCamera(); CreateGround(); CreateObstacles(); @@ -89,101 +100,163 @@ public class Game : MonoBehaviour if (cam != null) { cam.orthographic = true; - cam.orthographicSize = MapHeight / 2f + 2f; + 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() { - var ground = CreateSprite("Ground", new Color(0.2f, 0.4f, 0.2f), MapWidth, MapHeight); + // 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() { - // Dense house layout for landscape 38x18 map - // Bases on left/right, houses create horizontal lanes with cross-routes + // 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 - Vector2[] housePositions = { - // Left section (near player base) - create 3 exit lanes - new(-14f, 5f), - new(-12f, -4f), - new(-10f, 0f), + var obstacles = new List<(Vector2 pos, Vector2 size, Color color)>(); - // Left-mid section - force route decisions - new(-6f, 6f), - new(-4f, 2f), - new(-7f, -3f), - new(-3f, -5f), + // Street grid creates the main routes + // Houses line the streets with backyards creating sneaky routes - // Center section - dense, creates cat-and-mouse area - new(2f, 5f), - new(0f, 1f), - new(-2f, -2f), - new(3f, -4f), - new(-1f, -6f), - new(1f, 6f), + // === ROW 1 (Top) - y around 12-16 === + CreateHouseRow(obstacles, y: 14f, xStart: -32f, xEnd: 32f, houseWidth: 5f, gapWidth: 3f, yardSide: -1); - // Right-mid section - mirror complexity - new(6f, 5f), - new(4f, -2f), - new(7f, -5f), - new(5f, 2f), + // === ROW 2 (Upper-mid) - y around 4-8 === + CreateHouseRow(obstacles, y: 6f, xStart: -28f, xEnd: 28f, houseWidth: 6f, gapWidth: 3.5f, yardSide: 1); - // Right section (near enemy base) - create 3 approach lanes - new(14f, 4f), - new(12f, -3f), - new(10f, 0f), - }; + // === 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); - Vector2[] houseSizes = { - // Left section - new(2.5f, 3f), - new(3f, 2.5f), - new(2f, 2f), + // === ROW 4 (Lower-mid) - y around -8 to -4 === + CreateHouseRow(obstacles, y: -8f, xStart: -28f, xEnd: 28f, houseWidth: 6f, gapWidth: 3.5f, yardSide: 1); - // Left-mid - new(3f, 2.5f), - new(2f, 3f), - new(2.5f, 2f), - new(2.5f, 2.5f), + // === ROW 5 (Bottom) - y around -12 to -16 === + CreateHouseRow(obstacles, y: -14f, xStart: -32f, xEnd: 32f, houseWidth: 5f, gapWidth: 3f, yardSide: 1); - // Center - varied sizes for interesting gaps - new(2f, 3f), - new(3f, 2f), - new(2f, 2.5f), - new(2.5f, 2f), - new(2f, 2f), - new(2.5f, 2.5f), + // === Add some fences/hedges in backyards for extra cover === + // These create the sneaky backyard routes + AddBackyardObstacles(obstacles); - // Right-mid - new(2.5f, 2.5f), - new(3f, 2f), - new(2.5f, 3f), - new(2f, 2f), - - // Right section - new(3f, 2.5f), - new(2f, 3f), - new(2.5f, 2f), - }; - - for (int i = 0; i < housePositions.Length; i++) + // Create all obstacles + int houseIndex = 0; + foreach (var (pos, size, color) in obstacles) { - var house = CreateSprite($"House_{i}", new Color(0.35f, 0.35f, 0.4f), houseSizes[i].x, houseSizes[i].y); - house.transform.position = new Vector3(housePositions[i].x, housePositions[i].y, 0); + 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; // Local space - scale handles actual size + 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 diff --git a/Backyard CTF/Assets/Scripts/Game.cs.meta b/Backyard CTF/Assets/Scripts/Game.cs.meta new file mode 100644 index 0000000..2e22b8b --- /dev/null +++ b/Backyard CTF/Assets/Scripts/Game.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f62c6569cd5544a03b7d7d1cee7da3f6 \ No newline at end of file diff --git a/Backyard CTF/Assets/Scripts/RouteDrawer.cs.meta b/Backyard CTF/Assets/Scripts/RouteDrawer.cs.meta new file mode 100644 index 0000000..34215f7 --- /dev/null +++ b/Backyard CTF/Assets/Scripts/RouteDrawer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8c4467d56dec84c76a24b128afc73a3f \ No newline at end of file diff --git a/Backyard CTF/Assets/Scripts/SimpleAI.cs.meta b/Backyard CTF/Assets/Scripts/SimpleAI.cs.meta new file mode 100644 index 0000000..806d13a --- /dev/null +++ b/Backyard CTF/Assets/Scripts/SimpleAI.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: faa2f6c6b185d4351829086d17055949 \ No newline at end of file diff --git a/Backyard CTF/Assets/Scripts/Unit.cs b/Backyard CTF/Assets/Scripts/Unit.cs index f42cc9d..5f22ab2 100644 --- a/Backyard CTF/Assets/Scripts/Unit.cs +++ b/Backyard CTF/Assets/Scripts/Unit.cs @@ -137,25 +137,25 @@ public class Unit : MonoBehaviour 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 if (theirDistance > myDistance) - { - other.TagOut(); - } else { - // Equal distance - both get tagged (more chaos!) - TagOut(); + // They are farther or equal - they get tagged other.TagOut(); } } @@ -193,9 +193,10 @@ public class Unit : MonoBehaviour { yield return new WaitForSeconds(Game.RespawnDelay); - // Respawn at base + // Respawn at base (landscape - horizontal offset toward center) Transform baseTransform = Game.Instance.GetBase(team); - Vector3 offset = new Vector3(Random.Range(-2f, 2f), team == Team.Player ? -2f : 2f, 0); + float xOffset = team == Team.Player ? 2f : -2f; + Vector3 offset = new Vector3(xOffset, Random.Range(-2f, 2f), 0); transform.position = baseTransform.position + offset; isTaggedOut = false; diff --git a/Backyard CTF/Assets/Scripts/Unit.cs.meta b/Backyard CTF/Assets/Scripts/Unit.cs.meta new file mode 100644 index 0000000..28c948e --- /dev/null +++ b/Backyard CTF/Assets/Scripts/Unit.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6ea1fe45ed12847fdbf61f14ae451c47 \ No newline at end of file diff --git a/Backyard CTF/Assets/Scripts/Visibility.cs.meta b/Backyard CTF/Assets/Scripts/Visibility.cs.meta new file mode 100644 index 0000000..3028324 --- /dev/null +++ b/Backyard CTF/Assets/Scripts/Visibility.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 02e237c0633a948708044095c3dc90c5 \ No newline at end of file