package hero import ( "image/color" "math" "github.com/hajimehoshi/ebiten/v2" ) // ============================================================ // CONFIGURATION // Tweak these values to change gameplay behavior // ============================================================ const ( // Default values if not specified in config defaultRadius = 24.0 defaultSpeed = 180.0 defaultMaxStamina = 100.0 defaultStaminaDrain = 50.0 // Per second when sprinting defaultStaminaRegen = 30.0 // Per second when not sprinting // Sprint mechanics sprintSpeedMultiplier = 2.0 sprintRecoveryThreshold = 0.2 // 20% stamina needed to sprint again // Animation normalAnimSpeed = 0.15 // Seconds per frame when walking sprintAnimSpeed = 0.08 // Seconds per frame when sprinting // Visual state thresholds exhaustedThreshold = 0.2 // Show exhausted state below 20% stamina ) // ============================================================ // TYPES // ============================================================ type Input struct { Left bool Right bool Up bool Down bool Sprint bool } type Bounds struct { Width float64 Height float64 } type VisualState int const ( StateIdle VisualState = iota StateSprinting StateExhausted ) type Direction int const ( DirDown Direction = iota DirUp DirLeft DirRight ) type Hero struct { // Position and size X float64 Y float64 Radius float64 // Movement Speed float64 // Appearance Color color.NRGBA // Stamina system Stamina float64 MaxStamina float64 StaminaDrain float64 StaminaRegen float64 // Internal state canSprint bool wasSprintHeld bool isSprinting bool direction Direction animFrame int animTimer float64 } type Config struct { StartX float64 StartY float64 Radius float64 Speed float64 Color color.NRGBA MaxStamina float64 StaminaDrain float64 StaminaRegen float64 } // ============================================================ // INITIALIZATION // ============================================================ func New(cfg Config) *Hero { if cfg.Radius <= 0 { cfg.Radius = defaultRadius } if cfg.Speed <= 0 { cfg.Speed = defaultSpeed } if cfg.Color.A == 0 { cfg.Color = color.NRGBA{R: 210, G: 220, B: 255, A: 255} } if cfg.MaxStamina <= 0 { cfg.MaxStamina = defaultMaxStamina } if cfg.StaminaDrain <= 0 { cfg.StaminaDrain = defaultStaminaDrain } if cfg.StaminaRegen <= 0 { cfg.StaminaRegen = defaultStaminaRegen } return &Hero{ 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, direction: DirDown, animFrame: 0, animTimer: 0, } } // ============================================================ // UPDATE // ============================================================ func (h *Hero) Update(input Input, dt float64, bounds Bounds) { h.updateMovement(input, dt, bounds) h.updateStamina(input, dt) h.updateAnimation(input, dt) } func (h *Hero) updateMovement(input Input, dt float64, bounds Bounds) { dx, dy := 0.0, 0.0 if input.Left { dx -= 1 h.direction = DirLeft } if input.Right { dx += 1 h.direction = DirRight } if input.Up { dy -= 1 h.direction = DirUp } if input.Down { dy += 1 h.direction = DirDown } isMoving := dx != 0 || dy != 0 if isMoving { length := math.Hypot(dx, dy) dx /= length dy /= length } speed := h.Speed h.isSprinting = input.Sprint && h.canSprint && h.Stamina > 0 && isMoving if h.isSprinting { speed *= sprintSpeedMultiplier } 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) h.X = clamp(h.X, h.Radius, maxX) h.Y = clamp(h.Y, h.Radius, maxY) } func (h *Hero) updateStamina(input Input, dt float64) { if !input.Sprint { h.wasSprintHeld = false if h.Stamina >= h.MaxStamina*sprintRecoveryThreshold { h.canSprint = true } } if h.isSprinting { 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 } } } func (h *Hero) updateAnimation(input Input, dt float64) { isMoving := input.Left || input.Right || input.Up || input.Down if isMoving { animSpeed := normalAnimSpeed if h.isSprinting { animSpeed = sprintAnimSpeed } h.animTimer += dt if h.animTimer >= animSpeed { h.animTimer = 0 h.animFrame = 1 - h.animFrame } } else { h.animFrame = 0 h.animTimer = 0 } } // ============================================================ // STATE // ============================================================ func (h *Hero) getVisualState() VisualState { if h.Stamina < h.MaxStamina*exhaustedThreshold { return StateExhausted } if h.isSprinting { return StateSprinting } return StateIdle } // ============================================================ // RENDERING // ============================================================ func (h *Hero) Draw(screen *ebiten.Image) { sprite := h.getCurrentSprite() if sprite != nil { op := &ebiten.DrawImageOptions{} bounds := sprite.Bounds() w, height := float64(bounds.Dx()), float64(bounds.Dy()) op.GeoM.Translate(-w/2, -height/2) op.GeoM.Translate(h.X, h.Y) state := h.getVisualState() switch state { case StateExhausted: op.ColorScale.ScaleWithColor(color.RGBA{R: 255, G: 100, B: 100, A: 255}) case StateSprinting: // No color change case StateIdle: // No color change } screen.DrawImage(sprite, op) } } func (h *Hero) getCurrentSprite() *ebiten.Image { var sprite *ebiten.Image switch h.direction { case DirUp: if h.animFrame == 0 { sprite = spriteBack1 } else { sprite = spriteBack2 } case DirDown: if h.animFrame == 0 { sprite = spriteFront1 } else { sprite = spriteFront2 } case DirLeft: if h.animFrame == 0 { sprite = spriteLeft1 } else { sprite = spriteLeft2 } case DirRight: if h.animFrame == 0 { sprite = spriteRight1 } else { sprite = spriteRight2 } } return sprite } // ============================================================ // UTILITIES // ============================================================ func clamp(value, min, max float64) float64 { if value < min { return min } if value > max { return max } return value }