From 232ad036459ddd1e19b5c4963d971ddb3b8b197d Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Wed, 22 Apr 2026 02:06:16 +0300 Subject: [PATCH] scaffold game loop and placeholder ship render --- go.mod | 11 ++++++++++ go.sum | 16 ++++++++++++++ main.go | 36 ++++++++++++++++++++++++++++++ render.go | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ ship.go | 47 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 176 insertions(+) create mode 100644 go.sum create mode 100644 main.go create mode 100644 render.go create mode 100644 ship.go diff --git a/go.mod b/go.mod index 04c47d3975cd4b37da79049956055fdd9dfd097b..71f938cf40d0a88c70cd55a2fdb8356acc2f4066 100644 --- a/go.mod +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..03c113f6e84ec85df02da08fa76d9df621d023f7 --- /dev/null +++ b/go.sum @@ -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= diff --git a/main.go b/main.go new file mode 100644 index 0000000000000000000000000000000000000000..7d86e990e5d09da1af41eb85e0784164ed11bc82 --- /dev/null +++ b/main.go @@ -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) + } +} diff --git a/render.go b/render.go new file mode 100644 index 0000000000000000000000000000000000000000..a81cbcdd5b4fd5e613d2e4fe2faf2140dfc53e04 --- /dev/null +++ b/render.go @@ -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) +} diff --git a/ship.go b/ship.go new file mode 100644 index 0000000000000000000000000000000000000000..4b78b4986b3971fcb408351bf0983bd227c5011d --- /dev/null +++ b/ship.go @@ -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}, + }, + } +}