ludic migrate state packages <every example program> packages/ludic.lab/example/plate.ludic 1804 vars into 126 states, 64 into lets; 23498 edits in 460 files Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
114 lines
5.2 KiB
Text
114 lines
5.2 KiB
Text
# platformer_demo.ludic — the ludic.platformer controller (#58) exercised
|
|
# headlessly and deterministically over the shared engine collision (entity
|
|
# solids): gravity + landing, jump apex height, coyote-time tracking, double jump,
|
|
# the JumpRequested veto hook, and a horizontal run into a wall.
|
|
#
|
|
# Movement intent is written directly (the AI / custom-input path: the input system
|
|
# is switched off with `disable system esys_platformer_input`), so the run needs no
|
|
# key feed and is fully reproducible. A full run prints:
|
|
# 1 1 1 1 1 1 1 1
|
|
#
|
|
# Build (from the repo root):
|
|
# LUDIC_MODULES=packages ludicc --headless examples/games/platformer_demo.ludic
|
|
program PlatformerDemo {
|
|
import "ludic.platformer/platformer.ludic"
|
|
|
|
property Position { x: int = 0, y: int = 0 }
|
|
property Body {
|
|
vx: fixed = 0.0, vy: fixed = 0.0, gravity: fixed = 0.0, max_fall: fixed = 0.0,
|
|
rx: fixed = 0.0, ry: fixed = 0.0, policy: int = 0,
|
|
on_ground: int = 0, hit_wall: int = 0, hit_ceiling: int = 0
|
|
}
|
|
property Collider {
|
|
w: int = 0, h: int = 0, offx: int = 0, offy: int = 0,
|
|
is_trigger: int = 0, one_way: int = 0, layer: int = 0, mask: int = 0,
|
|
hit: int = 0, entered: int = 0, exited: int = 0
|
|
}
|
|
|
|
model Hero { Position, Body, Collider, Platformer }
|
|
model Floor { Position, Collider }
|
|
model Wall { Position, Collider }
|
|
@OnSpawn(Hero) handler HeroInit { }
|
|
@OnSpawn(Floor) handler FloorInit { }
|
|
@OnSpawn(Wall) handler WallInit { }
|
|
|
|
state PlatformerDemoState {
|
|
jumps: int = 0
|
|
have_power: int = 0
|
|
vetoed_air: int = 0
|
|
}
|
|
@On(JumpPerformed) handler CountJumps(platformer_demo_st: mut PlatformerDemoState) { platformer_demo_st.jumps += 1 }
|
|
# a game rule: forbid the air (double) jump unless a "power" flag is set — pure
|
|
# lever-4 veto, no controller edit.
|
|
@On(JumpRequested) handler GateAir(platformer_demo_st: mut PlatformerDemoState) { if (kind == 2) and (platformer_demo_st.have_power == 0) { platformer_demo_st.vetoed_air = 1; cancel } }
|
|
|
|
function tick_n(n: int) -> void { var i = 0; while i < n { tick_fixed(); i += 1 } }
|
|
function bi(b: bool) -> int { if b { return 1 }; return 0 }
|
|
function pf_coyote(e: int) -> int {
|
|
let P = World.prop_id("Platformer")
|
|
return World.get(e, P, World.field_id(P, "coyote_t"))
|
|
}
|
|
|
|
entry (platformer_demo_st: mut PlatformerDemoState) {
|
|
let PB = World.prop_id("Body")
|
|
let PP = World.prop_id("Position")
|
|
let PL = World.prop_id("Platformer")
|
|
let fy = World.field_id(PP, "y")
|
|
let fx = World.field_id(PP, "x")
|
|
let fgrd = World.field_id(PB, "on_ground")
|
|
|
|
# a solid floor (top at y=192) and a wall at x=480; both plain solid colliders,
|
|
# so the platformer rides the same esys_move sweep the physics tests use.
|
|
spawn Floor { Position { x: 0, y: 192 }, Collider { w: 640, h: 32 } }
|
|
spawn Wall { Position { x: 480, y: 0 }, Collider { w: 16, h: 192 } }
|
|
|
|
# the input system is off — drive intent directly for a reproducible test.
|
|
disable system esys_platformer_input
|
|
|
|
spawn Hero { Position { x: 80, y: 40 }, Collider { w: 12, h: 16 },
|
|
Platformer { move_speed: 3, jump_height: 4, apex_frames: 14, air_jumps: 1, coyote_frames: 6 } }
|
|
let h = World.query_next(PL, 0)
|
|
|
|
# ---- A. gravity + landing ----------------------------------------------
|
|
tick_n(90)
|
|
let floor_y = 192 - 16 # feet on the floor top
|
|
print(bi(World.get(h, PP, fy) == floor_y)) # 1
|
|
print(bi(World.get(h, PB, fgrd) == 1)) # 1
|
|
|
|
# ---- B. jump apex height ~= jump_height tiles (held for full height) ----
|
|
Platformer.hold(h, 1) # hold jump: full-height arc
|
|
Platformer.jump(h)
|
|
var top = World.get(h, PP, fy)
|
|
var i = 0
|
|
while i < 20 { tick_fixed(); let y = World.get(h, PP, fy); if y < top { top = y }; i += 1 }
|
|
let rise = floor_y - top # px risen at apex (target 64 = 4 tiles)
|
|
print(bi((rise >= 56) and (rise <= 72))) # 1
|
|
Platformer.hold(h, 0)
|
|
tick_n(40)
|
|
print(bi(World.get(h, PB, fgrd) == 1)) # 1 — landed again
|
|
|
|
# ---- C. coyote time is tracked (refilled while grounded) ---------------
|
|
print(bi(pf_coyote(h) == 6)) # 1
|
|
|
|
# ---- D. double jump gated by the veto hook -----------------------------
|
|
platformer_demo_st.have_power = 0
|
|
Platformer.jump(h); tick_n(3) # ground jump -> airborne
|
|
let after_ground = platformer_demo_st.jumps
|
|
Platformer.jump(h); tick_n(2) # air jump attempt -> vetoed
|
|
print(bi((platformer_demo_st.jumps == after_ground) and (platformer_demo_st.vetoed_air == 1))) # 1
|
|
|
|
platformer_demo_st.have_power = 1
|
|
let before_power = platformer_demo_st.jumps
|
|
Platformer.jump(h); tick_n(2) # air jump now allowed
|
|
print(bi(platformer_demo_st.jumps == before_power + 1)) # 1
|
|
tick_n(60) # settle on the floor
|
|
|
|
# ---- E. horizontal run into the wall (collision reuse) -----------------
|
|
Platformer.move(h, 1) # hold right
|
|
tick_n(200)
|
|
let hx = World.get(h, PP, fx)
|
|
print(bi((hx >= 460) and (hx < 480))) # 1 — stopped at the wall (right edge ~468)
|
|
|
|
quit()
|
|
}
|
|
}
|