Added menu and sprinting

This commit is contained in:
2025-11-19 08:50:39 -07:00
parent bd33e7e123
commit c5a3bcb3f4
5 changed files with 494 additions and 59 deletions

View File

@@ -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 {