94 lines
2.6 KiB
Go
94 lines
2.6 KiB
Go
package pncdsl
|
||
|
||
import "image"
|
||
|
||
// AnimationClip describes one named sprite-sheet animation. Frames are
|
||
// source rectangles into the character's Sprite asset; the renderer
|
||
// projects them onto the character's screen-space W×H footprint.
|
||
//
|
||
// FrameTime is the time (seconds) each frame is shown; Loop=true rewinds
|
||
// to the first frame after the last, Loop=false freezes on the final
|
||
// frame. A clip with zero frames or zero FrameTime is treated as static
|
||
// and the renderer falls back to the whole-sprite draw path.
|
||
type AnimationClip struct {
|
||
Frames []Rectangle
|
||
FrameTime float64
|
||
Loop bool
|
||
}
|
||
|
||
// Reserved clip names the engine picks automatically:
|
||
//
|
||
// "idle" — selected when the character is not moving.
|
||
// "walk" — selected while the character has an active path.
|
||
//
|
||
// A character with neither name falls back to the static whole-sprite
|
||
// (or placeholder) draw.
|
||
const (
|
||
clipIdle = "idle"
|
||
clipWalk = "walk"
|
||
)
|
||
|
||
// tickAnimation advances a character's current animation clip and
|
||
// switches between idle/walk based on the moving flag.
|
||
func (g *Game) tickAnimation(c *runtimeChar, dt float64) {
|
||
desired := clipIdle
|
||
if c.moving {
|
||
desired = clipWalk
|
||
}
|
||
clip, has := c.def.Animations[desired]
|
||
if !has {
|
||
// Fall back to the other clip if the desired one is missing —
|
||
// a character with only "idle" defined keeps idling during walks.
|
||
other := clipIdle
|
||
if desired == clipIdle {
|
||
other = clipWalk
|
||
}
|
||
if cl, ok := c.def.Animations[other]; ok {
|
||
desired = other
|
||
clip = cl
|
||
has = true
|
||
}
|
||
}
|
||
if !has || len(clip.Frames) == 0 || clip.FrameTime <= 0 {
|
||
c.currentClip = ""
|
||
c.clipElapsed = 0
|
||
c.clipFrame = 0
|
||
return
|
||
}
|
||
if c.currentClip != desired {
|
||
c.currentClip = desired
|
||
c.clipElapsed = 0
|
||
c.clipFrame = 0
|
||
}
|
||
c.clipElapsed += dt
|
||
for c.clipElapsed >= clip.FrameTime {
|
||
c.clipElapsed -= clip.FrameTime
|
||
c.clipFrame++
|
||
if c.clipFrame >= len(clip.Frames) {
|
||
if clip.Loop {
|
||
c.clipFrame = 0
|
||
} else {
|
||
c.clipFrame = len(clip.Frames) - 1
|
||
c.clipElapsed = 0
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// currentFrameRect returns the source rectangle of the character's
|
||
// current animation frame, in image.Rectangle form so it can feed
|
||
// ebiten.Image.SubImage directly. ok=false when the character should be
|
||
// drawn without animation (no clip / empty clip / out-of-range frame).
|
||
func (c *runtimeChar) currentFrameRect() (image.Rectangle, bool) {
|
||
if c.currentClip == "" {
|
||
return image.Rectangle{}, false
|
||
}
|
||
clip, ok := c.def.Animations[c.currentClip]
|
||
if !ok || c.clipFrame < 0 || c.clipFrame >= len(clip.Frames) {
|
||
return image.Rectangle{}, false
|
||
}
|
||
r := clip.Frames[c.clipFrame]
|
||
return image.Rect(int(r.X), int(r.Y), int(r.X+r.W), int(r.Y+r.H)), true
|
||
}
|