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 " clean - Remove build artifacts"
|
||||
@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-all - Build for all platforms"
|
||||
|
||||
@@ -5,10 +5,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
|
||||
"github.com/atridad/BigFeelings/internal/hero"
|
||||
"github.com/atridad/BigFeelings/internal/status"
|
||||
"github.com/atridad/BigFeelings/internal/ui/hud"
|
||||
"github.com/atridad/BigFeelings/internal/ui/menu"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -23,19 +25,28 @@ var (
|
||||
backgroundColor = color.NRGBA{R: 0, G: 0, B: 0, A: 255}
|
||||
)
|
||||
|
||||
type gameState int
|
||||
|
||||
const (
|
||||
statePlaying gameState = iota
|
||||
statePaused
|
||||
)
|
||||
|
||||
type controls struct {
|
||||
Left bool
|
||||
Right bool
|
||||
Up bool
|
||||
Down bool
|
||||
Left bool
|
||||
Right bool
|
||||
Up bool
|
||||
Down bool
|
||||
Sprint 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),
|
||||
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),
|
||||
Sprint: ebiten.IsKeyPressed(ebiten.KeyShift),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +59,33 @@ func New() *Game {
|
||||
}
|
||||
|
||||
func (g *Game) Update() error {
|
||||
g.state.update(readControls())
|
||||
// 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())
|
||||
} 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
|
||||
}
|
||||
|
||||
@@ -61,27 +98,26 @@ func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
||||
}
|
||||
|
||||
type state struct {
|
||||
hero *hero.Hero
|
||||
status *status.Manager
|
||||
hud hud.Overlay
|
||||
bounds hero.Bounds
|
||||
lastTick time.Time
|
||||
hero *hero.Hero
|
||||
hud hud.Overlay
|
||||
bounds hero.Bounds
|
||||
lastTick time.Time
|
||||
pauseMenu *menu.PauseMenu
|
||||
gameState gameState
|
||||
}
|
||||
|
||||
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}},
|
||||
StartX: ScreenWidth / 2,
|
||||
StartY: ScreenHeight / 2,
|
||||
Radius: 28,
|
||||
Speed: 180,
|
||||
Color: color.NRGBA{R: 210, G: 220, B: 255, A: 255},
|
||||
MaxStamina: 100,
|
||||
StaminaDrain: 50,
|
||||
StaminaRegen: 30,
|
||||
}),
|
||||
hud: hud.Overlay{
|
||||
X: ScreenWidth - 220,
|
||||
@@ -92,7 +128,9 @@ func newState() *state {
|
||||
Width: ScreenWidth,
|
||||
Height: ScreenHeight,
|
||||
},
|
||||
lastTick: now,
|
||||
lastTick: now,
|
||||
pauseMenu: menu.NewPauseMenu(),
|
||||
gameState: statePlaying,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,17 +140,34 @@ func (s *state) update(input controls) {
|
||||
s.lastTick = now
|
||||
|
||||
s.hero.Update(hero.Input{
|
||||
Left: input.Left,
|
||||
Right: input.Right,
|
||||
Up: input.Up,
|
||||
Down: input.Down,
|
||||
Left: input.Left,
|
||||
Right: input.Right,
|
||||
Up: input.Up,
|
||||
Down: input.Down,
|
||||
Sprint: input.Sprint,
|
||||
}, 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())
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@ import (
|
||||
|
||||
// Direction flags from the controls.
|
||||
type Input struct {
|
||||
Left bool
|
||||
Right bool
|
||||
Up bool
|
||||
Down bool
|
||||
Left bool
|
||||
Right bool
|
||||
Up bool
|
||||
Down bool
|
||||
Sprint bool
|
||||
}
|
||||
|
||||
// Playfield limits for movement.
|
||||
@@ -22,22 +23,41 @@ type Bounds struct {
|
||||
Height float64
|
||||
}
|
||||
|
||||
// Visual states for the hero.
|
||||
type VisualState int
|
||||
|
||||
const (
|
||||
StateIdle VisualState = iota
|
||||
StateSprinting
|
||||
StateExhausted
|
||||
)
|
||||
|
||||
// Player avatar data.
|
||||
type Hero struct {
|
||||
X float64
|
||||
Y float64
|
||||
Radius float64
|
||||
Speed float64
|
||||
Color color.NRGBA
|
||||
X float64
|
||||
Y float64
|
||||
Radius float64
|
||||
Speed float64
|
||||
Color color.NRGBA
|
||||
Stamina float64
|
||||
MaxStamina float64
|
||||
StaminaDrain float64
|
||||
StaminaRegen float64
|
||||
canSprint bool
|
||||
wasSprintHeld bool
|
||||
isSprinting bool
|
||||
}
|
||||
|
||||
// Spawn settings for the avatar.
|
||||
type Config struct {
|
||||
StartX float64
|
||||
StartY float64
|
||||
Radius float64
|
||||
Speed float64
|
||||
Color color.NRGBA
|
||||
StartX float64
|
||||
StartY float64
|
||||
Radius float64
|
||||
Speed float64
|
||||
Color color.NRGBA
|
||||
MaxStamina float64
|
||||
StaminaDrain float64
|
||||
StaminaRegen float64
|
||||
}
|
||||
|
||||
// Builds an avatar from the config with fallbacks.
|
||||
@@ -51,13 +71,28 @@ func New(cfg Config) *Hero {
|
||||
if cfg.Color.A == 0 {
|
||||
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{
|
||||
X: cfg.StartX,
|
||||
Y: cfg.StartY,
|
||||
Radius: cfg.Radius,
|
||||
Speed: cfg.Speed,
|
||||
Color: cfg.Color,
|
||||
X: cfg.StartX,
|
||||
Y: cfg.StartY,
|
||||
Radius: cfg.Radius,
|
||||
Speed: cfg.Speed,
|
||||
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
|
||||
}
|
||||
|
||||
if dx != 0 || dy != 0 {
|
||||
isMoving := dx != 0 || dy != 0
|
||||
|
||||
if isMoving {
|
||||
length := math.Hypot(dx, dy)
|
||||
dx /= length
|
||||
dy /= length
|
||||
}
|
||||
|
||||
h.X += dx * h.Speed * dt
|
||||
h.Y += dy * h.Speed * dt
|
||||
speed := h.Speed
|
||||
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
state := h.getVisualState()
|
||||
|
||||
vector.FillCircle(
|
||||
screen,
|
||||
float32(h.X),
|
||||
@@ -104,6 +181,162 @@ func (h *Hero) Draw(screen *ebiten.Image) {
|
||||
h.Color,
|
||||
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 {
|
||||
|
||||
@@ -24,10 +24,9 @@ func (o Overlay) Draw(screen *ebiten.Image, meters []status.Meter) {
|
||||
// Instruction text
|
||||
instructions := Column{
|
||||
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: "Track resource signals and plan ahead.", Color: o.Color},
|
||||
},
|
||||
Label{Text: "Hold Shift to Sprint", Color: o.Color}},
|
||||
Spacing: 7,
|
||||
}
|
||||
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