Name change
This commit is contained in:
@@ -2,44 +2,51 @@ package hero
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"math"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
"github.com/atridad/LilGuy/internal/projectile"
|
||||
)
|
||||
|
||||
// Default values and gameplay constants.
|
||||
|
||||
const (
|
||||
// Default values if not specified in config
|
||||
defaultRadius = 24.0
|
||||
defaultSpeed = 180.0
|
||||
defaultSpeed = 200.0
|
||||
defaultMaxStamina = 100.0
|
||||
defaultStaminaDrain = 50.0 // Per second when sprinting
|
||||
defaultStaminaRegen = 30.0 // Per second when not sprinting
|
||||
defaultStaminaDrain = 50.0
|
||||
defaultStaminaRegen = 30.0
|
||||
|
||||
// Sprint mechanics
|
||||
sprintSpeedMultiplier = 2.0
|
||||
sprintRecoveryThreshold = 0.2 // 20% stamina needed to sprint again
|
||||
sprintSpeedMultiplier = 1.8
|
||||
sprintRecoveryThreshold = 0.2
|
||||
|
||||
// Animation
|
||||
normalAnimSpeed = 0.15 // Seconds per frame when walking
|
||||
sprintAnimSpeed = 0.08 // Seconds per frame when sprinting
|
||||
normalAnimSpeed = 0.15
|
||||
idleAnimSpeed = 0.3
|
||||
sprintAnimSpeed = 0.08
|
||||
|
||||
// Visual state thresholds
|
||||
exhaustedThreshold = 0.2 // Show exhausted state below 20% stamina
|
||||
exhaustedThreshold = 0.2
|
||||
|
||||
animFrameWrap = 4096
|
||||
heroSpriteScale = 0.175
|
||||
fixedSpriteHeight = 329.0
|
||||
fixedSpriteWidth = 315.0
|
||||
|
||||
gravity = 1200.0
|
||||
jumpStrength = -450.0
|
||||
maxFallSpeed = 800.0
|
||||
groundFriction = 0.85
|
||||
airFriction = 0.95
|
||||
)
|
||||
|
||||
type Input struct {
|
||||
Left bool
|
||||
Right bool
|
||||
Up bool
|
||||
Down bool
|
||||
Jump bool
|
||||
Sprint bool
|
||||
}
|
||||
|
||||
type Bounds struct {
|
||||
Width float64
|
||||
Height float64
|
||||
Ground float64
|
||||
}
|
||||
|
||||
type VisualState int
|
||||
@@ -53,37 +60,38 @@ const (
|
||||
type Direction int
|
||||
|
||||
const (
|
||||
DirDown Direction = iota
|
||||
DirUp
|
||||
DirLeft
|
||||
DirLeft Direction = iota
|
||||
DirRight
|
||||
)
|
||||
|
||||
type Hero struct {
|
||||
// Position and size
|
||||
X float64
|
||||
Y float64
|
||||
Radius float64
|
||||
|
||||
// Movement
|
||||
VelocityX float64
|
||||
VelocityY float64
|
||||
|
||||
Speed float64
|
||||
|
||||
// Appearance
|
||||
Color color.NRGBA
|
||||
|
||||
// Stamina system
|
||||
Stamina float64
|
||||
MaxStamina float64
|
||||
StaminaDrain float64
|
||||
StaminaRegen float64
|
||||
|
||||
// Internal state
|
||||
canSprint bool
|
||||
wasSprintHeld bool
|
||||
isSprinting bool
|
||||
isMoving bool
|
||||
isGrounded bool
|
||||
direction Direction
|
||||
animFrame int
|
||||
animTimer float64
|
||||
lastAnimKey animationKey
|
||||
|
||||
ProjectileConfig projectile.ProjectileConfig
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
@@ -129,60 +137,80 @@ func New(cfg Config) *Hero {
|
||||
StaminaRegen: cfg.StaminaRegen,
|
||||
canSprint: true,
|
||||
wasSprintHeld: false,
|
||||
direction: DirDown,
|
||||
direction: DirRight,
|
||||
animFrame: 0,
|
||||
animTimer: 0,
|
||||
lastAnimKey: animationKey{direction: DirRight, state: animIdle},
|
||||
ProjectileConfig: projectile.ProjectileConfig{
|
||||
Speed: 500.0,
|
||||
Radius: 4.0,
|
||||
Color: color.NRGBA{R: 255, G: 0, B: 0, A: 255},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hero) Update(input Input, dt float64, bounds Bounds) {
|
||||
h.updateMovement(input, dt, bounds)
|
||||
h.updateStamina(input, dt)
|
||||
h.updateAnimation(input, dt)
|
||||
h.updateAnimation(dt)
|
||||
}
|
||||
|
||||
func (h *Hero) updateMovement(input Input, dt float64, bounds Bounds) {
|
||||
dx, dy := 0.0, 0.0
|
||||
h.VelocityY += gravity * dt
|
||||
if h.VelocityY > maxFallSpeed {
|
||||
h.VelocityY = maxFallSpeed
|
||||
}
|
||||
|
||||
h.Y += h.VelocityY * dt
|
||||
|
||||
footPosition := h.Y
|
||||
if footPosition >= bounds.Ground {
|
||||
h.Y = bounds.Ground
|
||||
h.VelocityY = 0
|
||||
h.isGrounded = true
|
||||
} else {
|
||||
h.isGrounded = false
|
||||
}
|
||||
|
||||
if input.Jump && h.isGrounded {
|
||||
h.VelocityY = jumpStrength
|
||||
h.isGrounded = false
|
||||
}
|
||||
|
||||
targetVelocityX := 0.0
|
||||
if input.Left {
|
||||
dx -= 1
|
||||
targetVelocityX -= h.Speed
|
||||
h.direction = DirLeft
|
||||
}
|
||||
if input.Right {
|
||||
dx += 1
|
||||
targetVelocityX += h.Speed
|
||||
h.direction = DirRight
|
||||
}
|
||||
if input.Up {
|
||||
dy -= 1
|
||||
h.direction = DirUp
|
||||
}
|
||||
if input.Down {
|
||||
dy += 1
|
||||
h.direction = DirDown
|
||||
}
|
||||
|
||||
isMoving := dx != 0 || dy != 0
|
||||
h.isMoving = targetVelocityX != 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
|
||||
h.isSprinting = input.Sprint && h.canSprint && h.Stamina > 0 && h.isMoving
|
||||
if h.isSprinting {
|
||||
speed *= sprintSpeedMultiplier
|
||||
targetVelocityX *= sprintSpeedMultiplier
|
||||
}
|
||||
|
||||
h.X += dx * speed * dt
|
||||
h.Y += dy * speed * dt
|
||||
friction := groundFriction
|
||||
if !h.isGrounded {
|
||||
friction = airFriction
|
||||
}
|
||||
|
||||
maxX := math.Max(h.Radius, bounds.Width-h.Radius)
|
||||
maxY := math.Max(h.Radius, bounds.Height-h.Radius)
|
||||
h.VelocityX = h.VelocityX*friction + targetVelocityX*(1-friction)
|
||||
|
||||
h.X = clamp(h.X, h.Radius, maxX)
|
||||
h.Y = clamp(h.Y, h.Radius, maxY)
|
||||
h.X += h.VelocityX * dt
|
||||
|
||||
if h.X < h.Radius {
|
||||
h.X = h.Radius
|
||||
h.VelocityX = 0
|
||||
}
|
||||
if h.X > bounds.Width-h.Radius {
|
||||
h.X = bounds.Width - h.Radius
|
||||
h.VelocityX = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hero) updateStamina(input Input, dt float64) {
|
||||
@@ -208,22 +236,30 @@ func (h *Hero) updateStamina(input Input, dt float64) {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hero) updateAnimation(input Input, dt float64) {
|
||||
isMoving := input.Left || input.Right || input.Up || input.Down
|
||||
|
||||
func (h *Hero) updateAnimation(dt float64) {
|
||||
isMoving := h.isMoving
|
||||
key := animationKey{direction: h.direction, state: animIdle}
|
||||
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 {
|
||||
key.state = animMove
|
||||
}
|
||||
|
||||
if key != h.lastAnimKey {
|
||||
h.animFrame = 0
|
||||
h.animTimer = 0
|
||||
h.lastAnimKey = key
|
||||
}
|
||||
|
||||
if isMoving {
|
||||
animSpeed := normalAnimSpeed * 0.5
|
||||
if h.isSprinting {
|
||||
animSpeed = sprintAnimSpeed * 0.5
|
||||
}
|
||||
h.animTimer += dt
|
||||
frameAdvance := int(h.animTimer / animSpeed)
|
||||
if frameAdvance > 0 {
|
||||
h.animTimer -= animSpeed * float64(frameAdvance)
|
||||
h.animFrame = (h.animFrame + frameAdvance) % animFrameWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,10 +279,13 @@ 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)
|
||||
actualHeight := float64(bounds.Dy())
|
||||
actualWidth := float64(bounds.Dx())
|
||||
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Translate(-actualWidth/2, -actualHeight)
|
||||
op.GeoM.Scale(heroSpriteScale, heroSpriteScale)
|
||||
op.GeoM.Translate(h.X, h.Y)
|
||||
|
||||
state := h.getVisualState()
|
||||
@@ -254,9 +293,7 @@ func (h *Hero) Draw(screen *ebiten.Image) {
|
||||
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)
|
||||
@@ -264,44 +301,9 @@ func (h *Hero) Draw(screen *ebiten.Image) {
|
||||
}
|
||||
|
||||
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
|
||||
return getKnightSprite(h.direction, h.isMoving, h.animFrame)
|
||||
}
|
||||
|
||||
func clamp(value, min, max float64) float64 {
|
||||
if value < min {
|
||||
return min
|
||||
}
|
||||
if value > max {
|
||||
return max
|
||||
}
|
||||
return value
|
||||
func (h *Hero) GetDirection() Direction {
|
||||
return h.direction
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user