~bigbes/game-prototype-ftl

1a02c6ed43bd641d8988ad4a214052ffa5807926 — Eugene Blikh 30 days ago 1d4896f
systems-power: drawHud, room dimming, right-click input

systems-power
3 files changed, 108 insertions(+), 11 deletions(-)

M main.go
M render.go
M web/index.html
M main.go => main.go +30 -2
@@ 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)
	}
}

M render.go => render.go +77 -8
@@ 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}

M web/index.html => web/index.html +1 -1
@@ 5,7 5,7 @@
  <title>ftl-shape</title>
  <style>body { margin: 0; background: #000; } canvas { display: block; margin: 0 auto; }</style>
</head>
<body>
<body oncontextmenu="return false">
  <canvas id="ebiten" width="1280" height="720"></canvas>
  <script src="wasm_exec.js"></script>
  <script>