@@ 15,6 15,7 @@ type Game struct {
crew []Crew
selectedCrew int
lastTime time.Time
+ systems []System
}
// Update advances simulation by dt seconds and handles input.
@@ 35,6 36,15 @@ func (g *Game) Update() error {
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
cx, cy := ebiten.CursorPosition()
+ if cy >= HudY {
+ // HUD strip: route to power management.
+ if role, ok := hudHitTest(cx, cy); ok {
+ addPower(g.systems, role, ReactorCap)
+ }
+ // HUD-band miss (gutter / out-of-columns) is a no-op; do not fall through.
+ return nil
+ }
+ // Below HudY: crew-select / move logic.
tx, ty := cx/TilePx, cy/TilePx
for i := range g.crew {
@@ 51,16 61,27 @@ func (g *Game) Update() error {
}
}
+ if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonRight) {
+ cx, cy := ebiten.CursorPosition()
+ if cy >= HudY {
+ if role, ok := hudHitTest(cx, cy); ok {
+ removePower(g.systems, role)
+ }
+ // HUD-band miss is a no-op.
+ }
+ }
+
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)
+ drawShip(screen, g.ship, g.systems)
for i, c := range g.crew {
drawCrew(screen, c, i == g.selectedCrew)
}
+ drawHud(screen, g.systems)
}
// Layout fixes the virtual resolution; the window scales to fit.
@@ 72,9 93,16 @@ func main() {
ship := NewPlayerShip()
walk := walkableTiles(ship)
crew := NewStartingCrew()
+ systems := NewStartingSystems()
ebiten.SetWindowSize(1280, 720)
ebiten.SetWindowTitle("ftl-shape")
- if err := ebiten.RunGame(&Game{ship: ship, walk: walk, crew: crew, selectedCrew: -1}); err != nil {
+ if err := ebiten.RunGame(&Game{
+ ship: ship,
+ walk: walk,
+ crew: crew,
+ selectedCrew: -1,
+ systems: systems,
+ }); err != nil {
panic(err)
}
}
@@ 1,6 1,7 @@
package main
import (
+ "fmt"
"image/color"
"github.com/hajimehoshi/ebiten/v2"
@@ 35,23 36,41 @@ func roleColor(role RoomRole) color.RGBA {
}
}
-// 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) {
+// dimRoleColor returns ~60% brightness of the powered tint for a role.
+// Used by drawRoom when the system is unpowered.
+func dimRoleColor(role RoomRole) color.RGBA {
+ c := roleColor(role)
+ return color.RGBA{
+ R: uint8(uint16(c.R) * 6 / 10),
+ G: uint8(uint16(c.G) * 6 / 10),
+ B: uint8(uint16(c.B) * 6 / 10),
+ A: 255,
+ }
+}
+
+// drawShip draws every room of the ship, dimming rooms whose system has no
+// power. Rendering delegates to drawRoom so per-room logic lives in one place.
+func drawShip(screen *ebiten.Image, s Ship, sys []System) {
for _, r := range s.Rooms {
- drawRoom(screen, r)
+ powered := sys[int(r.Role)].PowerLevel > 0
+ drawRoom(screen, r, powered)
}
}
-// 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) {
+// drawRoom draws a single room: a role-tinted fill (dimmed when unpowered),
+// a 1-px border, and the role name printed inside.
+func drawRoom(screen *ebiten.Image, r Room, powered bool) {
x := float32(r.GridX * TilePx)
y := float32(r.GridY * TilePx)
w := float32(r.GridW * TilePx)
h := float32(r.GridH * TilePx)
- fill := roleColor(r.Role)
+ var fill color.RGBA
+ if powered {
+ fill = roleColor(r.Role)
+ } else {
+ fill = dimRoleColor(r.Role)
+ }
vector.DrawFilledRect(screen, x, y, w, h, fill, false)
border := color.RGBA{230, 230, 230, 255}
@@ 65,6 84,56 @@ func drawRoom(screen *ebiten.Image, r Room) {
ebitenutil.DebugPrintAt(screen, r.Name, int(x)+3, int(y)+2)
}
+// drawHud renders the power management strip at the bottom of the screen.
+// It shows a reactor readout on the left and five per-system power columns
+// on the right. Cells are stacked bottom-up; filled cells use the system's
+// full roleColor, empty cells use dimRoleColor, each with a 1-px white outline.
+func drawHud(screen *ebiten.Image, sys []System) {
+ // Reactor readout.
+ ebitenutil.DebugPrintAt(screen,
+ fmt.Sprintf("Reactor: %d/%d", reactorUsed(sys), ReactorCap),
+ 8, HudY+8)
+
+ const cellW = 12
+ const cellH = 10
+ const cellGap = 2
+ outline := color.RGBA{255, 255, 255, 255}
+
+ // Role labels in display order (index == int(RoomRole)).
+ roleLabels := [HudColCount]string{"Pilot", "Weap", "Shld", "Med", "Eng"}
+
+ for i := 0; i < HudColCount; i++ {
+ role := RoomRole(i)
+ s := sys[i]
+ colLeft := HudX + i*HudColW
+
+ // Role label at the top of the column.
+ ebitenutil.DebugPrintAt(screen, roleLabels[i], colLeft+4, HudY+4)
+
+ // Cells stacked bottom-up. Row 0 is at the bottom of the HUD strip.
+ for j := 0; j < s.MaxLevel; j++ {
+ // Compute y so that row 0 is at the bottom. We allocate space
+ // starting below the label (label height ~12px).
+ cellBottom := HudY + (VirtualH - HudY) - cellGap
+ cy := cellBottom - (j+1)*(cellH+cellGap)
+ cx := colLeft + 4
+
+ var fill color.RGBA
+ if j < s.PowerLevel {
+ fill = roleColor(role)
+ } else {
+ fill = dimRoleColor(role)
+ }
+ vector.DrawFilledRect(screen, float32(cx), float32(cy), float32(cellW), float32(cellH), fill, false)
+ // 1-px white outline on every cell.
+ vector.DrawFilledRect(screen, float32(cx), float32(cy), float32(cellW), 1, outline, false)
+ vector.DrawFilledRect(screen, float32(cx), float32(cy+cellH-1), float32(cellW), 1, outline, false)
+ vector.DrawFilledRect(screen, float32(cx), float32(cy), 1, float32(cellH), outline, false)
+ vector.DrawFilledRect(screen, float32(cx+cellW-1), float32(cy), 1, float32(cellH), outline, false)
+ }
+ }
+}
+
const CrewRadius = 6
var white = color.RGBA{255, 255, 255, 255}