M go.mod => go.mod +11 -0
@@ 1,3 1,14 @@
module sourcecraft.dev/bigbes/ftl-shape
go 1.26.2
+
+require github.com/hajimehoshi/ebiten/v2 v2.9.9
+
+require (
+ github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1 // indirect
+ github.com/ebitengine/hideconsole v1.0.0 // indirect
+ github.com/ebitengine/purego v0.9.0 // indirect
+ github.com/jezek/xgb v1.1.1 // indirect
+ golang.org/x/sync v0.17.0 // indirect
+ golang.org/x/sys v0.36.0 // indirect
+)
A go.sum => go.sum +16 -0
@@ 0,0 1,16 @@
+github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1 h1:+kz5iTT3L7uU+VhlMfTb8hHcxLO3TlaELlX8wa4XjA0=
+github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1/go.mod h1:lKJoeixeJwnFmYsBny4vvCJGVFc3aYDalhuDsfZzWHI=
+github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE=
+github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A=
+github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
+github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
+github.com/hajimehoshi/ebiten/v2 v2.9.9 h1:JdDag6Ndj12iD4lxQGG8kbsrh7ssj4Sbzth6r929H/M=
+github.com/hajimehoshi/ebiten/v2 v2.9.9/go.mod h1:DAt4tnkYYpCvu3x9i1X/nK/vOruNXIlYq/tBXxnhrXM=
+github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
+github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
+golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA=
+golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA=
+golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
+golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
+golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
A main.go => main.go +36 -0
@@ 0,0 1,36 @@
+package main
+
+import (
+ "image/color"
+
+ "github.com/hajimehoshi/ebiten/v2"
+)
+
+// Game holds the top-level state for the Ebitengine run loop.
+type Game struct {
+ ship Ship
+}
+
+// Update advances simulation; nothing to do in this milestone.
+func (g *Game) Update() error {
+ return nil
+}
+
+// Draw renders the whole frame.
+func (g *Game) Draw(screen *ebiten.Image) {
+ screen.Fill(color.RGBA{10, 15, 25, 255})
+ drawShip(screen, g.ship)
+}
+
+// Layout fixes the virtual resolution; the window scales to fit.
+func (g *Game) Layout(_, _ int) (int, int) {
+ return VirtualW, VirtualH
+}
+
+func main() {
+ ebiten.SetWindowSize(1280, 720)
+ ebiten.SetWindowTitle("ftl-shape")
+ if err := ebiten.RunGame(&Game{ship: NewPlayerShip()}); err != nil {
+ panic(err)
+ }
+}
A render.go => render.go +66 -0
@@ 0,0 1,66 @@
+package main
+
+import (
+ "image/color"
+
+ "github.com/hajimehoshi/ebiten/v2"
+ "github.com/hajimehoshi/ebiten/v2/ebitenutil"
+ "github.com/hajimehoshi/ebiten/v2/vector"
+)
+
+// Rendering constants. All coordinate math goes through TilePx;
+// no code should reason about window pixels directly.
+const (
+ TilePx = 16
+ VirtualW = 640
+ VirtualH = 360
+)
+
+// roleColor maps a RoomRole to its tint. The mapping lives here so
+// that ship data stays free of any rendering concerns.
+func roleColor(role RoomRole) color.RGBA {
+ switch role {
+ case RolePilot:
+ return color.RGBA{90, 130, 180, 255} // steel blue
+ case RoleWeapons:
+ return color.RGBA{170, 80, 70, 255} // dusty red
+ case RoleShields:
+ return color.RGBA{80, 140, 120, 255} // teal green
+ case RoleMedBay:
+ return color.RGBA{190, 160, 90, 255} // warm ochre
+ case RoleEngines:
+ return color.RGBA{140, 95, 160, 255} // muted violet
+ default:
+ return color.RGBA{120, 120, 120, 255}
+ }
+}
+
+// drawShip draws every room of the ship. Rendering delegates to
+// drawRoom so per-room logic lives in one place.
+func drawShip(screen *ebiten.Image, s Ship) {
+ for _, r := range s.Rooms {
+ drawRoom(screen, r)
+ }
+}
+
+// drawRoom draws a single room: a role-tinted fill, a 1-px border,
+// and the role name printed inside.
+func drawRoom(screen *ebiten.Image, r Room) {
+ x := float32(r.GridX * TilePx)
+ y := float32(r.GridY * TilePx)
+ w := float32(r.GridW * TilePx)
+ h := float32(r.GridH * TilePx)
+
+ fill := roleColor(r.Role)
+ vector.DrawFilledRect(screen, x, y, w, h, fill, false)
+
+ border := color.RGBA{230, 230, 230, 255}
+ // Four 1-px edges.
+ vector.DrawFilledRect(screen, x, y, w, 1, border, false)
+ vector.DrawFilledRect(screen, x, y+h-1, w, 1, border, false)
+ vector.DrawFilledRect(screen, x, y, 1, h, border, false)
+ vector.DrawFilledRect(screen, x+w-1, y, 1, h, border, false)
+
+ // Label: a few pixels inside the top-left corner.
+ ebitenutil.DebugPrintAt(screen, r.Name, int(x)+3, int(y)+2)
+}
A ship.go => ship.go +47 -0
@@ 0,0 1,47 @@
+package main
+
+// RoomRole identifies the functional purpose of a room aboard a ship.
+// Rendering code maps roles to colors; ship data itself stays
+// free of any renderer-specific information.
+type RoomRole int
+
+const (
+ RolePilot RoomRole = iota
+ RoleWeapons
+ RoleShields
+ RoleMedBay
+ RoleEngines
+)
+
+// Room describes a rectangular cell on the ship grid.
+// All coordinates and sizes are in tile units; conversion to pixels
+// happens only in the renderer.
+type Room struct {
+ ID int
+ Name string
+ GridX int
+ GridY int
+ GridW int
+ GridH int
+ Role RoomRole
+}
+
+// Ship is a pure-data description of a ship layout.
+type Ship struct {
+ Rooms []Room
+}
+
+// NewPlayerShip returns a hard-coded 5-room placeholder layout:
+// Pilot front-left, Weapons mid-upper, Shields mid-center,
+// MedBay mid-lower, Engines rear-right.
+func NewPlayerShip() Ship {
+ return Ship{
+ Rooms: []Room{
+ {ID: 1, Name: "Pilot", GridX: 3, GridY: 9, GridW: 3, GridH: 3, Role: RolePilot},
+ {ID: 2, Name: "Weapons", GridX: 6, GridY: 6, GridW: 4, GridH: 3, Role: RoleWeapons},
+ {ID: 3, Name: "Shields", GridX: 6, GridY: 9, GridW: 4, GridH: 3, Role: RoleShields},
+ {ID: 4, Name: "MedBay", GridX: 6, GridY: 12, GridW: 4, GridH: 3, Role: RoleMedBay},
+ {ID: 5, Name: "Engines", GridX: 10, GridY: 9, GridW: 3, GridH: 3, Role: RoleEngines},
+ },
+ }
+}