~bigbes/game-prototype-ftl

ref: f9d6b6136f9ab4134496b9c1f0fac5eb21d711a9 game-prototype-ftl/render.go -rw-r--r-- 5.0 KiB
f9d6b613 — Eugene Blikh docs(systems-power): record verify summary 30 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package main

import (
	"fmt"
	"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}
	}
}

// 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 {
		powered := sys[int(r.Role)].PowerLevel > 0
		drawRoom(screen, r, powered)
	}
}

// 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)

	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}
	// 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)
}

// 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}

func drawCrew(screen *ebiten.Image, c Crew, selected bool) {
	var fx, fy float64
	if len(c.Path) > 0 {
		fx = float64(c.TileX) + (float64(c.Path[0][0])-float64(c.TileX))*c.MoveT
		fy = float64(c.TileY) + (float64(c.Path[0][1])-float64(c.TileY))*c.MoveT
	} else {
		fx = float64(c.TileX)
		fy = float64(c.TileY)
	}
	px := float32(fx*TilePx + TilePx/2)
	py := float32(fy*TilePx + TilePx/2)

	vector.DrawFilledCircle(screen, px, py, CrewRadius, c.Color, true)
	if selected {
		vector.StrokeCircle(screen, px, py, CrewRadius+1, 1, white, true)
	}
	ebitenutil.DebugPrintAt(screen, string(c.Initial), int(px)-2, int(py)-5)
}