~bigbes/game-prototype-ftl

232ad036459ddd1e19b5c4963d971ddb3b8b197d — Eugene Blikh a month ago b4322f8
scaffold game loop and placeholder ship render
5 files changed, 176 insertions(+), 0 deletions(-)

M go.mod
A go.sum
A main.go
A render.go
A ship.go
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},
		},
	}
}