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:
@@ -254,18 +254,18 @@ Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Unity 6 LTS project opens without errors
|
||||
- [ ] URP 2D Renderer is active (check Graphics settings)
|
||||
- [ ] New Input System is the active input handling mode
|
||||
- [ ] Folder structure matches spec (`Features/`, `Shared/`, `Settings/`, `Scenes/`)
|
||||
- [ ] `GameInputActions` asset exists with placeholder actions
|
||||
- [ ] Android build target configured (IL2CPP, ARM64, API 24+)
|
||||
- [ ] iOS build target configured (IL2CPP, ARM64, iOS 13+)
|
||||
- [ ] Desktop build targets configured
|
||||
- [ ] Main.unity scene has Global Light 2D with low intensity
|
||||
- [ ] `.gitignore` excludes Library/, Temp/, builds
|
||||
- [ ] Project uses text-based asset serialization
|
||||
- [ ] Initial git commit created
|
||||
- [x] Unity 6 LTS project opens without errors
|
||||
- [x] URP 2D Renderer is active (check Graphics settings)
|
||||
- [x] New Input System is the active input handling mode
|
||||
- [x] Folder structure matches spec (`Features/`, `Shared/`, `Settings/`, `Scenes/`)
|
||||
- [x] `GameInputActions` asset exists with placeholder actions
|
||||
- [~] Android build target configured (IL2CPP, ARM64, API 24+) - partially configured, UI differs from plan
|
||||
- [~] iOS build target configured (IL2CPP, ARM64, iOS 13+) - partially configured, UI differs from plan
|
||||
- [x] Desktop build targets configured
|
||||
- [ ] Main.unity scene has Global Light 2D with low intensity - TODO: add in Unity Editor
|
||||
- [x] `.gitignore` excludes Library/, Temp/, builds
|
||||
- [x] Project uses text-based asset serialization
|
||||
- [x] Initial git commit created
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
---
|
||||
title: "feat: Teaser Prototype Playable Core"
|
||||
type: feat
|
||||
date: 2026-02-01
|
||||
revised: 2026-02-01
|
||||
---
|
||||
|
||||
# Teaser Prototype: Playable Core (Simplified)
|
||||
|
||||
## Overview
|
||||
|
||||
Build a minimal playable 1v1 capture-the-flag teaser that captures the "practiced chaos" of Neighborhood Quarterback - where chaos has patterns you can learn to exploit, like Rocket League.
|
||||
|
||||
**Target experience:** Fast rounds with flag grabs, chases, tag-outs, and scrambles. Players should feel "I almost had it" and "I can learn this."
|
||||
|
||||
## Guiding Principle
|
||||
|
||||
**Build the skateboard, not the car chassis.** Get something playable in days, not weeks. Polish comes after validating the core loop is fun.
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
6 scripts, 3 phases, ~15 tasks:
|
||||
|
||||
```
|
||||
Assets/Scripts/
|
||||
├── Game.cs # Score, reset, win, spawn
|
||||
├── Unit.cs # Movement, state, respawn, flag carrying
|
||||
├── RouteDrawer.cs # Click-drag to draw routes
|
||||
├── Flag.cs # Pickup, drop, return
|
||||
├── Visibility.cs # Simple sprite show/hide (no shaders)
|
||||
└── SimpleAI.cs # Chase flag or flag carrier
|
||||
```
|
||||
|
||||
No feature folders for MVP. No managers. Refactor when needed.
|
||||
|
||||
## Technical Decisions
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| Pathfinding | None | Draw route, unit follows, stops at obstacles |
|
||||
| Fog of War | Sprite SetActive | Hide enemies outside vision radius. No shaders. |
|
||||
| State | Bools/enums on scripts | No state machine frameworks |
|
||||
| AI | One behavior | Chase player flag (or flag carrier) |
|
||||
| Events | Direct method calls | No event bus for 6 scripts |
|
||||
|
||||
## Constants (Hardcoded, Tune Later)
|
||||
|
||||
```csharp
|
||||
// In Game.cs - move to ScriptableObject if needed
|
||||
const float UnitSpeed = 5f;
|
||||
const float VisionRadius = 4f;
|
||||
const float TagRadius = 0.75f;
|
||||
const float RespawnDelay = 3f;
|
||||
const float FlagReturnDelay = 5f;
|
||||
const int WinScore = 3;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Movement & Map (3-4 days)
|
||||
|
||||
**Goal:** Draw routes, units follow them, obstacles block.
|
||||
|
||||
### Tasks
|
||||
|
||||
- [x] **1.1** Create placeholder art in Main.unity:
|
||||
- Green plane (ground, ~40x30 units)
|
||||
- Gray rectangles (4-6 houses as obstacles with BoxCollider2D)
|
||||
- Colored circles (units - blue team, red team)
|
||||
- Two base zones (opposite corners)
|
||||
|
||||
- [x] **1.2** Create `Unit.cs`:
|
||||
```csharp
|
||||
public class Unit : MonoBehaviour
|
||||
{
|
||||
public enum Team { Player, Enemy }
|
||||
public Team team;
|
||||
public bool isTaggedOut;
|
||||
public bool hasFlag;
|
||||
|
||||
List<Vector2> route;
|
||||
int routeIndex;
|
||||
|
||||
public void SetRoute(List<Vector2> waypoints) { ... }
|
||||
void Update() { /* follow route, stop at end */ }
|
||||
public void TagOut() { /* disable, start respawn coroutine */ }
|
||||
IEnumerator Respawn() { /* wait 3s, teleport to base, enable */ }
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **1.3** Create `RouteDrawer.cs`:
|
||||
- On mouse down over player unit: start route
|
||||
- While dragging: collect points, draw LineRenderer preview
|
||||
- On mouse up: call `unit.SetRoute(points)`
|
||||
- Clear line after route applied
|
||||
|
||||
- [x] **1.4** Create `Game.cs` (partial):
|
||||
```csharp
|
||||
public class Game : MonoBehaviour
|
||||
{
|
||||
public Unit[] playerUnits; // Assign in inspector
|
||||
public Unit[] enemyUnits;
|
||||
public Transform playerBase;
|
||||
public Transform enemyBase;
|
||||
|
||||
void Start() { SpawnUnits(); }
|
||||
void SpawnUnits() { /* position 5 units at each base */ }
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **1.5** Wire up scene:
|
||||
- Create 5 Unit prefabs per team
|
||||
- Add colliders to obstacles
|
||||
- Test: draw route, unit follows, stops at obstacle
|
||||
|
||||
**Verification:**
|
||||
- [ ] Can draw route on player unit, unit follows
|
||||
- [ ] Unit stops when hitting obstacle
|
||||
- [ ] Unit stops at end of route
|
||||
- [ ] New route replaces old route
|
||||
- [ ] Enemy units visible but not controllable
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Flag, Tagging, Scoring (2-3 days)
|
||||
|
||||
**Goal:** Grab flag, tag enemies, score points, win.
|
||||
|
||||
### Tasks
|
||||
|
||||
- [x] **2.1** Create `Flag.cs`:
|
||||
```csharp
|
||||
public class Flag : MonoBehaviour
|
||||
{
|
||||
public Unit.Team team;
|
||||
public Transform homePosition;
|
||||
public Unit carriedBy;
|
||||
float dropTimer;
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (carriedBy != null)
|
||||
transform.position = carriedBy.transform.position;
|
||||
else if (transform.position != homePosition.position)
|
||||
HandleDroppedState();
|
||||
}
|
||||
|
||||
public void Pickup(Unit unit) { carriedBy = unit; unit.hasFlag = true; }
|
||||
public void Drop() { carriedBy.hasFlag = false; carriedBy = null; dropTimer = 5f; }
|
||||
void ReturnHome() { transform.position = homePosition.position; }
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **2.2** Add flag pickup detection:
|
||||
- OnTriggerEnter2D: if enemy unit enters flag trigger, Pickup()
|
||||
- In `Game.cs`: when unit with flag enters own base, Score()
|
||||
|
||||
- [x] **2.3** Add tagging to `Unit.cs`:
|
||||
- OnTriggerEnter2D: if enemy unit overlaps
|
||||
- Determine loser: farther from own base gets tagged
|
||||
- (Or for more chaos: both get tagged)
|
||||
- If tagged unit has flag, call flag.Drop()
|
||||
|
||||
- [x] **2.4** Add scoring to `Game.cs`:
|
||||
```csharp
|
||||
int playerScore, enemyScore;
|
||||
|
||||
public void Score(Unit.Team team)
|
||||
{
|
||||
if (team == Unit.Team.Player) playerScore++;
|
||||
else enemyScore++;
|
||||
|
||||
Debug.Log($"Score: {playerScore} - {enemyScore}");
|
||||
|
||||
if (playerScore >= WinScore || enemyScore >= WinScore)
|
||||
EndGame();
|
||||
else
|
||||
ResetRound();
|
||||
}
|
||||
|
||||
void ResetRound()
|
||||
{
|
||||
// Return flags, respawn all units at bases
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **2.5** Add simple UI:
|
||||
- TextMeshPro showing score
|
||||
- "YOU WIN" / "YOU LOSE" text on game end
|
||||
- (No menu - press Play in editor)
|
||||
|
||||
**Verification:**
|
||||
- [ ] Walking over enemy flag picks it up
|
||||
- [ ] Flag follows carrier
|
||||
- [ ] Reaching base with flag scores point
|
||||
- [ ] Overlapping enemy triggers tag-out
|
||||
- [ ] Tagged unit respawns after 3 seconds
|
||||
- [ ] Dropped flag returns home after 5 seconds
|
||||
- [ ] First to 3 wins
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Visibility & AI (2-3 days)
|
||||
|
||||
**Goal:** Can't see enemies outside vision range. AI provides opposition.
|
||||
|
||||
### Tasks
|
||||
|
||||
- [x] **3.1** Create `Visibility.cs`:
|
||||
```csharp
|
||||
public class Visibility : MonoBehaviour
|
||||
{
|
||||
public Unit[] playerUnits;
|
||||
public Unit[] enemyUnits;
|
||||
public Flag enemyFlag;
|
||||
|
||||
void Update()
|
||||
{
|
||||
foreach (var enemy in enemyUnits)
|
||||
{
|
||||
bool visible = IsVisibleToAnyPlayerUnit(enemy.transform.position);
|
||||
enemy.GetComponent<SpriteRenderer>().enabled = visible;
|
||||
}
|
||||
|
||||
// Also hide enemy flag if not carried and not visible
|
||||
if (enemyFlag.carriedBy == null)
|
||||
enemyFlag.GetComponent<SpriteRenderer>().enabled =
|
||||
IsVisibleToAnyPlayerUnit(enemyFlag.transform.position);
|
||||
}
|
||||
|
||||
bool IsVisibleToAnyPlayerUnit(Vector2 pos)
|
||||
{
|
||||
foreach (var unit in playerUnits)
|
||||
{
|
||||
if (unit.isTaggedOut) continue;
|
||||
if (Vector2.Distance(unit.transform.position, pos) < VisionRadius)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **3.2** Create `SimpleAI.cs`:
|
||||
```csharp
|
||||
public class SimpleAI : MonoBehaviour
|
||||
{
|
||||
public Unit[] aiUnits;
|
||||
public Flag playerFlag;
|
||||
public Transform aiBase;
|
||||
float decisionTimer;
|
||||
|
||||
void Update()
|
||||
{
|
||||
decisionTimer -= Time.deltaTime;
|
||||
if (decisionTimer <= 0)
|
||||
{
|
||||
MakeDecisions();
|
||||
decisionTimer = 0.5f; // Decide every 0.5s
|
||||
}
|
||||
}
|
||||
|
||||
void MakeDecisions()
|
||||
{
|
||||
foreach (var unit in aiUnits)
|
||||
{
|
||||
if (unit.isTaggedOut) continue;
|
||||
|
||||
Vector2 target;
|
||||
if (unit.hasFlag)
|
||||
target = aiBase.position; // Return flag
|
||||
else if (playerFlag.carriedBy != null)
|
||||
target = playerFlag.carriedBy.transform.position; // Chase carrier
|
||||
else
|
||||
target = playerFlag.transform.position; // Go for flag
|
||||
|
||||
// Simple route: straight line to target
|
||||
unit.SetRoute(new List<Vector2> { target });
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **3.3** Add slight route randomness:
|
||||
- Offset target by small random amount
|
||||
- Prevents all AI units clumping perfectly
|
||||
|
||||
- [x] **3.4** Playtest & tune:
|
||||
- Adjust VisionRadius, TagRadius, speeds
|
||||
- Make AI beatable but not trivial
|
||||
|
||||
**Verification:**
|
||||
- [ ] Enemy units hidden when far from player units
|
||||
- [ ] Enemies appear when player unit gets close
|
||||
- [ ] AI units move toward player flag
|
||||
- [ ] AI chases player flag carrier
|
||||
- [ ] AI returns flag to base when carrying
|
||||
- [ ] Can beat AI after 2-3 attempts
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria (MVP)
|
||||
|
||||
- [ ] Game starts with 5 units per side at bases
|
||||
- [ ] Draw route on unit, unit follows path
|
||||
- [ ] Unit stops at obstacles
|
||||
- [ ] Grabbing enemy flag awards 1 point
|
||||
- [ ] Returning flag to base (with own flag present) awards 1 more point
|
||||
- [ ] Overlapping enemies triggers tag-out (farther from base loses)
|
||||
- [ ] Tagged units respawn at base after 3 seconds
|
||||
- [ ] Enemies only visible within vision radius of player units
|
||||
- [ ] AI controls enemy team, chases flag
|
||||
- [ ] First to 3 points wins
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope (v2)
|
||||
|
||||
- Shader-based fog of war (smooth edges, feathering)
|
||||
- Unit classes (Sneak/Patrol/Speed)
|
||||
- Jail escort mechanics
|
||||
- Motion lights
|
||||
- Pre-phase setup
|
||||
- Multiplayer
|
||||
- AI with strategic roles
|
||||
- Route obstacle preview
|
||||
- Audio
|
||||
- Main menu
|
||||
- Mobile optimization
|
||||
|
||||
---
|
||||
|
||||
## What We're Testing
|
||||
|
||||
This prototype answers one question: **Is commanding units in CTF fun?**
|
||||
|
||||
If yes → Add fog polish, AI strategy, classes
|
||||
If no → Revisit core mechanics before adding complexity
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- Soul Doc: `soul.md`
|
||||
- Brainstorm: `docs/brainstorms/2026-02-01-teaser-prototype-brainstorm.md`
|
||||
- Input actions: `Assets/Settings/Input/GameInputActions.inputactions`
|
||||
Reference in New Issue
Block a user