First run at basic structure
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Binaries
|
||||||
|
bin/
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
50
Makefile
Normal file
50
Makefile
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
.PHONY: run build clean fmt
|
||||||
|
|
||||||
|
export CGO_CFLAGS=-Wno-deprecated-declarations
|
||||||
|
|
||||||
|
run:
|
||||||
|
@go run ./cmd/bigfeelings
|
||||||
|
|
||||||
|
build:
|
||||||
|
@go build -o bin/bigfeelings ./cmd/bigfeelings
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@rm -rf bin/
|
||||||
|
@go clean
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
@go fmt ./...
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
deps:
|
||||||
|
@go mod download
|
||||||
|
@go mod tidy
|
||||||
|
|
||||||
|
# Run with verbose output
|
||||||
|
run-verbose:
|
||||||
|
@go run -v ./cmd/bigfeelings
|
||||||
|
|
||||||
|
# Build for current platform
|
||||||
|
build-local: clean
|
||||||
|
@mkdir -p bin
|
||||||
|
@go build -o bin/bigfeelings ./cmd/bigfeelings
|
||||||
|
@echo "Built: bin/bigfeelings"
|
||||||
|
|
||||||
|
# Build for multiple platforms
|
||||||
|
build-all: clean
|
||||||
|
@mkdir -p bin
|
||||||
|
GOOS=darwin GOARCH=arm64 go build -o bin/bigfeelings-darwin-arm64 ./cmd/bigfeelings
|
||||||
|
GOOS=darwin GOARCH=amd64 go build -o bin/bigfeelings-darwin-amd64 ./cmd/bigfeelings
|
||||||
|
GOOS=linux GOARCH=amd64 go build -o bin/bigfeelings-linux-amd64 ./cmd/bigfeelings
|
||||||
|
GOOS=windows GOARCH=amd64 go build -o bin/bigfeelings-windows-amd64.exe ./cmd/bigfeelings
|
||||||
|
@echo "Built all platforms"
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Available targets:"
|
||||||
|
@echo " run - Run the game"
|
||||||
|
@echo " build - Build binary to bin/"
|
||||||
|
@echo " clean - Remove build artifacts"
|
||||||
|
@echo " fmt - Format code"
|
||||||
|
\ @echo " deps - Download and tidy dependencies"
|
||||||
|
@echo " build-local - Clean build for current platform"
|
||||||
|
@echo " build-all - Build for all platforms"
|
||||||
19
cmd/bigfeelings/main.go
Normal file
19
cmd/bigfeelings/main.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
|
||||||
|
game "github.com/atridad/BigFeelings/internal/game"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ebiten.SetWindowSize(game.ScreenWidth, game.ScreenHeight)
|
||||||
|
ebiten.SetWindowTitle(game.WindowTitle)
|
||||||
|
ebiten.SetTPS(game.TargetTPS)
|
||||||
|
|
||||||
|
if err := ebiten.RunGame(game.New()); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
22
go.mod
Normal file
22
go.mod
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
module github.com/atridad/BigFeelings
|
||||||
|
|
||||||
|
go 1.24.0
|
||||||
|
|
||||||
|
toolchain go1.24.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/hajimehoshi/ebiten/v2 v2.9.4
|
||||||
|
golang.org/x/image v0.31.0
|
||||||
|
)
|
||||||
|
|
||||||
|
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/go-text/typesetting v0.3.0 // indirect
|
||||||
|
github.com/jezek/xgb v1.1.1 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
|
golang.org/x/text v0.29.0 // indirect
|
||||||
|
)
|
||||||
28
go.sum
Normal file
28
go.sum
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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/go-text/typesetting v0.3.0 h1:OWCgYpp8njoxSRpwrdd1bQOxdjOXDj9Rqart9ML4iF4=
|
||||||
|
github.com/go-text/typesetting v0.3.0/go.mod h1:qjZLkhRgOEYMhU9eHBr3AR4sfnGJvOXNLt8yRAySFuY=
|
||||||
|
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
|
||||||
|
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
|
||||||
|
github.com/hajimehoshi/bitmapfont/v4 v4.1.0 h1:eE3qa5Do4qhowZVIHjsrX5pYyyPN6sAFWMsO7QREm3U=
|
||||||
|
github.com/hajimehoshi/bitmapfont/v4 v4.1.0/go.mod h1:/PD+aLjAJ0F2UoQx6hkOfXqWN7BkroDUMr5W+IT1dpE=
|
||||||
|
github.com/hajimehoshi/ebiten/v2 v2.9.4 h1:IlPJpwtksylmmvNhQjv4W2bmCFWXtjY7Z10Esise1bk=
|
||||||
|
github.com/hajimehoshi/ebiten/v2 v2.9.4/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=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
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=
|
||||||
|
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||||
|
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||||
118
internal/game/game.go
Normal file
118
internal/game/game.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
|
||||||
|
"github.com/atridad/BigFeelings/internal/hero"
|
||||||
|
"github.com/atridad/BigFeelings/internal/status"
|
||||||
|
"github.com/atridad/BigFeelings/internal/ui/hud"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ScreenWidth = 960
|
||||||
|
ScreenHeight = 540
|
||||||
|
|
||||||
|
TargetTPS = 60
|
||||||
|
WindowTitle = "Big Feelings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
backgroundColor = color.NRGBA{R: 0, G: 0, B: 0, A: 255}
|
||||||
|
)
|
||||||
|
|
||||||
|
type controls struct {
|
||||||
|
Left bool
|
||||||
|
Right bool
|
||||||
|
Up bool
|
||||||
|
Down bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func readControls() controls {
|
||||||
|
return controls{
|
||||||
|
Left: ebiten.IsKeyPressed(ebiten.KeyArrowLeft) || ebiten.IsKeyPressed(ebiten.KeyA),
|
||||||
|
Right: ebiten.IsKeyPressed(ebiten.KeyArrowRight) || ebiten.IsKeyPressed(ebiten.KeyD),
|
||||||
|
Up: ebiten.IsKeyPressed(ebiten.KeyArrowUp) || ebiten.IsKeyPressed(ebiten.KeyW),
|
||||||
|
Down: ebiten.IsKeyPressed(ebiten.KeyArrowDown) || ebiten.IsKeyPressed(ebiten.KeyS),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
state *state
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Game {
|
||||||
|
return &Game{state: newState()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) Update() error {
|
||||||
|
g.state.update(readControls())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) Draw(screen *ebiten.Image) {
|
||||||
|
g.state.draw(screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
||||||
|
return ScreenWidth, ScreenHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
type state struct {
|
||||||
|
hero *hero.Hero
|
||||||
|
status *status.Manager
|
||||||
|
hud hud.Overlay
|
||||||
|
bounds hero.Bounds
|
||||||
|
lastTick time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func newState() *state {
|
||||||
|
now := time.Now()
|
||||||
|
return &state{
|
||||||
|
hero: hero.New(hero.Config{
|
||||||
|
StartX: ScreenWidth / 2,
|
||||||
|
StartY: ScreenHeight / 2,
|
||||||
|
Radius: 28,
|
||||||
|
Speed: 180,
|
||||||
|
Color: color.NRGBA{R: 210, G: 220, B: 255, A: 255},
|
||||||
|
}),
|
||||||
|
status: status.NewManager([]status.Config{
|
||||||
|
{Label: "Core", Base: 60, Color: color.NRGBA{R: 255, G: 208, B: 0, A: 255}},
|
||||||
|
{Label: "Drive", Base: 45, Color: color.NRGBA{R: 0, G: 190, B: 255, A: 255}},
|
||||||
|
{Label: "Flux", Base: 30, Color: color.NRGBA{R: 255, G: 92, B: 120, A: 255}},
|
||||||
|
}),
|
||||||
|
hud: hud.Overlay{
|
||||||
|
X: ScreenWidth - 220,
|
||||||
|
Y: 20,
|
||||||
|
Color: color.White,
|
||||||
|
},
|
||||||
|
bounds: hero.Bounds{
|
||||||
|
Width: ScreenWidth,
|
||||||
|
Height: ScreenHeight,
|
||||||
|
},
|
||||||
|
lastTick: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) update(input controls) {
|
||||||
|
now := time.Now()
|
||||||
|
dt := now.Sub(s.lastTick).Seconds()
|
||||||
|
s.lastTick = now
|
||||||
|
|
||||||
|
s.hero.Update(hero.Input{
|
||||||
|
Left: input.Left,
|
||||||
|
Right: input.Right,
|
||||||
|
Up: input.Up,
|
||||||
|
Down: input.Down,
|
||||||
|
}, dt, s.bounds)
|
||||||
|
|
||||||
|
s.status.Update()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) draw(screen *ebiten.Image) {
|
||||||
|
screen.Fill(backgroundColor)
|
||||||
|
s.hero.Draw(screen)
|
||||||
|
s.hud.Draw(screen, s.status.Meters())
|
||||||
|
}
|
||||||
117
internal/hero/hero.go
Normal file
117
internal/hero/hero.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package hero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Direction flags from the controls.
|
||||||
|
type Input struct {
|
||||||
|
Left bool
|
||||||
|
Right bool
|
||||||
|
Up bool
|
||||||
|
Down bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Playfield limits for movement.
|
||||||
|
type Bounds struct {
|
||||||
|
Width float64
|
||||||
|
Height float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player avatar data.
|
||||||
|
type Hero struct {
|
||||||
|
X float64
|
||||||
|
Y float64
|
||||||
|
Radius float64
|
||||||
|
Speed float64
|
||||||
|
Color color.NRGBA
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn settings for the avatar.
|
||||||
|
type Config struct {
|
||||||
|
StartX float64
|
||||||
|
StartY float64
|
||||||
|
Radius float64
|
||||||
|
Speed float64
|
||||||
|
Color color.NRGBA
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds an avatar from the config with fallbacks.
|
||||||
|
func New(cfg Config) *Hero {
|
||||||
|
if cfg.Radius <= 0 {
|
||||||
|
cfg.Radius = 24
|
||||||
|
}
|
||||||
|
if cfg.Speed <= 0 {
|
||||||
|
cfg.Speed = 180
|
||||||
|
}
|
||||||
|
if cfg.Color.A == 0 {
|
||||||
|
cfg.Color = color.NRGBA{R: 210, G: 220, B: 255, A: 255}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Hero{
|
||||||
|
X: cfg.StartX,
|
||||||
|
Y: cfg.StartY,
|
||||||
|
Radius: cfg.Radius,
|
||||||
|
Speed: cfg.Speed,
|
||||||
|
Color: cfg.Color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Applies movement input and clamps to the playfield.
|
||||||
|
func (h *Hero) Update(input Input, dt float64, bounds Bounds) {
|
||||||
|
dx, dy := 0.0, 0.0
|
||||||
|
|
||||||
|
if input.Left {
|
||||||
|
dx -= 1
|
||||||
|
}
|
||||||
|
if input.Right {
|
||||||
|
dx += 1
|
||||||
|
}
|
||||||
|
if input.Up {
|
||||||
|
dy -= 1
|
||||||
|
}
|
||||||
|
if input.Down {
|
||||||
|
dy += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if dx != 0 || dy != 0 {
|
||||||
|
length := math.Hypot(dx, dy)
|
||||||
|
dx /= length
|
||||||
|
dy /= length
|
||||||
|
}
|
||||||
|
|
||||||
|
h.X += dx * h.Speed * dt
|
||||||
|
h.Y += dy * h.Speed * dt
|
||||||
|
|
||||||
|
maxX := math.Max(h.Radius, bounds.Width-h.Radius)
|
||||||
|
maxY := math.Max(h.Radius, bounds.Height-h.Radius)
|
||||||
|
|
||||||
|
h.X = clamp(h.X, h.Radius, maxX)
|
||||||
|
h.Y = clamp(h.Y, h.Radius, maxY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders the avatar as a filled circle.
|
||||||
|
func (h *Hero) Draw(screen *ebiten.Image) {
|
||||||
|
vector.FillCircle(
|
||||||
|
screen,
|
||||||
|
float32(h.X),
|
||||||
|
float32(h.Y),
|
||||||
|
float32(h.Radius),
|
||||||
|
h.Color,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clamp(value, min, max float64) float64 {
|
||||||
|
if value < min {
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
if value > max {
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
79
internal/status/status.go
Normal file
79
internal/status/status.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image/color"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HUD resource entry.
|
||||||
|
type Meter struct {
|
||||||
|
Label string
|
||||||
|
Base float64
|
||||||
|
Level float64
|
||||||
|
Color color.NRGBA
|
||||||
|
}
|
||||||
|
|
||||||
|
// Meter template values.
|
||||||
|
type Config struct {
|
||||||
|
Label string
|
||||||
|
Base float64
|
||||||
|
Color color.NRGBA
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collection of meters.
|
||||||
|
type Manager struct {
|
||||||
|
meters []Meter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds meters from configs.
|
||||||
|
func NewManager(cfgs []Config) *Manager {
|
||||||
|
meters := make([]Meter, len(cfgs))
|
||||||
|
for i, cfg := range cfgs {
|
||||||
|
meters[i] = Meter{
|
||||||
|
Label: safeLabel(cfg.Label, i),
|
||||||
|
Base: clamp(cfg.Base, 0, 100),
|
||||||
|
Level: clamp(cfg.Base, 0, 100),
|
||||||
|
Color: nonTransparent(cfg.Color),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Manager{meters: meters}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resets levels to base.
|
||||||
|
func (m *Manager) Update() {
|
||||||
|
for i := range m.meters {
|
||||||
|
m.meters[i].Level = m.meters[i].Base
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Meters exposes a copy of the internal slice to prevent mutation.
|
||||||
|
func (m *Manager) Meters() []Meter {
|
||||||
|
out := make([]Meter, len(m.meters))
|
||||||
|
copy(out, m.meters)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func clamp(value, min, max float64) float64 {
|
||||||
|
if value < min {
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
if value > max {
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func safeLabel(label string, fallback int) string {
|
||||||
|
if label == "" {
|
||||||
|
return fmt.Sprintf("Meter %d", fallback+1)
|
||||||
|
}
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
|
||||||
|
func nonTransparent(c color.NRGBA) color.NRGBA {
|
||||||
|
if c.A == 0 {
|
||||||
|
c.A = 255
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
209
internal/ui/hud/elements.go
Normal file
209
internal/ui/hud/elements.go
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
package hud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||||
|
"golang.org/x/image/font/basicfont"
|
||||||
|
|
||||||
|
"github.com/atridad/BigFeelings/internal/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
basicFace = text.NewGoXFace(basicfont.Face7x13)
|
||||||
|
basicFaceAscent = basicFace.Metrics().HAscent
|
||||||
|
)
|
||||||
|
|
||||||
|
func drawHUDText(screen *ebiten.Image, txt string, clr color.Color, x, y int) {
|
||||||
|
if clr == nil {
|
||||||
|
clr = color.White
|
||||||
|
}
|
||||||
|
op := &text.DrawOptions{}
|
||||||
|
op.GeoM.Translate(float64(x), float64(y)-basicFaceAscent)
|
||||||
|
op.ColorScale.ScaleWithColor(clr)
|
||||||
|
text.Draw(screen, txt, basicFace, op)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drawable HUD chunk.
|
||||||
|
type Element interface {
|
||||||
|
Draw(screen *ebiten.Image, x, y int) (width, height int)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plain text node.
|
||||||
|
type Label struct {
|
||||||
|
Text string
|
||||||
|
Color color.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l Label) Draw(screen *ebiten.Image, x, y int) (int, int) {
|
||||||
|
if l.Color == nil {
|
||||||
|
l.Color = color.White
|
||||||
|
}
|
||||||
|
drawHUDText(screen, l.Text, l.Color, x, y)
|
||||||
|
width := len(l.Text) * 7 // approximate width for basicfont
|
||||||
|
return width, 13
|
||||||
|
}
|
||||||
|
|
||||||
|
// Percent readout node.
|
||||||
|
type PercentageDisplay struct {
|
||||||
|
Meter status.Meter
|
||||||
|
Color color.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PercentageDisplay) Draw(screen *ebiten.Image, x, y int) (int, int) {
|
||||||
|
if p.Color == nil {
|
||||||
|
p.Color = color.White
|
||||||
|
}
|
||||||
|
txt := fmt.Sprintf("%3.0f%%", p.Meter.Level)
|
||||||
|
drawHUDText(screen, txt, p.Color, x, y)
|
||||||
|
return len(txt) * 7, 13
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combined label and percent.
|
||||||
|
type MeterLabel struct {
|
||||||
|
Meter status.Meter
|
||||||
|
Color color.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MeterLabel) Draw(screen *ebiten.Image, x, y int) (int, int) {
|
||||||
|
if m.Color == nil {
|
||||||
|
m.Color = color.White
|
||||||
|
}
|
||||||
|
txt := fmt.Sprintf("%s: %3.0f%%", m.Meter.Label, m.Meter.Level)
|
||||||
|
drawHUDText(screen, txt, m.Color, x, y)
|
||||||
|
return len(txt) * 7, 13
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal meter bar.
|
||||||
|
type Bar struct {
|
||||||
|
Meter status.Meter
|
||||||
|
MaxWidth int
|
||||||
|
Height int
|
||||||
|
BorderColor color.Color
|
||||||
|
ShowBorder bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Bar) Draw(screen *ebiten.Image, x, y int) (int, int) {
|
||||||
|
maxWidth := b.MaxWidth
|
||||||
|
if maxWidth <= 0 {
|
||||||
|
maxWidth = 180
|
||||||
|
}
|
||||||
|
height := b.Height
|
||||||
|
if height <= 0 {
|
||||||
|
height = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
fillWidth := int((b.Meter.Level / 100.0) * float64(maxWidth))
|
||||||
|
if fillWidth < 0 {
|
||||||
|
fillWidth = 0
|
||||||
|
}
|
||||||
|
if fillWidth > maxWidth {
|
||||||
|
fillWidth = maxWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw border if requested
|
||||||
|
if b.ShowBorder {
|
||||||
|
borderColor := b.BorderColor
|
||||||
|
if borderColor == nil {
|
||||||
|
borderColor = color.RGBA{R: 80, G: 80, B: 80, A: 255}
|
||||||
|
}
|
||||||
|
// Top border
|
||||||
|
drawRect(screen, x, y, maxWidth, 1, borderColor)
|
||||||
|
// Bottom border
|
||||||
|
drawRect(screen, x, y+height-1, maxWidth, 1, borderColor)
|
||||||
|
// Left border
|
||||||
|
drawRect(screen, x, y, 1, height, borderColor)
|
||||||
|
// Right border
|
||||||
|
drawRect(screen, x+maxWidth-1, y, 1, height, borderColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw filled portion
|
||||||
|
if fillWidth > 0 {
|
||||||
|
drawRect(screen, x, y, fillWidth, height, b.Meter.Color)
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxWidth, height
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper for filled rectangles.
|
||||||
|
func drawRect(screen *ebiten.Image, x, y, width, height int, clr color.Color) {
|
||||||
|
if width <= 0 || height <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a small 1x1 pixel image and scale it
|
||||||
|
img := ebiten.NewImage(1, 1)
|
||||||
|
img.Fill(clr)
|
||||||
|
|
||||||
|
op := &ebiten.DrawImageOptions{}
|
||||||
|
op.GeoM.Scale(float64(width), float64(height))
|
||||||
|
op.GeoM.Translate(float64(x), float64(y))
|
||||||
|
|
||||||
|
screen.DrawImage(img, op)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty padding block.
|
||||||
|
type Spacer struct {
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Spacer) Draw(screen *ebiten.Image, x, y int) (int, int) {
|
||||||
|
return s.Width, s.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal layout row.
|
||||||
|
type Row struct {
|
||||||
|
Elements []Element
|
||||||
|
Spacing int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Row) Draw(screen *ebiten.Image, x, y int) (int, int) {
|
||||||
|
totalWidth := 0
|
||||||
|
maxHeight := 0
|
||||||
|
currentX := x
|
||||||
|
|
||||||
|
for i, elem := range r.Elements {
|
||||||
|
w, h := elem.Draw(screen, currentX, y)
|
||||||
|
totalWidth += w
|
||||||
|
if h > maxHeight {
|
||||||
|
maxHeight = h
|
||||||
|
}
|
||||||
|
currentX += w
|
||||||
|
if i < len(r.Elements)-1 {
|
||||||
|
currentX += r.Spacing
|
||||||
|
totalWidth += r.Spacing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalWidth, maxHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertical stack layout.
|
||||||
|
type Column struct {
|
||||||
|
Elements []Element
|
||||||
|
Spacing int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Column) Draw(screen *ebiten.Image, x, y int) (int, int) {
|
||||||
|
maxWidth := 0
|
||||||
|
totalHeight := 0
|
||||||
|
currentY := y
|
||||||
|
|
||||||
|
for i, elem := range c.Elements {
|
||||||
|
w, h := elem.Draw(screen, x, currentY)
|
||||||
|
if w > maxWidth {
|
||||||
|
maxWidth = w
|
||||||
|
}
|
||||||
|
totalHeight += h
|
||||||
|
currentY += h
|
||||||
|
if i < len(c.Elements)-1 {
|
||||||
|
currentY += c.Spacing
|
||||||
|
totalHeight += c.Spacing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxWidth, totalHeight
|
||||||
|
}
|
||||||
52
internal/ui/hud/hud.go
Normal file
52
internal/ui/hud/hud.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package hud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
|
||||||
|
"github.com/atridad/BigFeelings/internal/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HUD overlay anchor.
|
||||||
|
type Overlay struct {
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
Color color.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paints the HUD overlay.
|
||||||
|
func (o Overlay) Draw(screen *ebiten.Image, meters []status.Meter) {
|
||||||
|
if o.Color == nil {
|
||||||
|
o.Color = color.White
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instruction text
|
||||||
|
instructions := Column{
|
||||||
|
Elements: []Element{
|
||||||
|
Label{Text: "Systems Prototype", Color: o.Color},
|
||||||
|
Label{Text: "Move with Arrow Keys / WASD", Color: o.Color},
|
||||||
|
Label{Text: "Track resource signals and plan ahead.", Color: o.Color},
|
||||||
|
},
|
||||||
|
Spacing: 7,
|
||||||
|
}
|
||||||
|
instructions.Draw(screen, 16, 16)
|
||||||
|
|
||||||
|
// Meter column
|
||||||
|
meterElements := make([]Element, 0, len(meters))
|
||||||
|
for _, meter := range meters {
|
||||||
|
meterElements = append(meterElements, Column{
|
||||||
|
Elements: []Element{
|
||||||
|
MeterLabel{Meter: meter, Color: o.Color},
|
||||||
|
Bar{Meter: meter, MaxWidth: 180, Height: 8, ShowBorder: false},
|
||||||
|
},
|
||||||
|
Spacing: 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
meterPanel := Column{
|
||||||
|
Elements: meterElements,
|
||||||
|
Spacing: 16,
|
||||||
|
}
|
||||||
|
meterPanel.Draw(screen, o.X, o.Y)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user