From b4322f8a40b68082c7ecc92baa53c0876632c96d Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Wed, 22 Apr 2026 02:02:52 +0300 Subject: [PATCH] chore: initialize project and task design --- docs/tasks/skeleton.md | 117 +++++++++++++++++++++++++++++++++++++++++ go.mod | 3 ++ 2 files changed, 120 insertions(+) create mode 100644 docs/tasks/skeleton.md create mode 100644 go.mod diff --git a/docs/tasks/skeleton.md b/docs/tasks/skeleton.md new file mode 100644 index 0000000000000000000000000000000000000000..644778028ab52128247ed79487e1e4cf562996b5 --- /dev/null +++ b/docs/tasks/skeleton.md @@ -0,0 +1,117 @@ +# 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: `