Files
sanspie 515f4ca1b3 Stage 11a: animation (D333–D340, D344, D354–D357)
Clips (.aeanim) and controllers (.aectrl) with importers, AnimationPlayer and Animator with crossfades, root motion and events on fixed steps, rewind support. Editor: Animation panel (dope sheet, curves, record, preview, trajectory), Animator panel, sprite sheet slicer and flipbooks, Programming workspace, export panel redesign. Motion 2D ledge climb. 11c: the component and Create palette.
2026-10-25 15:31:22 +03:00

138 lines
3.9 KiB
Go

package integration
import (
"testing"
"aego/core/mathx"
"aego/modules/animation"
"aego/modules/gameplay"
"aego/platform/input"
"aego/scene"
)
const (
platformTop = 1.05
platformLeft = 3.0
feetBelow = 0.5
)
func besideLedge(t *testing.T, below float32, climb bool) *platformer {
t.Helper()
pf := openPlatformer(t)
pf.step(5)
if !climb {
pf.motion().LedgeClimb = 0
}
pf.app.Scene.SetPosition(pf.player, mathx.Vec2{X: platformLeft - 0.32, Y: platformTop - feetBelow + below})
pf.hold(input.KeyRight, true)
return pf
}
// Moving into the floating platform from beside it climbs on when its top is within Ledge Climb, and the Animator shows Climb.
func TestLedgeClimbOntoPlatform(t *testing.T) {
for _, below := range []float32{0.02, 0.1, 0.2, 0.3, 0.45} {
pf := besideLedge(t, below, true)
climbed := false
for range 40 {
pf.step(1)
if pf.motion().Climbing {
a, _ := scene.Get[animation.Animator](pf.app.Scene, pf.player)
climbed = climbed || a.State == "Climb"
}
}
if y := pf.y(); !pf.motion().Grounded || mathx.Abs(y-(platformTop-feetBelow)) > 0.05 {
t.Fatalf("%.2f below the top: ended at y %.3f grounded %v, want on the platform", below, y, pf.motion().Grounded)
}
if !climbed {
t.Fatalf("%.2f below the top: the Animator never showed Climb", below)
}
}
pf := besideLedge(t, 0.55, true)
pf.step(60)
if pf.y() < 2 {
t.Fatal("a ledge higher than Ledge Climb must not be climbed")
}
}
// Without ledge climbing the character never hangs on the platform corner: it falls past it.
func TestNoSnagOnPlatformCorner(t *testing.T) {
for _, below := range []float32{0.02, 0.1, 0.2, 0.3} {
pf := besideLedge(t, below, false)
pf.step(60)
if y := pf.y(); y < 2 {
t.Fatalf("%.2f below the top: still at y %.3f after a second, hanging on the corner", below, y)
}
}
}
func startClimb(t *testing.T) *platformer {
t.Helper()
pf := besideLedge(t, 0.3, true)
for range 10 {
pf.step(1)
if pf.motion().Climbing {
return pf
}
}
t.Fatal("the climb did not start")
return nil
}
// Knockback, Stop and Dash end a climb at once; root motion is ignored while climbing.
func TestClimbInterruptions(t *testing.T) {
for name, act := range map[string]func(m *gameplay.Motion2D){
"knockback": func(m *gameplay.Motion2D) { m.AddVelocity(mathx.Vec2{X: -6, Y: -4}) },
"stop": func(m *gameplay.Motion2D) { m.Stop() },
"dash": func(m *gameplay.Motion2D) { m.Dash(mathx.Vec2{X: -8}, 0.2) },
} {
pf := startClimb(t)
act(pf.motion())
pf.step(1)
if pf.motion().Climbing {
t.Fatalf("%s did not end the climb", name)
}
}
a, b := startClimb(t), startClimb(t)
svc := gameplay.Of(b.app.Engine.MainWorld())
for range 6 {
svc.AddDisplacement(b.player, mathx.Vec2{X: -0.2})
a.step(1)
b.step(1)
}
if pa, pb := a.app.Scene.Local(a.player).Position, b.app.Scene.Local(b.player).Position; pa != pb {
t.Fatalf("root motion moved a climbing character: %v vs %v", pb, pa)
}
}
// Rewinding into the middle of a climb and replaying gives the same state hash.
func TestClimbRewind(t *testing.T) {
s, pc := rewindSession(t)
pc.SetKeyEvery(1)
movePlayer(t, pc, mathx.Vec2{X: platformLeft - 0.32, Y: platformTop - feetBelow + 0.3})
e := s.Host.Engine
mid, hash := 0, [32]byte{}
for i := 1; i <= 40; i++ {
e.Input().BeginFrame()
keyHold(e, input.KeyD, true)
pc.Tick(1.0 / 60)
if m, _ := scene.Get[gameplay.Motion2D](pc.Scene(), pc.Scene().Find(pc.Scene().Root(), "Player")); mid == 0 && m.Climbing && m.Velocity == (mathx.Vec2{}) {
mid = i + 3
}
if i == mid && mid > 0 {
hash = pausedHash(pc)
}
}
if mid == 0 || hash == ([32]byte{}) {
t.Fatal("the player never climbed")
}
if err := pc.Seek(mid); err != nil {
t.Fatal(err)
}
if m, _ := scene.Get[gameplay.Motion2D](pc.Scene(), pc.Scene().Find(pc.Scene().Root(), "Player")); !m.Climbing {
t.Fatal("rewinding into the climb lost it")
}
if pc.StateHash() != hash {
t.Fatal("the state hash differs after rewinding into a climb")
}
}