Second milestone of the ftl-shape vertical slice. Adds interactive crew: click a crew member, click a destination tile, watch them walk. Built on top of the skeleton milestone's tile grid and ship layout.
Purpose. Introduce interactive crew: 3 crew members sit on walkable tiles; player clicks a crew to select, clicks a destination tile to issue a move order; crew walks tile-by-tile along a shortest path. Foundation for combat-milestone features that require crew to be "at" a specific system.
In scope. Walkable-tile model derived from existing ship rooms; 3 named placeholder crew with distinct tints; click-to-select / click-to-move input; BFS pathfinding on the walkable tile graph; tile-by-tile movement animation; selection feedback (1-px ring around selected crew).
Out of scope. Doors / walls, crew skills, crew health, hover-tile preview, multi-select, queued orders, damage, enemy ship, combat, pause.
Chosen approach.
tiles.go: func walkableTiles(s Ship) map[[2]int]bool — union of every room's interior tiles, keyed by [x, y]. Computed once at ship construction, cached on Game. Naturally connected because rooms share tile edges in the skeleton layout.crew.go:
type Crew struct { ID int; Name string; Initial rune; Color color.RGBA; TileX, TileY int; Path [][2]int; MoveT float64 } — tile position is canonical; Path is the remaining waypoints (empty = idle); MoveT is progress in [0,1) toward the next waypoint.func NewStartingCrew() []Crew — Alice in Pilot, Bob in Shields, Carol in Engines.func bfsPath(walk map[[2]int]bool, from, to [2]int) [][2]int — standard 4-neighbour BFS over the walkable set; returns waypoints excluding from and including to. Empty path means unreachable or same-tile.Game.Update reads the mouse: left-click → convert window pixels to virtual pixels (using ebiten.WindowSize + the known Layout scale) → tile coords. If the click lands on a crew member, select that crew (store its index in selectedCrew). Otherwise, if a crew is selected and the click tile is walkable, replace that crew's Path with a fresh BFS result and reset MoveT.func updateCrew(c *Crew, dt float64) — if Path non-empty, advance MoveT += dt / tileTime (default tileTime = 0.35s). When MoveT >= 1, snap TileX, TileY to the next waypoint, pop it, reset MoveT. Rendered position is linearly interpolated between the current tile and the next waypoint.drawCrew(screen *ebiten.Image, c Crew, selected bool) added to render.go. Crew is a 12 px colored circle centred on the interpolated position, plus the initial letter via ebitenutil.DebugPrintAt. Selected crew gets a 1-px white outer ring.Game gains walk map[[2]int]bool, crew []Crew, selectedCrew int (−1 if none), lastTime time.Time. Update computes dt from wall-clock delta.Tradeoffs that settled the decisions.
map[[2]int]bool (vs [][]bool grid): tiny ship, map lookup is fast enough, keeps the code resolution-independent. Memory overhead is irrelevant at ~30 tiles.Unknowns. Mouse-pixel → virtual-pixel conversion relies on ebiten.WindowSize reporting the current window size (including after resize). Ebitengine's documented behaviour confirms this.
TDD: yes — bfsPath, walkableTiles, and the updateCrew tick are pure functions with clear inputs/outputs and meaningful branching (same-tile, unreachable, multi-room path, boundary-crossing path, tile snap on MoveT >= 1). Input handling and rendering stay manual — they need the real mouse and display.
ship.go, tiles.go, and crew.go must not import Ebitengine. Pathfinding and walkability are pure.(TileX, TileY) — a tile coordinate. Render-time interpolation is never written back to TileX / TileY.bfsPath is deterministic: same walkable set + same from / to → same path, via a stable neighbour ordering.GOOS=js GOARCH=wasm and native targets. Mouse input works in the browser via the same API.crew.go has no ebiten. imports; render.go never mutates crew state.