new features

This commit is contained in:
2026-05-25 22:27:53 +02:00
parent c4c99260bd
commit f75ada75ba
11 changed files with 1017 additions and 59 deletions

View File

@@ -1,10 +1,93 @@
package pncdsl
// AnimationClip is a placeholder for sprite-sheet animation data. Not used
// for rendering yet — characters draw as flat colored rectangles in this
// milestone — but the field exists so domain code can reference it.
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 // source rects on the sprite sheet
FrameTime float64
Loop bool
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
}