@@ 47,3 47,87 @@ Second milestone of the `ftl-shape` vertical slice. Adds interactive crew: click
- Fail fast: unreachable paths return empty, not "closest-so-far". Clicking outside the ship is a no-op, not a teleport.
- Separate data from rendering and input — `crew.go` has no `ebiten.` imports; `render.go` never mutates crew state.
- Deterministic core: same input sequence and timestep → identical game state.
+
+## Plan
+
+Approach: three phases, TDD-first on the two pure units (tiles/pathing, then crew tick), then a single integration phase that wires input + rendering into `main.go` and `render.go`. Greenfield feature, no backwards-compat risk.
+
+### Phase 1 — Walkability and pathfinding (TDD)
+
+- **1.1** `tiles_test.go` (create) — tests first, must fail before 1.2.
+ - `TestWalkableTiles_coversAllRooms` — `walkableTiles(NewPlayerShip())` returns the exact union of the 5 rooms (54 tiles total: 9+12+12+12+9); spot-check tiles inside each room are present and an off-ship tile is absent.
+ - `TestWalkableTiles_ignoresOutside` — tiles at `(0,0)` and `(20,20)` are absent.
+ - `TestBFSPath_sameTile` — `bfsPath(walk, {4,10}, {4,10})` → `nil`/empty.
+ - `TestBFSPath_adjacentTile` — `bfsPath(walk, {4,10}, {5,10})` → `[[2]int{{5,10}}]`.
+ - `TestBFSPath_unreachable` — from walkable tile to off-ship tile → empty.
+ - `TestBFSPath_throughShields` — `bfsPath(walk, Pilot-center, Engines-center)` is non-empty, contains only walkable tiles, ends on the Engines tile, and length equals Manhattan distance (7 for `(4,10)→(11,10)`).
+ - `TestBFSPath_deterministic` — two consecutive calls produce equal slices.
+
+- **1.2** `tiles.go` (create)
+ - `package main`; no Ebitengine imports.
+ - `func walkableTiles(s Ship) map[[2]int]bool` — iterates `s.Rooms`, inserts every `(x, y)` with `GridX ≤ x < GridX+GridW`, `GridY ≤ y < GridY+GridH`.
+ - `func bfsPath(walk map[[2]int]bool, from, to [2]int) [][2]int` — 4-neighbour BFS with fixed neighbour ordering `{{+1,0},{-1,0},{0,+1},{0,-1}}` (for determinism), visited-set, parent map; returns waypoints excluding `from`, including `to`. Empty on same-tile or unreachable.
+ - Invariant: pure, no rendering/input deps; `bfsPath` deterministic via fixed neighbour order.
+
+- Commit: `crew-movement: walkability + BFS pathfinding with tests`
+
+### Phase 2 — Crew type and movement tick (TDD)
+
+- **2.1** `crew_test.go` (create)
+ - `TestNewStartingCrew_count` — returns 3 crew.
+ - `TestNewStartingCrew_positions` — Alice at `(4,10)` (Pilot), Bob at `(7,10)` (Shields), Carol at `(11,10)` (Engines); initials `'A'`, `'B'`, `'C'`; distinct colours.
+ - `TestUpdateCrew_idle` — empty `Path`, any `dt` → all fields unchanged.
+ - `TestUpdateCrew_subTileProgress` — path `[[2]int{{5,10}}]`, `dt = 0.1` (< `TileTime`) → `TileX/TileY` unchanged, `MoveT ≈ 0.1/TileTime`, path unchanged.
+ - `TestUpdateCrew_tileSnap` — path `[[2]int{{5,10}}]`, `dt = TileTime` → `TileX==5`, `Path` empty, `MoveT == 0`.
+ - `TestUpdateCrew_multiStepOvershoot` — path `[{5,10},{6,10}]`, `dt = 1.5*TileTime` → `TileX==6`, remaining path `[{6,10}]` wait, check logic. Actually: start `(4,10)`, path `[{5,10},{6,10}]`; `dt=1.5*TileTime` means advance 1.5 tiles. After 1 tile: `(5,10)`, path `[{6,10}]`, `MoveT=0`. Remaining 0.5 tile into next: `MoveT≈0.5`, `TileX==5`, path `[{6,10}]`. Assert that final state.
+ - `TestUpdateCrew_canonicalIntegral` — after any tick, `TileX` and `TileY` are integers equal to the last crossed waypoint (never fractional); `MoveT` is render-time only.
+
+- **2.2** `crew.go` (create)
+ - `package main`; imports `image/color` only (no Ebitengine).
+ - `const TileTime = 0.35` — seconds per tile.
+ - `type Crew struct { ID int; Name string; Initial rune; Color color.RGBA; TileX, TileY int; Path [][2]int; MoveT float64 }`.
+ - `func NewStartingCrew() []Crew` — hard-coded 3 crew: Alice/Bob/Carol at tiles `(4,10)`, `(7,10)`, `(11,10)`, with distinct muted tints (teal, amber, magenta). Initials `'A'`, `'B'`, `'C'`.
+ - `func updateCrew(c *Crew, dt float64)` — if `len(c.Path) == 0` return. Advance `c.MoveT += dt / TileTime`. While `c.MoveT >= 1 && len(c.Path) > 0`: snap `(c.TileX, c.TileY) = c.Path[0]`, `c.Path = c.Path[1:]`, `c.MoveT -= 1`. If `len(c.Path) == 0`, force `c.MoveT = 0`.
+ - Invariants: `crew.go` does not import Ebitengine; `TileX/TileY` only mutated on waypoint crossing.
+
+- Commit: `crew-movement: crew struct, starting roster, tick`
+
+### Phase 3 — Input and rendering integration
+
+- **3.1** `render.go` (modify — append, no changes to existing code)
+ - Add `const CrewRadius = 6` (12 px diameter at `TilePx=16`).
+ - Add `func drawCrew(screen *ebiten.Image, c Crew, selected bool)`:
+ - Compute interpolated pixel centre: if `len(c.Path) > 0`, `(cx, cy) = lerp((TileX,TileY), Path[0], MoveT)` in tile units; else `(TileX, TileY)`. Multiply by `TilePx`, add `TilePx/2` for tile centre.
+ - `vector.DrawFilledCircle(screen, px, py, CrewRadius, c.Color, true)` (antialiased so placeholder doesn't look too harsh; everything else stays non-AA).
+ - If `selected`, draw a 1-px white ring: `vector.StrokeCircle(screen, px, py, CrewRadius+1, 1, white, true)`.
+ - Label: `ebitenutil.DebugPrintAt(screen, string(c.Initial), int(px)-2, int(py)-5)`.
+ - Invariant: reads `Crew`, does not mutate; uses only virtual coords through `TilePx`.
+
+- **3.2** `main.go` (modify)
+ - Add imports: `time`.
+ - Extend `Game`: `walk map[[2]int]bool`, `crew []Crew`, `selectedCrew int`, `lastTime time.Time`.
+ - `main()`: after `NewPlayerShip()`, compute `walk := walkableTiles(ship)`, `crew := NewStartingCrew()`; initialise `Game{ship, walk, crew, -1, time.Time{}}`.
+ - `Update()`:
+ - First call: `lastTime = time.Now()`, return nil.
+ - Else: `dt := time.Since(g.lastTime).Seconds(); g.lastTime = time.Now()`. Clamp `dt` to a sane ceiling (e.g. `0.1`) to avoid huge jumps on resume.
+ - For each crew: `updateCrew(&g.crew[i], dt)`.
+ - Poll left-click: `if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft)` (new import `inpututil`) — read `cx, cy := ebiten.CursorPosition()` (already in virtual coords). Derive tile `tx, ty := cx/TilePx, cy/TilePx`.
+ - Selection: iterate crew; if cursor is within `CrewRadius` of crew centre (tile-space check is sufficient: same tile or adjacent-and-close), set `g.selectedCrew = i` and return.
+ - Otherwise, if `g.selectedCrew >= 0` and `g.walk[[2]int{tx,ty}]`: issue move — `from := [2]int{crew.TileX, crew.TileY}; crew.Path = bfsPath(g.walk, from, [2]int{tx,ty}); crew.MoveT = 0`.
+ - `Draw()`: after `drawShip`, iterate crew and call `drawCrew(screen, c, i == g.selectedCrew)`.
+ - Invariants: virtual-coord drawing only; single codebase WASM-safe (`time.Since` and `inpututil` both work under `GOOS=js`).
+
+- Commit: `crew-movement: wire input, tick, and render`
+
+### Test strategy
+
+Per task `TDD: yes` — tests from Phase 1 and Phase 2 written first, must fail on the pre-implementation tree, then green after the paired source file. Phase 3 has no automated tests (input and rendering need real mouse/display); acceptance is manual via `go run .` and `make play-web`:
+- Click each crew in turn: ring appears, persists until another crew or empty click.
+- Click a destination tile on a selected crew: crew walks there tile-by-tile, ring follows them.
+- Click an unreachable tile (e.g. outside ship): no-op.
+- Clicking a new destination on an already-moving crew: path replaces, no crash.
+
+### Open questions / risks
+
+- `inpututil` is a standard Ebitengine subpackage that works under WASM; no conditional imports needed. If it doesn't, Phase 3 falls back to `ebiten.IsMouseButtonPressed` + manual edge detection.
+- Mouse coords in the browser: `ebiten.CursorPosition()` is documented to return logical-space coords via `Layout`; this is how WASM-served Ebitengine games handle input. If verify finds the browser returns raw canvas pixels, the fix is a single `(windowW, windowH) := ebiten.WindowSize()` scale step.
@@ 0,0 1,48 @@
+# summary-crew-movement-resume
+
+Handoff summary written 2026-04-22. Resumes the `crew-movement` milestone after the plan stage.
+
+## Goal
+
+Building `ftl-shape`, an FTL-like game in Go + Ebitengine (virtual resolution 640×360 at a 1280×720 window) that compiles to native desktop and browser WASM. Vertical slice scope: one ship, one combat encounter, pixel-art placeholders.
+
+## Current state
+
+- `skeleton` milestone **complete and shipped** through the full `up:` workflow (Design → Plan → Execute → Verify → Review). Merge-ready, no findings at confidence ≥80. Native and WASM both verified rendering the 5-room placeholder ship.
+- `crew-movement` milestone **in progress**: Design committed at `aa11b70`; Plan written but **uncommitted** in `docs/tasks/crew-movement.md` (git status: `M`). Waiting on user approval before `up:uexecute`.
+- 6 commits on `main`; no remote.
+- Project memory: `~/.claude/projects/-Users-blikh-data-home-ftl-shape/memory/project_ftl_shape.md`.
+
+## Infrastructure / Environment
+
+Purely local. Go 1.26.2 at `/opt/homebrew/Cellar/go/1.26.2/libexec`. No remote work, no running background processes, no tmux.
+
+## Active blocker
+
+None. Workflow is paused at "plan presented, awaiting approval". Resume with execute.
+
+## Key files
+
+- `docs/tasks/skeleton.md` — done; Design / Plan / Verify / Conclusion all filled.
+- `docs/tasks/crew-movement.md` — Design + Plan present; Plan uncommitted.
+- `main.go`, `ship.go`, `render.go` — current skeleton code.
+- `Makefile` — targets `run`, `build`, `build-wasm`, `serve`, `play-web`.
+- `web/index.html`, `.gitignore` — WASM shell and ignore rules.
+
+## What to do next
+
+1. `git -C /Users/blikh/data/home/ftl-shape diff docs/tasks/crew-movement.md` → review the Plan section.
+2. On approval: `git -C /Users/blikh/data/home/ftl-shape add docs/tasks/crew-movement.md && git commit -m "docs(crew-movement): plan"`.
+3. Invoke `up:uexecute`. The plan dispatches 3 phases: tiles + BFS (TDD) → crew struct + tick (TDD) → input & render integration.
+4. **Model**: let `up:implementer` and `up:reviewer` use their default (Sonnet 4.6 — what those agents are designed for). If dispatch errors with `"model ... may not exist or you may not have access to it"`, fall back to `model: "opus"`. That fallback worked every time this session after one transient Sonnet failure; Sonnet is still worth trying first.
+5. After execute: `up:uverify` → `up:ureview`.
+6. Remaining milestones designed but not yet started: `systems-power`, `combat`, `polish`.
+
+## Gotchas
+
+- Subagent model: try Sonnet default first; fall back to `model: "opus"` only if dispatch errors with model-access failure. See step 4.
+- Go ≥1.24 moved `wasm_exec.js` to `$(go env GOROOT)/lib/wasm/wasm_exec.js`; the Makefile uses the new path. Older toolchains will fail loudly (acceptable per Design's fail-fast principle).
+- `go build ./...` from the repo root leaves a bare `./ftl-shape` binary. `.gitignore` already covers it along with `bin/`, `web/main.wasm`, `web/wasm_exec.js`.
+- Crew-movement plan uses `inpututil.IsMouseButtonJustPressed` + `ebiten.CursorPosition()`. Ebitengine docs state `CursorPosition` returns logical (post-`Layout`) coords, so no manual window → virtual scaling should be needed. Flagged as a browser verify-time smoke in the plan's "Open questions / risks" section — if it misbehaves, scale via `ebiten.WindowSize()`.
+- Reviewer noted during skeleton review that Design prose mentions `text/v2` for labels while code uses `ebitenutil.DebugPrintAt`; sub-threshold, deferred as future polish. Crew labels will follow the same convention for consistency.
+- The `skeleton` Verify flow includes a live Chrome smoke via the `mcp__chrome-devtools__*` tools plus `python3 -m http.server 8765` in `web/`. Same pattern works for verifying crew-movement's browser behaviour.