Added menu and sprinting
This commit is contained in:
2
Makefile
2
Makefile
@@ -45,6 +45,6 @@ help:
|
|||||||
@echo " build - Build binary to bin/"
|
@echo " build - Build binary to bin/"
|
||||||
@echo " clean - Remove build artifacts"
|
@echo " clean - Remove build artifacts"
|
||||||
@echo " fmt - Format code"
|
@echo " fmt - Format code"
|
||||||
\ @echo " deps - Download and tidy dependencies"
|
@echo " deps - Download and tidy dependencies"
|
||||||
@echo " build-local - Clean build for current platform"
|
@echo " build-local - Clean build for current platform"
|
||||||
@echo " build-all - Build for all platforms"
|
@echo " build-all - Build for all platforms"
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||||
|
|
||||||
"github.com/atridad/BigFeelings/internal/hero"
|
"github.com/atridad/BigFeelings/internal/hero"
|
||||||
"github.com/atridad/BigFeelings/internal/status"
|
"github.com/atridad/BigFeelings/internal/status"
|
||||||
"github.com/atridad/BigFeelings/internal/ui/hud"
|
"github.com/atridad/BigFeelings/internal/ui/hud"
|
||||||
|
"github.com/atridad/BigFeelings/internal/ui/menu"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -23,11 +25,19 @@ var (
|
|||||||
backgroundColor = color.NRGBA{R: 0, G: 0, B: 0, A: 255}
|
backgroundColor = color.NRGBA{R: 0, G: 0, B: 0, A: 255}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type gameState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
statePlaying gameState = iota
|
||||||
|
statePaused
|
||||||
|
)
|
||||||
|
|
||||||
type controls struct {
|
type controls struct {
|
||||||
Left bool
|
Left bool
|
||||||
Right bool
|
Right bool
|
||||||
Up bool
|
Up bool
|
||||||
Down bool
|
Down bool
|
||||||
|
Sprint bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func readControls() controls {
|
func readControls() controls {
|
||||||
@@ -36,6 +46,7 @@ func readControls() controls {
|
|||||||
Right: ebiten.IsKeyPressed(ebiten.KeyArrowRight) || ebiten.IsKeyPressed(ebiten.KeyD),
|
Right: ebiten.IsKeyPressed(ebiten.KeyArrowRight) || ebiten.IsKeyPressed(ebiten.KeyD),
|
||||||
Up: ebiten.IsKeyPressed(ebiten.KeyArrowUp) || ebiten.IsKeyPressed(ebiten.KeyW),
|
Up: ebiten.IsKeyPressed(ebiten.KeyArrowUp) || ebiten.IsKeyPressed(ebiten.KeyW),
|
||||||
Down: ebiten.IsKeyPressed(ebiten.KeyArrowDown) || ebiten.IsKeyPressed(ebiten.KeyS),
|
Down: ebiten.IsKeyPressed(ebiten.KeyArrowDown) || ebiten.IsKeyPressed(ebiten.KeyS),
|
||||||
|
Sprint: ebiten.IsKeyPressed(ebiten.KeyShift),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +59,33 @@ func New() *Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) Update() error {
|
func (g *Game) Update() error {
|
||||||
|
// Handle escape key to toggle pause
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
||||||
|
if g.state.gameState == statePlaying {
|
||||||
|
g.state.gameState = statePaused
|
||||||
|
g.state.pauseMenu.Reset()
|
||||||
|
} else if g.state.gameState == statePaused {
|
||||||
|
g.state.gameState = statePlaying
|
||||||
|
// Reset lastTick to prevent delta time accumulation while paused
|
||||||
|
g.state.lastTick = time.Now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.state.gameState == statePlaying {
|
||||||
g.state.update(readControls())
|
g.state.update(readControls())
|
||||||
|
} else if g.state.gameState == statePaused {
|
||||||
|
if selectedOption := g.state.pauseMenu.Update(); selectedOption != nil {
|
||||||
|
switch *selectedOption {
|
||||||
|
case menu.OptionResume:
|
||||||
|
g.state.gameState = statePlaying
|
||||||
|
// Reset lastTick to prevent delta time accumulation while paused
|
||||||
|
g.state.lastTick = time.Now()
|
||||||
|
case menu.OptionQuit:
|
||||||
|
return ebiten.Termination
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,10 +99,11 @@ func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
|||||||
|
|
||||||
type state struct {
|
type state struct {
|
||||||
hero *hero.Hero
|
hero *hero.Hero
|
||||||
status *status.Manager
|
|
||||||
hud hud.Overlay
|
hud hud.Overlay
|
||||||
bounds hero.Bounds
|
bounds hero.Bounds
|
||||||
lastTick time.Time
|
lastTick time.Time
|
||||||
|
pauseMenu *menu.PauseMenu
|
||||||
|
gameState gameState
|
||||||
}
|
}
|
||||||
|
|
||||||
func newState() *state {
|
func newState() *state {
|
||||||
@@ -77,11 +115,9 @@ func newState() *state {
|
|||||||
Radius: 28,
|
Radius: 28,
|
||||||
Speed: 180,
|
Speed: 180,
|
||||||
Color: color.NRGBA{R: 210, G: 220, B: 255, A: 255},
|
Color: color.NRGBA{R: 210, G: 220, B: 255, A: 255},
|
||||||
}),
|
MaxStamina: 100,
|
||||||
status: status.NewManager([]status.Config{
|
StaminaDrain: 50,
|
||||||
{Label: "Core", Base: 60, Color: color.NRGBA{R: 255, G: 208, B: 0, A: 255}},
|
StaminaRegen: 30,
|
||||||
{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{
|
hud: hud.Overlay{
|
||||||
X: ScreenWidth - 220,
|
X: ScreenWidth - 220,
|
||||||
@@ -93,6 +129,8 @@ func newState() *state {
|
|||||||
Height: ScreenHeight,
|
Height: ScreenHeight,
|
||||||
},
|
},
|
||||||
lastTick: now,
|
lastTick: now,
|
||||||
|
pauseMenu: menu.NewPauseMenu(),
|
||||||
|
gameState: statePlaying,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,13 +144,30 @@ func (s *state) update(input controls) {
|
|||||||
Right: input.Right,
|
Right: input.Right,
|
||||||
Up: input.Up,
|
Up: input.Up,
|
||||||
Down: input.Down,
|
Down: input.Down,
|
||||||
|
Sprint: input.Sprint,
|
||||||
}, dt, s.bounds)
|
}, dt, s.bounds)
|
||||||
|
|
||||||
s.status.Update()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *state) draw(screen *ebiten.Image) {
|
func (s *state) draw(screen *ebiten.Image) {
|
||||||
screen.Fill(backgroundColor)
|
screen.Fill(backgroundColor)
|
||||||
s.hero.Draw(screen)
|
s.hero.Draw(screen)
|
||||||
s.hud.Draw(screen, s.status.Meters())
|
|
||||||
|
// Create stamina meter from hero's stamina
|
||||||
|
staminaColor := color.NRGBA{R: 0, G: 255, B: 180, A: 255}
|
||||||
|
if s.hero.Stamina < s.hero.MaxStamina*0.2 {
|
||||||
|
staminaColor = color.NRGBA{R: 255, G: 60, B: 60, A: 255}
|
||||||
|
}
|
||||||
|
|
||||||
|
staminaMeter := status.Meter{
|
||||||
|
Label: "Stamina",
|
||||||
|
Base: s.hero.MaxStamina,
|
||||||
|
Level: s.hero.Stamina,
|
||||||
|
Color: staminaColor,
|
||||||
|
}
|
||||||
|
s.hud.Draw(screen, []status.Meter{staminaMeter})
|
||||||
|
|
||||||
|
// Draw pause menu if paused
|
||||||
|
if s.gameState == statePaused {
|
||||||
|
s.pauseMenu.Draw(screen, ScreenWidth, ScreenHeight)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type Input struct {
|
|||||||
Right bool
|
Right bool
|
||||||
Up bool
|
Up bool
|
||||||
Down bool
|
Down bool
|
||||||
|
Sprint bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Playfield limits for movement.
|
// Playfield limits for movement.
|
||||||
@@ -22,6 +23,15 @@ type Bounds struct {
|
|||||||
Height float64
|
Height float64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Visual states for the hero.
|
||||||
|
type VisualState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateIdle VisualState = iota
|
||||||
|
StateSprinting
|
||||||
|
StateExhausted
|
||||||
|
)
|
||||||
|
|
||||||
// Player avatar data.
|
// Player avatar data.
|
||||||
type Hero struct {
|
type Hero struct {
|
||||||
X float64
|
X float64
|
||||||
@@ -29,6 +39,13 @@ type Hero struct {
|
|||||||
Radius float64
|
Radius float64
|
||||||
Speed float64
|
Speed float64
|
||||||
Color color.NRGBA
|
Color color.NRGBA
|
||||||
|
Stamina float64
|
||||||
|
MaxStamina float64
|
||||||
|
StaminaDrain float64
|
||||||
|
StaminaRegen float64
|
||||||
|
canSprint bool
|
||||||
|
wasSprintHeld bool
|
||||||
|
isSprinting bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawn settings for the avatar.
|
// Spawn settings for the avatar.
|
||||||
@@ -38,6 +55,9 @@ type Config struct {
|
|||||||
Radius float64
|
Radius float64
|
||||||
Speed float64
|
Speed float64
|
||||||
Color color.NRGBA
|
Color color.NRGBA
|
||||||
|
MaxStamina float64
|
||||||
|
StaminaDrain float64
|
||||||
|
StaminaRegen float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// Builds an avatar from the config with fallbacks.
|
// Builds an avatar from the config with fallbacks.
|
||||||
@@ -51,6 +71,15 @@ func New(cfg Config) *Hero {
|
|||||||
if cfg.Color.A == 0 {
|
if cfg.Color.A == 0 {
|
||||||
cfg.Color = color.NRGBA{R: 210, G: 220, B: 255, A: 255}
|
cfg.Color = color.NRGBA{R: 210, G: 220, B: 255, A: 255}
|
||||||
}
|
}
|
||||||
|
if cfg.MaxStamina <= 0 {
|
||||||
|
cfg.MaxStamina = 100
|
||||||
|
}
|
||||||
|
if cfg.StaminaDrain <= 0 {
|
||||||
|
cfg.StaminaDrain = 50
|
||||||
|
}
|
||||||
|
if cfg.StaminaRegen <= 0 {
|
||||||
|
cfg.StaminaRegen = 30
|
||||||
|
}
|
||||||
|
|
||||||
return &Hero{
|
return &Hero{
|
||||||
X: cfg.StartX,
|
X: cfg.StartX,
|
||||||
@@ -58,6 +87,12 @@ func New(cfg Config) *Hero {
|
|||||||
Radius: cfg.Radius,
|
Radius: cfg.Radius,
|
||||||
Speed: cfg.Speed,
|
Speed: cfg.Speed,
|
||||||
Color: cfg.Color,
|
Color: cfg.Color,
|
||||||
|
Stamina: cfg.MaxStamina,
|
||||||
|
MaxStamina: cfg.MaxStamina,
|
||||||
|
StaminaDrain: cfg.StaminaDrain,
|
||||||
|
StaminaRegen: cfg.StaminaRegen,
|
||||||
|
canSprint: true,
|
||||||
|
wasSprintHeld: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,14 +113,41 @@ func (h *Hero) Update(input Input, dt float64, bounds Bounds) {
|
|||||||
dy += 1
|
dy += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if dx != 0 || dy != 0 {
|
isMoving := dx != 0 || dy != 0
|
||||||
|
|
||||||
|
if isMoving {
|
||||||
length := math.Hypot(dx, dy)
|
length := math.Hypot(dx, dy)
|
||||||
dx /= length
|
dx /= length
|
||||||
dy /= length
|
dy /= length
|
||||||
}
|
}
|
||||||
|
|
||||||
h.X += dx * h.Speed * dt
|
speed := h.Speed
|
||||||
h.Y += dy * h.Speed * dt
|
|
||||||
|
if !input.Sprint {
|
||||||
|
h.wasSprintHeld = false
|
||||||
|
if h.Stamina >= h.MaxStamina*0.2 {
|
||||||
|
h.canSprint = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h.isSprinting = input.Sprint && h.canSprint && h.Stamina > 0 && isMoving
|
||||||
|
if h.isSprinting {
|
||||||
|
speed *= 2.0
|
||||||
|
h.wasSprintHeld = true
|
||||||
|
h.Stamina -= h.StaminaDrain * dt
|
||||||
|
if h.Stamina <= 0 {
|
||||||
|
h.Stamina = 0
|
||||||
|
h.canSprint = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
h.Stamina += h.StaminaRegen * dt
|
||||||
|
if h.Stamina > h.MaxStamina {
|
||||||
|
h.Stamina = h.MaxStamina
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h.X += dx * speed * dt
|
||||||
|
h.Y += dy * speed * dt
|
||||||
|
|
||||||
maxX := math.Max(h.Radius, bounds.Width-h.Radius)
|
maxX := math.Max(h.Radius, bounds.Width-h.Radius)
|
||||||
maxY := math.Max(h.Radius, bounds.Height-h.Radius)
|
maxY := math.Max(h.Radius, bounds.Height-h.Radius)
|
||||||
@@ -94,8 +156,23 @@ func (h *Hero) Update(input Input, dt float64, bounds Bounds) {
|
|||||||
h.Y = clamp(h.Y, h.Radius, maxY)
|
h.Y = clamp(h.Y, h.Radius, maxY)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renders the avatar as a filled circle.
|
// Returns the current visual state based on hero state.
|
||||||
|
func (h *Hero) getVisualState() VisualState {
|
||||||
|
if h.Stamina < h.MaxStamina*0.2 {
|
||||||
|
return StateExhausted
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.isSprinting {
|
||||||
|
return StateSprinting
|
||||||
|
}
|
||||||
|
|
||||||
|
return StateIdle
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders the avatar.
|
||||||
func (h *Hero) Draw(screen *ebiten.Image) {
|
func (h *Hero) Draw(screen *ebiten.Image) {
|
||||||
|
state := h.getVisualState()
|
||||||
|
|
||||||
vector.FillCircle(
|
vector.FillCircle(
|
||||||
screen,
|
screen,
|
||||||
float32(h.X),
|
float32(h.X),
|
||||||
@@ -104,6 +181,162 @@ func (h *Hero) Draw(screen *ebiten.Image) {
|
|||||||
h.Color,
|
h.Color,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
eyeOffsetX := h.Radius * 0.3
|
||||||
|
eyeOffsetY := h.Radius * 0.25
|
||||||
|
|
||||||
|
switch state {
|
||||||
|
case StateExhausted:
|
||||||
|
drawExhaustedFace(screen, h.X, h.Y, h.Radius, eyeOffsetX, eyeOffsetY)
|
||||||
|
case StateSprinting:
|
||||||
|
drawSprintingFace(screen, h.X, h.Y, h.Radius, eyeOffsetX, eyeOffsetY)
|
||||||
|
case StateIdle:
|
||||||
|
drawIdleFace(screen, h.X, h.Y, h.Radius, eyeOffsetX, eyeOffsetY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawIdleFace(screen *ebiten.Image, x, y, radius, eyeOffsetX, eyeOffsetY float64) {
|
||||||
|
eyeRadius := radius * 0.15
|
||||||
|
|
||||||
|
vector.FillCircle(
|
||||||
|
screen,
|
||||||
|
float32(x-eyeOffsetX),
|
||||||
|
float32(y-eyeOffsetY),
|
||||||
|
float32(eyeRadius),
|
||||||
|
color.NRGBA{R: 0, G: 0, B: 0, A: 255},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
vector.FillCircle(
|
||||||
|
screen,
|
||||||
|
float32(x+eyeOffsetX),
|
||||||
|
float32(y-eyeOffsetY),
|
||||||
|
float32(eyeRadius),
|
||||||
|
color.NRGBA{R: 0, G: 0, B: 0, A: 255},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
smileRadius := radius * 0.5
|
||||||
|
smileY := y + radius*0.15
|
||||||
|
for angle := 0.3; angle <= 2.84; angle += 0.15 {
|
||||||
|
smileX := x + smileRadius*math.Cos(angle)
|
||||||
|
smileYPos := smileY + smileRadius*0.3*math.Sin(angle)
|
||||||
|
vector.FillCircle(
|
||||||
|
screen,
|
||||||
|
float32(smileX),
|
||||||
|
float32(smileYPos),
|
||||||
|
float32(radius*0.08),
|
||||||
|
color.NRGBA{R: 0, G: 0, B: 0, A: 255},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawSprintingFace(screen *ebiten.Image, x, y, radius, eyeOffsetX, eyeOffsetY float64) {
|
||||||
|
eyeWidth := radius * 0.2
|
||||||
|
eyeHeight := radius * 0.12
|
||||||
|
|
||||||
|
for ex := -eyeWidth; ex <= eyeWidth; ex += radius * 0.08 {
|
||||||
|
for ey := -eyeHeight; ey <= eyeHeight; ey += radius * 0.08 {
|
||||||
|
if ex*ex/(eyeWidth*eyeWidth)+ey*ey/(eyeHeight*eyeHeight) <= 1 {
|
||||||
|
vector.FillCircle(
|
||||||
|
screen,
|
||||||
|
float32(x-eyeOffsetX+ex),
|
||||||
|
float32(y-eyeOffsetY+ey),
|
||||||
|
float32(radius*0.05),
|
||||||
|
color.NRGBA{R: 0, G: 0, B: 0, A: 255},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ex := -eyeWidth; ex <= eyeWidth; ex += radius * 0.08 {
|
||||||
|
for ey := -eyeHeight; ey <= eyeHeight; ey += radius * 0.08 {
|
||||||
|
if ex*ex/(eyeWidth*eyeWidth)+ey*ey/(eyeHeight*eyeHeight) <= 1 {
|
||||||
|
vector.FillCircle(
|
||||||
|
screen,
|
||||||
|
float32(x+eyeOffsetX+ex),
|
||||||
|
float32(y-eyeOffsetY+ey),
|
||||||
|
float32(radius*0.05),
|
||||||
|
color.NRGBA{R: 0, G: 0, B: 0, A: 255},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mouthY := y + radius*0.3
|
||||||
|
mouthWidth := radius * 0.5
|
||||||
|
for mx := -mouthWidth; mx <= mouthWidth; mx += radius * 0.08 {
|
||||||
|
vector.FillCircle(
|
||||||
|
screen,
|
||||||
|
float32(x+mx),
|
||||||
|
float32(mouthY),
|
||||||
|
float32(radius*0.06),
|
||||||
|
color.NRGBA{R: 0, G: 0, B: 0, A: 255},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawExhaustedFace(screen *ebiten.Image, x, y, radius, eyeOffsetX, eyeOffsetY float64) {
|
||||||
|
eyeSize := radius * 0.15
|
||||||
|
|
||||||
|
for i := -eyeSize; i <= eyeSize; i += radius * 0.08 {
|
||||||
|
vector.FillCircle(
|
||||||
|
screen,
|
||||||
|
float32(x-eyeOffsetX+i),
|
||||||
|
float32(y-eyeOffsetY+i),
|
||||||
|
float32(radius*0.05),
|
||||||
|
color.NRGBA{R: 0, G: 0, B: 0, A: 255},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
vector.FillCircle(
|
||||||
|
screen,
|
||||||
|
float32(x-eyeOffsetX+i),
|
||||||
|
float32(y-eyeOffsetY-i),
|
||||||
|
float32(radius*0.05),
|
||||||
|
color.NRGBA{R: 0, G: 0, B: 0, A: 255},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := -eyeSize; i <= eyeSize; i += radius * 0.08 {
|
||||||
|
vector.FillCircle(
|
||||||
|
screen,
|
||||||
|
float32(x+eyeOffsetX+i),
|
||||||
|
float32(y-eyeOffsetY+i),
|
||||||
|
float32(radius*0.05),
|
||||||
|
color.NRGBA{R: 0, G: 0, B: 0, A: 255},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
vector.FillCircle(
|
||||||
|
screen,
|
||||||
|
float32(x+eyeOffsetX+i),
|
||||||
|
float32(y-eyeOffsetY-i),
|
||||||
|
float32(radius*0.05),
|
||||||
|
color.NRGBA{R: 0, G: 0, B: 0, A: 255},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
mouthY := y + radius*0.35
|
||||||
|
mouthWidth := radius * 0.2
|
||||||
|
mouthHeight := radius * 0.25
|
||||||
|
|
||||||
|
for angle := 0.0; angle < 2*math.Pi; angle += 0.3 {
|
||||||
|
mx := x + mouthWidth*math.Cos(angle)
|
||||||
|
my := mouthY + mouthHeight*math.Sin(angle)
|
||||||
|
vector.FillCircle(
|
||||||
|
screen,
|
||||||
|
float32(mx),
|
||||||
|
float32(my),
|
||||||
|
float32(radius*0.06),
|
||||||
|
color.NRGBA{R: 0, G: 0, B: 0, A: 255},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func clamp(value, min, max float64) float64 {
|
func clamp(value, min, max float64) float64 {
|
||||||
|
|||||||
@@ -24,10 +24,9 @@ func (o Overlay) Draw(screen *ebiten.Image, meters []status.Meter) {
|
|||||||
// Instruction text
|
// Instruction text
|
||||||
instructions := Column{
|
instructions := Column{
|
||||||
Elements: []Element{
|
Elements: []Element{
|
||||||
Label{Text: "Systems Prototype", Color: o.Color},
|
Label{Text: "Big Feelings", Color: o.Color},
|
||||||
Label{Text: "Move with Arrow Keys / WASD", Color: o.Color},
|
Label{Text: "Move with Arrow Keys / WASD", Color: o.Color},
|
||||||
Label{Text: "Track resource signals and plan ahead.", Color: o.Color},
|
Label{Text: "Hold Shift to Sprint", Color: o.Color}},
|
||||||
},
|
|
||||||
Spacing: 7,
|
Spacing: 7,
|
||||||
}
|
}
|
||||||
instructions.Draw(screen, 16, 16)
|
instructions.Draw(screen, 16, 16)
|
||||||
|
|||||||
148
internal/ui/menu/menu.go
Normal file
148
internal/ui/menu/menu.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package menu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||||
|
"golang.org/x/image/font/basicfont"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
basicFace = text.NewGoXFace(basicfont.Face7x13)
|
||||||
|
basicFaceAscent = basicFace.Metrics().HAscent
|
||||||
|
)
|
||||||
|
|
||||||
|
type MenuOption int
|
||||||
|
|
||||||
|
const (
|
||||||
|
OptionResume MenuOption = iota
|
||||||
|
OptionSettings
|
||||||
|
OptionQuit
|
||||||
|
optionCount
|
||||||
|
)
|
||||||
|
|
||||||
|
type PauseMenu struct {
|
||||||
|
selectedIndex int
|
||||||
|
showWIP bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPauseMenu() *PauseMenu {
|
||||||
|
return &PauseMenu{
|
||||||
|
selectedIndex: 0,
|
||||||
|
showWIP: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the selected option if one was chosen, nil otherwise
|
||||||
|
func (m *PauseMenu) Update() *MenuOption {
|
||||||
|
// Handle up/down navigation
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) || inpututil.IsKeyJustPressed(ebiten.KeyW) {
|
||||||
|
m.selectedIndex--
|
||||||
|
if m.selectedIndex < 0 {
|
||||||
|
m.selectedIndex = int(optionCount) - 1
|
||||||
|
}
|
||||||
|
m.showWIP = false
|
||||||
|
}
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) || inpututil.IsKeyJustPressed(ebiten.KeyS) {
|
||||||
|
m.selectedIndex++
|
||||||
|
if m.selectedIndex >= int(optionCount) {
|
||||||
|
m.selectedIndex = 0
|
||||||
|
}
|
||||||
|
m.showWIP = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle selection
|
||||||
|
if inpututil.IsKeyJustPressed(ebiten.KeyEnter) || inpututil.IsKeyJustPressed(ebiten.KeySpace) {
|
||||||
|
selected := MenuOption(m.selectedIndex)
|
||||||
|
if selected == OptionSettings {
|
||||||
|
m.showWIP = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &selected
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *PauseMenu) Draw(screen *ebiten.Image, screenWidth, screenHeight int) {
|
||||||
|
// Draw semi-transparent overlay
|
||||||
|
overlay := ebiten.NewImage(screenWidth, screenHeight)
|
||||||
|
overlay.Fill(color.RGBA{R: 0, G: 0, B: 0, A: 180})
|
||||||
|
screen.DrawImage(overlay, nil)
|
||||||
|
|
||||||
|
// Menu dimensions
|
||||||
|
menuWidth := 400
|
||||||
|
menuHeight := 300
|
||||||
|
menuX := (screenWidth - menuWidth) / 2
|
||||||
|
menuY := (screenHeight - menuHeight) / 2
|
||||||
|
|
||||||
|
// Draw menu background
|
||||||
|
vector.DrawFilledRect(screen,
|
||||||
|
float32(menuX), float32(menuY),
|
||||||
|
float32(menuWidth), float32(menuHeight),
|
||||||
|
color.RGBA{R: 40, G: 40, B: 50, A: 255},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw menu border
|
||||||
|
vector.StrokeRect(screen,
|
||||||
|
float32(menuX), float32(menuY),
|
||||||
|
float32(menuWidth), float32(menuHeight),
|
||||||
|
2,
|
||||||
|
color.RGBA{R: 100, G: 100, B: 120, A: 255},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw title
|
||||||
|
titleText := "PAUSED"
|
||||||
|
titleX := menuX + (menuWidth / 2) - (len(titleText) * 7 / 2)
|
||||||
|
titleY := menuY + 50
|
||||||
|
m.drawText(screen, titleText, color.White, titleX, titleY)
|
||||||
|
|
||||||
|
// Draw menu options
|
||||||
|
options := []string{"Resume", "Settings", "Quit"}
|
||||||
|
startY := menuY + 110
|
||||||
|
|
||||||
|
for i, option := range options {
|
||||||
|
optionY := startY + (i * 40)
|
||||||
|
optionX := menuX + (menuWidth / 2) - (len(option) * 7 / 2)
|
||||||
|
|
||||||
|
// Draw selection indicator
|
||||||
|
if i == m.selectedIndex {
|
||||||
|
indicatorX := optionX - 20
|
||||||
|
m.drawText(screen, ">", color.RGBA{R: 255, G: 200, B: 0, A: 255}, indicatorX, optionY)
|
||||||
|
m.drawText(screen, option, color.RGBA{R: 255, G: 255, B: 100, A: 255}, optionX, optionY)
|
||||||
|
} else {
|
||||||
|
m.drawText(screen, option, color.RGBA{R: 180, G: 180, B: 180, A: 255}, optionX, optionY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw WIP message if settings was selected
|
||||||
|
if m.showWIP {
|
||||||
|
wipY := startY + 140
|
||||||
|
wipText := "Work In Progress"
|
||||||
|
wipX := menuX + (menuWidth / 2) - (len(wipText) * 7 / 2)
|
||||||
|
m.drawText(screen, wipText, color.RGBA{R: 255, G: 150, B: 0, A: 255}, wipX, wipY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw controls hint at bottom
|
||||||
|
hintText := "Use Arrow Keys/WASD to navigate, Enter/Space to select"
|
||||||
|
hintX := menuX + (menuWidth / 2) - (len(hintText) * 7 / 2)
|
||||||
|
hintY := menuY + menuHeight - 30
|
||||||
|
m.drawText(screen, hintText, color.RGBA{R: 150, G: 150, B: 150, A: 255}, hintX, hintY)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *PauseMenu) drawText(screen *ebiten.Image, txt string, clr color.Color, x, y int) {
|
||||||
|
op := &text.DrawOptions{}
|
||||||
|
op.GeoM.Translate(float64(x), float64(y)-basicFaceAscent)
|
||||||
|
op.ColorScale.ScaleWithColor(clr)
|
||||||
|
text.Draw(screen, txt, basicFace, op)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *PauseMenu) Reset() {
|
||||||
|
m.selectedIndex = 0
|
||||||
|
m.showWIP = false
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user