# skeleton First milestone of the `ftl-shape` vertical slice. Establishes the Go + Ebitengine pipeline and a placeholder ship render that later milestones (crew-movement, systems-power, combat, polish) build on. ## Design **Purpose.** Prove the Go + Ebitengine pipeline builds and runs on both desktop (macOS/Linux/Windows) and in-browser WASM, rendering a static placeholder player ship with 5 rooms in a ship-like layout. No interactivity. Foundation for every subsequent milestone. **In scope.** Go module, Ebitengine main loop, virtual-resolution rendering, room data model, rectangle-based room rendering with labels, Makefile for native + WASM builds, minimal `web/index.html` shell, dev server via `wasmserve`, one-command "run in browser" target. **Out of scope.** Input, crew, animation, enemy ship, sound, combat, menus, save system, disk-loaded assets, WASM size optimization. **Chosen approach.** - **Rendering.** Virtual resolution 640×360; window opens at 1280×720 (2× scale). Ebitengine's `Layout` returns 640×360; all drawing in virtual pixels. Resizable window is fine — Ebitengine handles scaling. - **Project layout.** Single package at module root (`sourcecraft.dev/bigbes/ftl-shape`). Files by concern: - `main.go` — entrypoint, `Game` struct, `Update`/`Draw`/`Layout` methods. - `ship.go` — `Ship`, `Room` types, placeholder ship constructor. - `render.go` — drawing helpers (rect fill, border, label). - **Ship model.** `Ship{Rooms []Room}` and `Room{ID, Name, GridX, GridY, GridW, GridH, Role}` in tile units. Render layer converts tile→virtual pixels at 16 px/tile. - **Placeholder ship content.** 5 rooms — Pilot (front/left), Weapons (mid-upper), Shields (mid-center), MedBay (mid-lower), Engines (rear/right). Each rendered as a role-tinted filled rectangle + 1-px border + role label via Ebitengine's default text face from `text/v2`. - **Build tooling — `Makefile` targets.** - `run` — native, `go run .`. - `build` — native binary to `bin/ftl-shape`. - `build-wasm` — produce `web/main.wasm`; copy `wasm_exec.js` from the installed Go toolchain (`$(go env GOROOT)/lib/wasm/wasm_exec.js` on Go ≥1.24). - `play-web` — one-command "run in browser": start `wasmserve` (auto-rebuilds on source change), print `http://localhost:8080`, and auto-open the browser (`open` on macOS, `xdg-open` on Linux, print-only otherwise). - `serve` — same as `play-web` without the auto-open step. - **Browser shell.** `web/index.html` loads `wasm_exec.js` + `main.wasm` using the standard Go WASM snippet. `.gitignore` excludes `bin/` and `web/main.wasm`. **Tradeoffs that settled the decisions.** - Grid-based rooms over freeform pixel coords: aligns with FTL's movement model; simplifies future click-to-move milestone. - Single package over `cmd/ + internal/`: small surface area, minimum friction. Split when a file crosses ~300 lines or a real subsystem boundary emerges. - Virtual resolution over native drawing: crisp pixel art at any window size; resizable-safe. Cost is a single `Layout` decision — negligible. **Unknowns.** WASM binary size for Ebitengine builds is typically 10–20 MB. Acceptable for dev; optimization is out of scope. **TDD: no** — skeleton is a visual + build-pipeline proof. `Ship`/`Room` are trivial data structs with no logic to assert; success is "window opens, rectangles appear, WASM loads in browser." TDD applies at the `crew-movement` milestone where pathfinding has real behavior. ### Invariants - All drawing uses virtual-resolution coordinates (640×360). No code draws in window-pixel units. - Single codebase builds unchanged for `GOOS=js GOARCH=wasm` and native targets; no platform-conditional imports (`//go:build`, `GOOS`-suffixed files) in this milestone. - Room positions and sizes are stored in tile units on `Room`. Pixel conversion happens only at render time. - No external asset files in milestone 1 — all placeholder art is code-drawn (filled rects + default text face). - `ship.go` does not import Ebitengine. Data types know nothing about rendering. ### Principles - YAGNI: no sub-packages, no interfaces, no ECS until a file crosses ~300 lines or a clear boundary emerges. - Fail fast on init errors (WASM bootstrap, font load). Panic is acceptable; no silent fallbacks. - Deterministic rendering: same ship state → same pixels. No hidden randomness in this milestone. - Separate data from rendering: ship/room types never call the renderer and never hold Ebitengine state. ## Plan Approach: two phases — first gets a native window rendering the placeholder ship, second adds the WASM build and the one-command browser target. Each phase is a self-contained runnable checkpoint. Backwards-compat: greenfield project, nothing to break. ### Phase 1 — Native app rendering the placeholder ship - **1.1** `go.mod` (modify) - Add dep `github.com/hajimehoshi/ebiten/v2`; run `go mod tidy`. - **1.2** `ship.go` (create) - Package `main`. - `type RoomRole int` with constants `RolePilot, RoleWeapons, RoleShields, RoleMedBay, RoleEngines`. - `type Room struct { ID int; Name string; GridX, GridY, GridW, GridH int; Role RoomRole }` — tile-unit coords only. - `type Ship struct { Rooms []Room }`. - `func NewPlayerShip() Ship` — hard-coded 5-room layout: Pilot front-left, Weapons mid-upper, Shields mid-center, MedBay mid-lower, Engines rear-right. - Invariants: **no Ebitengine imports in this file**; positions and sizes in tile units only. - **1.3** `render.go` (create) - Package `main`. Constants `TilePx = 16`, `VirtualW = 640`, `VirtualH = 360`. - `func roleColor(role RoomRole) color.RGBA` — role → tint mapping (mapping lives here, not in `ship.go`). - `func drawShip(screen *ebiten.Image, s Ship)` — iterates rooms, delegates to `drawRoom`. - `func drawRoom(screen *ebiten.Image, r Room)` — fills role-tinted rect, draws 1-px border, writes role name via `ebitenutil.DebugPrintAt` positioned inside the room. - Invariant: all coordinate math converts tile → virtual pixels through `TilePx`; no window-pixel math. - **1.4** `main.go` (create) - Package `main`. `type Game struct { ship Ship }`. - `func (g *Game) Update() error` — returns nil. - `func (g *Game) Draw(screen *ebiten.Image)` — fills dark background, calls `drawShip(screen, g.ship)`. - `func (g *Game) Layout(int, int) (int, int)` — returns `VirtualW, VirtualH`. - `func main()` — `ebiten.SetWindowSize(1280, 720)`, `ebiten.SetWindowTitle("ftl-shape")`, `ebiten.RunGame(&Game{ship: NewPlayerShip()})`, panic on error. - Invariant: no platform-conditional imports; single build source for native + WASM. - Commit: `scaffold game loop and placeholder ship render` ### Phase 2 — WASM build and browser run target - **2.1** `Makefile` (create) - Targets: `run`, `build`, `build-wasm`, `serve`, `play-web`, `.PHONY` line. - `build`: `go build -o bin/ftl-shape .`. - `build-wasm`: `GOOS=js GOARCH=wasm go build -o web/main.wasm .` then `cp "$$(go env GOROOT)/lib/wasm/wasm_exec.js" web/wasm_exec.js` (Go ≥1.24 path). - `serve`: `go run github.com/hajimehoshi/wasmserve@latest .` — wasmserve rebuilds wasm on source change and serves on `:8080`. - `play-web`: detect OS (`uname`), background-delay `open`/`xdg-open http://localhost:8080`, then invoke the same wasmserve command. - **2.2** `web/index.html` (create) - ~20-line WASM loader: `