feat(engine): 2D collision / physics-lite — Body + Collider + esys_move (#65)
Phase 0 of the gameplay-controller work (#57): turn the static Collision.* overlap tests into a real physics-lite moving-body-with-collision layer that the platformer / shooter / NPC-AI / RPG families build on. New engine-owned system esys_move (runtime/native/systems_move.ludic), spliced and Update-phase-registered when a game declares `Body` (parse.ludic), standing entirely on the reflection ABI like the SpriteAnim / Motion / Light2D systems: - Body { vx, vy, gravity, max_fall, rx, ry, policy, on_ground, hit_wall, hit_ceiling } — Q16.16 velocity + engine-owned sub-pixel accumulators. - Collider { w, h, offx, offy, is_trigger, one_way, layer, mask, hit, entered, exited } — AABB shape off Position, layer/mask filtering, one-way + triggers. - Solids { tile, wall, oneway } — optional config entity enabling the tile-grid broadphase over the Map.* tilemap. esys_move integrates velocity + gravity, then resolves per-axis swept AABB against both solid Collider entities and the tile grid (no tunneling), handles one-way platforms (block only a downward landing), reports trigger/sensor overlaps without resolving, and sets on_ground / hit_wall / hit_ceiling for a controller to poll. No float, no hidden singletons, no runtime dispatch — integer + deterministic, so replay / lockstep / world_save hold. Contact is surfaced as polled flags (the SpriteAnim.event_fired shape), so a game raises its own CollisionResolved / TriggerEntered events with no engine coupling. Two self-asserting examples (entity + tile broadphase) wired into `x test`; docs added to the collision section. A build with no Body compiles byte-for-byte the same (bootstrap fixpoint + all golden renders unchanged). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
6c6dfae235
commit
0b149a6876
7 changed files with 17645 additions and 16763 deletions
|
|
@ -5,3 +5,11 @@ order: 6
|
|||
---
|
||||
|
||||
2D overlap tests on integer coordinates (pixels or tiles). Rectangles are <code>(x, y, w, h)</code> from the top-left; circles are <code>(x, y, r)</code>. Each returns a bool. Squared distances are computed in 64-bit so large coordinates never overflow.
|
||||
|
||||
These are static boolean tests. For a *moving* body with real collision resolution, the engine ships a physics-lite movement system built on the same integer math — declare the well-known components and the engine advances them for you each frame, no handler wired:
|
||||
|
||||
- <code>property Body { vx, vy, gravity, max_fall, rx, ry, policy, on_ground, hit_wall, hit_ceiling }</code> — velocity/accel state in Q16.16 fixed-point (<code>vx</code>/<code>vy</code>/<code>gravity</code>/<code>max_fall</code>), engine-owned sub-pixel accumulators (<code>rx</code>/<code>ry</code>), an integration <code>policy</code> (<code>0</code> platformer with gravity, <code>1</code> top-down), and the derived contact flags the engine sets each frame.
|
||||
- <code>property Collider { w, h, offx, offy, is_trigger, one_way, layer, mask, hit, entered, exited }</code> — an AABB shape offset from the entity's <code>Position { x, y }</code>, with layer/mask filtering, one-way-platform and trigger flags, and per-frame trigger outputs.
|
||||
- <code>property Solids { tile, wall, oneway }</code> — an optional single config entity that turns on the tile-grid broadphase over the <a href="map"><code>Map</code></a> tilemap (<code>tile</code> px size, the solid <code>wall</code> glyph, and an optional one-way <code>oneway</code> glyph).
|
||||
|
||||
The engine-owned <code>esys_move</code> system (Update phase) integrates velocity and gravity into a tentative move, then resolves it with a per-axis **swept AABB** — against both solid <code>Collider</code> entities and the tile grid — so a fast body never tunnels through a wall. It handles one-way platforms (which block only a downward landing), reports trigger/sensor overlaps without resolving them, and sets <code>on_ground</code> / <code>hit_wall</code> / <code>hit_ceiling</code> for a controller to read. Everything is integer and deterministic, so movement reproduces exactly under replay, lockstep and <code>world_save</code> snapshots. Contact is surfaced as polled flags (the <code>SpriteAnim.event_fired</code> shape) rather than engine-emitted events, so a game raises its own <code>CollisionResolved</code> / <code>TriggerEntered</code> events from its handler with no coupling. This is the shared foundation the built-in gameplay controllers build on. Related: <a href="grid"><code>Grid</code></a>, <a href="map"><code>Map</code></a>, <a href="motion"><code>Motion</code></a>, <a href="annot-enginesystem"><code>@EngineSystem</code></a>.
|
||||
|
|
|
|||
125
examples/library/physics.ludic
Normal file
125
examples/library/physics.ludic
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# physics.ludic — the 2D collision / physics-lite foundation (#65): the
|
||||
# engine-owned `esys_move` system integrating velocity + gravity and resolving
|
||||
# swept-AABB collision against solid `Collider` entities, driven straight from
|
||||
# `entry` (tick_fixed runs the Update phase where the engine ticks Body).
|
||||
#
|
||||
# Everything is integer + Q16.16 fixed-point and deterministic, so a full run
|
||||
# prints: 184 1 1 284 1 1 50 51 104 1 134 1 120 0 1 1 1 0 1 1 1
|
||||
program Physics {
|
||||
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
|
||||
}
|
||||
|
||||
function tick_n(n: int) -> void { var i = 0; while i < n { tick_fixed(); i = i + 1 } }
|
||||
function bi(b: bool) -> int { if b { return 1 }; return 0 }
|
||||
|
||||
# despawn every live entity carrying a property, so each scenario starts clean.
|
||||
function clear_prop(name: pointer) -> void {
|
||||
let p = World.prop_id(name)
|
||||
if p < 0 { return }
|
||||
var e = World.query_next(p, 0)
|
||||
while e >= 0 { despawn e; e = World.query_next(p, 0) }
|
||||
}
|
||||
function clear() -> void { clear_prop("Body"); clear_prop("Collider") }
|
||||
|
||||
entry {
|
||||
let pb = World.prop_id("Body")
|
||||
let pc = World.prop_id("Collider")
|
||||
let pp = World.prop_id("Position")
|
||||
let f_y = World.field_id(pp, "y")
|
||||
let f_x = World.field_id(pp, "x")
|
||||
let f_vx = World.field_id(pb, "vx")
|
||||
let f_vy = World.field_id(pb, "vy")
|
||||
let f_grd = World.field_id(pb, "on_ground")
|
||||
let f_hw = World.field_id(pb, "hit_wall")
|
||||
let f_hit = World.field_id(pc, "hit")
|
||||
let f_ent = World.field_id(pc, "entered")
|
||||
let f_ext = World.field_id(pc, "exited")
|
||||
|
||||
# ---- A. gravity + landing on a solid floor entity ----------------------
|
||||
clear()
|
||||
spawn Floor { Position { x: 0, y: 200 }, Collider { w: 400, h: 16 } }
|
||||
spawn Runner { Position { x: 50, y: 40 }, Body { gravity: fixed(1), max_fall: fixed(6) }, Collider { w: 16, h: 16 } }
|
||||
var a = World.query_next(pb, 0)
|
||||
tick_n(80)
|
||||
print(World.get(a, pp, f_y)) # 184 — feet rest on the floor top (200)
|
||||
print(World.get(a, pb, f_grd)) # 1 — on_ground
|
||||
print(bi(World.get(a, pb, f_vy) == 0)) # 1 — vertical velocity zeroed
|
||||
|
||||
# ---- B. horizontal run into a solid wall -------------------------------
|
||||
clear()
|
||||
spawn Floor { Position { x: 0, y: 200 }, Collider { w: 400, h: 16 } }
|
||||
spawn Wall { Position { x: 300, y: 0 }, Collider { w: 16, h: 400 } }
|
||||
spawn Runner { Position { x: 100, y: 184 }, Body { gravity: fixed(1), max_fall: fixed(6) }, Collider { w: 16, h: 16 } }
|
||||
a = World.query_next(pb, 0)
|
||||
# re-apply the run velocity each frame, the way a controller drives it from
|
||||
# held input — so hit_wall stays asserted while pressed against the wall.
|
||||
var bi2 = 0
|
||||
while bi2 < 80 { World.set(a, pb, f_vx, fixed(4)); tick_fixed(); bi2 = bi2 + 1 }
|
||||
print(World.get(a, pp, f_x)) # 284 — right edge stops at the wall (300)
|
||||
print(World.get(a, pb, f_hw)) # 1 — hit_wall while pressed
|
||||
print(bi(World.get(a, pb, f_vx) == 0)) # 1 — the sweep zeroed it this frame
|
||||
|
||||
# ---- C. sub-pixel accumulation (top-down, no gravity) ------------------
|
||||
clear()
|
||||
spawn Runner { Position { x: 50, y: 50 }, Body { vx: 0.25, policy: 1 }, Collider { w: 16, h: 16 } }
|
||||
a = World.query_next(pb, 0)
|
||||
tick_n(3)
|
||||
print(World.get(a, pp, f_x)) # 50 — 0.75px accumulated, no whole step yet
|
||||
tick_n(1)
|
||||
print(World.get(a, pp, f_x)) # 51 — the 4th quarter-pixel carries one px
|
||||
|
||||
# ---- D. landing on a solid platform entity -----------------------------
|
||||
clear()
|
||||
spawn Plat { Position { x: 400, y: 120 }, Collider { w: 64, h: 8 } }
|
||||
spawn Runner { Position { x: 410, y: 40 }, Body { gravity: fixed(1), max_fall: fixed(6) }, Collider { w: 16, h: 16 } }
|
||||
a = World.query_next(pb, 0)
|
||||
tick_n(60)
|
||||
print(World.get(a, pp, f_y)) # 104 — feet on the platform top (120)
|
||||
print(World.get(a, pb, f_grd)) # 1
|
||||
|
||||
# ---- E. one-way platform: land from above, pass through from below -----
|
||||
clear()
|
||||
spawn Plat { Position { x: 400, y: 150 }, Collider { w: 64, h: 8, one_way: 1 } }
|
||||
spawn Runner { Position { x: 405, y: 60 }, Body { gravity: fixed(1), max_fall: fixed(6) }, Collider { w: 16, h: 16 } }
|
||||
let e1 = World.query_next(pb, 0)
|
||||
spawn Riser { Position { x: 430, y: 200 }, Body { vy: fixed(0 - 4), policy: 1 }, Collider { w: 16, h: 16 } }
|
||||
var e2 = World.query_next(pb, 0)
|
||||
if e2 == e1 { e2 = World.query_next(pb, e1 + 1) }
|
||||
tick_n(20)
|
||||
print(World.get(e1, pp, f_y)) # 134 — landed on the one-way top (150)
|
||||
print(World.get(e1, pb, f_grd)) # 1
|
||||
print(World.get(e2, pp, f_y)) # 120 — rose straight through it (200 - 4*20)
|
||||
|
||||
# ---- F. trigger / sensor: enter + exit edges over a moving body --------
|
||||
clear()
|
||||
spawn Sensor { Position { x: 500, y: 100 }, Collider { w: 32, h: 32, is_trigger: 1 } }
|
||||
let sid = World.query_next(pc, 0)
|
||||
spawn Runner { Position { x: 460, y: 108 }, Body { vx: fixed(4), policy: 1 }, Collider { w: 16, h: 16 } }
|
||||
let aid = World.query_next(pb, 0)
|
||||
tick_n(6) # x=484 — not yet overlapping
|
||||
print(World.get(sid, pc, f_ent)) # 0
|
||||
print(bi(World.get(sid, pc, f_hit) < 0)) # 1 — nothing inside
|
||||
tick_n(1) # x=488 — overlap begins this frame
|
||||
print(World.get(sid, pc, f_ent)) # 1 — entered edge
|
||||
print(bi(World.get(sid, pc, f_hit) == aid)) # 1 — reports the body inside
|
||||
tick_n(10) # x=528 — still inside, no new edge
|
||||
print(World.get(sid, pc, f_ent)) # 0
|
||||
print(bi(World.get(sid, pc, f_hit) == aid)) # 1
|
||||
tick_n(1) # x=532 — overlap ends this frame
|
||||
print(World.get(sid, pc, f_ext)) # 1 — exited edge
|
||||
print(bi(World.get(sid, pc, f_hit) < 0)) # 1 — empty again
|
||||
|
||||
quit()
|
||||
}
|
||||
}
|
||||
84
examples/library/physics_tiles.ludic
Normal file
84
examples/library/physics_tiles.ludic
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# physics_tiles.ludic — the tile-grid broadphase of the #65 movement system:
|
||||
# `esys_move` resolves a Body against the Map.* tilemap (the platformer /
|
||||
# top-down world), configured by a single `Solids { tile, wall, oneway }` entity.
|
||||
# Reuses the same deterministic swept-AABB core as entity collision; verifies a
|
||||
# tile floor, a tile wall, and a one-way tile platform (land from above, rise
|
||||
# through from below).
|
||||
#
|
||||
# Deterministic, so a full run prints: 112 1 80 1 48 1 60
|
||||
program PhysicsTiles {
|
||||
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 }
|
||||
property Solids { tile: int = 0, wall: int = 0, oneway: int = 0 }
|
||||
|
||||
function clear_prop(name: pointer) -> void {
|
||||
let p = World.prop_id(name)
|
||||
if p < 0 { return }
|
||||
var e = World.query_next(p, 0)
|
||||
while e >= 0 { despawn e; e = World.query_next(p, 0) }
|
||||
}
|
||||
function clear() -> void { clear_prop("Body"); clear_prop("Collider") }
|
||||
|
||||
entry {
|
||||
let pb = World.prop_id("Body")
|
||||
let pp = World.prop_id("Position")
|
||||
let f_x = World.field_id(pp, "x")
|
||||
let f_y = World.field_id(pp, "y")
|
||||
let f_vx = World.field_id(pb, "vx")
|
||||
let f_grd = World.field_id(pb, "on_ground")
|
||||
let f_hw = World.field_id(pb, "hit_wall")
|
||||
|
||||
# 16px tiles; '#' (35) is solid, '=' (61) is a one-way platform.
|
||||
Map.size(16, 10)
|
||||
Map.row(4, " === ") # one-way platform, cols 10-12 (y 64)
|
||||
Map.row(5, " # ") # wall column, col 6, rows 5-7
|
||||
Map.row(6, " # ")
|
||||
Map.row(7, " # ")
|
||||
Map.row(8, "################") # solid floor, row 8 (top edge y 128)
|
||||
spawn Config { Solids { tile: 16, wall: 35, oneway: 61 } }
|
||||
|
||||
# ---- T1. fall and land on the tile floor -------------------------------
|
||||
clear()
|
||||
spawn Runner { Position { x: 32, y: 16 }, Body { gravity: fixed(1), max_fall: fixed(6) }, Collider { w: 16, h: 16 } }
|
||||
var a = World.query_next(pb, 0)
|
||||
var i = 0
|
||||
while i < 80 { tick_fixed(); i = i + 1 }
|
||||
print(World.get(a, pp, f_y)) # 112 — feet rest on the tile floor top (128)
|
||||
print(World.get(a, pb, f_grd)) # 1
|
||||
|
||||
# ---- T2. run into a tile wall column -----------------------------------
|
||||
clear()
|
||||
spawn Runner { Position { x: 16, y: 112 }, Body { gravity: fixed(1), max_fall: fixed(6) }, Collider { w: 16, h: 16 } }
|
||||
a = World.query_next(pb, 0)
|
||||
i = 0
|
||||
while i < 60 { World.set(a, pb, f_vx, fixed(4)); tick_fixed(); i = i + 1 }
|
||||
print(World.get(a, pp, f_x)) # 80 — right edge stops at the wall column (96)
|
||||
print(World.get(a, pb, f_hw)) # 1
|
||||
|
||||
# ---- T3a. land on a one-way tile platform from above -------------------
|
||||
clear()
|
||||
spawn Runner { Position { x: 176, y: 16 }, Body { gravity: fixed(1), max_fall: fixed(6) }, Collider { w: 16, h: 16 } }
|
||||
a = World.query_next(pb, 0)
|
||||
i = 0
|
||||
while i < 40 { tick_fixed(); i = i + 1 }
|
||||
print(World.get(a, pp, f_y)) # 48 — feet on the one-way top (64)
|
||||
print(World.get(a, pb, f_grd)) # 1
|
||||
|
||||
# ---- T3b. rise straight up through the one-way tile --------------------
|
||||
clear()
|
||||
spawn Riser { Position { x: 176, y: 120 }, Body { vy: fixed(0 - 4), policy: 1 }, Collider { w: 16, h: 16 } }
|
||||
a = World.query_next(pb, 0)
|
||||
i = 0
|
||||
while i < 15 { tick_fixed(); i = i + 1 }
|
||||
print(World.get(a, pp, f_y)) # 60 — passed through (120 - 4*15), above the platform
|
||||
|
||||
quit()
|
||||
}
|
||||
}
|
||||
558
runtime/native/systems_move.ludic
Normal file
558
runtime/native/systems_move.ludic
Normal file
|
|
@ -0,0 +1,558 @@
|
|||
# ============================================================================
|
||||
# systems_move.ludic — the engine-owned 2D movement + collision system (#65).
|
||||
#
|
||||
# Phase 0 of the gameplay-controller work (#57): turns today's static overlap
|
||||
# tests (Collision.*) into a real physics-lite moving-body-with-collision layer
|
||||
# that the platformer / shooter / NPC-AI / RPG families all build on.
|
||||
#
|
||||
# Like the lighting system (systems_light.ludic) this lives in its own file
|
||||
# because it links against the tilemap (rt_tile / rt_mapw / rt_maph in
|
||||
# core.ludic) for the tile-grid broadphase; it is spliced only when a game
|
||||
# declares `Body` or a solid `Collider` (parse.ludic), and inserted into the
|
||||
# Update phase after the game's own handlers (emit_engine_systems_for_phase) so
|
||||
# gameplay this frame is already settled when the world is integrated.
|
||||
#
|
||||
# Everything is integer + deterministic — velocities are Q16.16 fixed-point, the
|
||||
# frame clock is fixed — so lockstep, replay and `world_save` snapshots hold.
|
||||
# There are no floats, no hidden singletons (all state lives in per-entity
|
||||
# components), and no runtime dispatch: derived contact flags are polled by the
|
||||
# game's own handlers, exactly like SpriteAnim.event_fired (#48), so a game emits
|
||||
# its own CollisionResolved / TriggerEntered events without the engine coupling
|
||||
# to a user-declared event.
|
||||
#
|
||||
# Component contracts (POD `property` bundles, every field defaulted so a game
|
||||
# overrides only what it needs, and any absent field no-ops cleanly):
|
||||
#
|
||||
# property Position { x, y } # world anchor, integer px (shared)
|
||||
# property Body {
|
||||
# vx, vy : fixed = 0 # velocity, Q16.16 px/frame
|
||||
# gravity : fixed = 0 # per-frame accel added to vy (policy 0 only)
|
||||
# max_fall : fixed = 0 # terminal downward speed clamp (0 = none)
|
||||
# rx, ry : fixed = 0 # engine-owned subpixel accumulators
|
||||
# policy : int = 0 # 0 = platformer (gravity), 1 = top-down (none)
|
||||
# on_ground : int # OUTPUT 0/1 — a solid is directly below
|
||||
# hit_wall : int # OUTPUT 0/1 — the X sweep was blocked
|
||||
# hit_ceiling : int # OUTPUT 0/1 — blocked moving up
|
||||
# hit_nx, hit_ny : int # OUTPUT — last contact normal (-1/0/1), optional
|
||||
# }
|
||||
# property Collider {
|
||||
# w, h : int # AABB size in px
|
||||
# offx, offy : int = 0 # AABB offset from the Position anchor
|
||||
# is_trigger : int = 0 # 1 = sensor: reports overlap, never resolves
|
||||
# one_way : int = 0 # 1 = platform: blocks only a downward landing
|
||||
# layer : int = 0 # collision layer bitmask (0 = all)
|
||||
# mask : int = 0 # which layers this collides with (0 = all)
|
||||
# hit : int # OUTPUT (trigger) — overlapping entity id, -1 none
|
||||
# entered : int # OUTPUT (trigger) — 1 the frame overlap begins
|
||||
# exited : int # OUTPUT (trigger) — 1 the frame overlap ends
|
||||
# }
|
||||
# property Solids { tile, wall, oneway } # optional, one config entity
|
||||
# tile — tile size in px; > 0 turns the tile-grid broadphase on.
|
||||
# wall — the solid glyph in the Map.* tilemap (as set by Map.row).
|
||||
# oneway — an optional glyph that is a one-way platform (0 = none).
|
||||
#
|
||||
# A Body with no Collider integrates freely (projectiles, particles): velocity is
|
||||
# applied but nothing resolves. A Collider with no Body is a static solid other
|
||||
# bodies collide against (walls, platforms, moving-platform kinematics the game
|
||||
# drives itself).
|
||||
# ============================================================================
|
||||
|
||||
const PHYS_ONE: int = 65536 # Q16.16 one whole pixel
|
||||
|
||||
# ---- per-frame config + reflected ids (filled at the top of esys_move) -------
|
||||
var pm_pos: int = 0 - 1 # Position prop id
|
||||
var pm_pos_x: int = 0 - 1
|
||||
var pm_pos_y: int = 0 - 1
|
||||
var pm_col: int = 0 - 1 # Collider prop id
|
||||
var pm_c_w: int = 0 - 1
|
||||
var pm_c_h: int = 0 - 1
|
||||
var pm_c_offx: int = 0 - 1
|
||||
var pm_c_offy: int = 0 - 1
|
||||
var pm_c_trig: int = 0 - 1
|
||||
var pm_c_oneway: int = 0 - 1
|
||||
var pm_c_layer: int = 0 - 1
|
||||
var pm_c_mask: int = 0 - 1
|
||||
var phys_ts: int = 0 # tile size px (0 = grid off)
|
||||
var phys_wall: int = 0 # solid glyph
|
||||
var phys_oneway: int = 0 # one-way platform glyph (0 = none)
|
||||
|
||||
# the moving body's own layer/mask, so the solid scan can filter by them
|
||||
var phys_cur_layer: int = 0
|
||||
var phys_cur_mask: int = 0
|
||||
|
||||
# sweep outputs (Ludic returns one value, so axis clampers report contact here)
|
||||
var phys_hit_x: int = 0 # X sweep blocked
|
||||
var phys_hit_up: int = 0 # Y sweep blocked moving up
|
||||
var phys_hit_down: int = 0 # Y sweep blocked moving down
|
||||
|
||||
# trigger edge tracking: the body a trigger overlapped last frame, stored as
|
||||
# (entity id + 1) so a zero-filled slot reads as "none" (entity 0 is valid).
|
||||
var phys_trig_prev: words = null
|
||||
|
||||
# ---- small integer helpers --------------------------------------------------
|
||||
# floor division toward negative infinity (positions can go slightly negative);
|
||||
# ts is always > 0 here.
|
||||
function phys_floordiv(a: int, b: int) -> int {
|
||||
var q = a / b
|
||||
let r = a - q * b
|
||||
if (r != 0) and ((a < 0) != (b < 0)) { q = q - 1 }
|
||||
return q
|
||||
}
|
||||
|
||||
# a solid (fully blocking) tile: out of bounds counts as a wall, so the map edge
|
||||
# is a free arena boundary; otherwise the glyph equals the configured wall.
|
||||
function phys_tile_solid(c: int, r: int) -> bool {
|
||||
if c < 0 { return true }
|
||||
if r < 0 { return true }
|
||||
if c >= rt_mapw { return true }
|
||||
if r >= rt_maph { return true }
|
||||
return rt_tile(c, r) == phys_wall
|
||||
}
|
||||
# a one-way platform tile (in bounds only — the arena edge is a full wall above).
|
||||
function phys_tile_oneway(c: int, r: int) -> bool {
|
||||
if phys_oneway == 0 { return false }
|
||||
if c < 0 { return false }
|
||||
if r < 0 { return false }
|
||||
if c >= rt_mapw { return false }
|
||||
if r >= rt_maph { return false }
|
||||
return rt_tile(c, r) == phys_oneway
|
||||
}
|
||||
# is any full-solid tile present in column c over the row span [r0, r1]?
|
||||
function phys_col_solid(c: int, r0: int, r1: int) -> bool {
|
||||
var r = r0
|
||||
while r <= r1 { if phys_tile_solid(c, r) { return true }; r = r + 1 }
|
||||
return false
|
||||
}
|
||||
# is any full-solid tile present in row r over the column span [c0, c1]?
|
||||
function phys_row_solid(r: int, c0: int, c1: int) -> bool {
|
||||
var c = c0
|
||||
while c <= c1 { if phys_tile_solid(c, r) { return true }; c = c + 1 }
|
||||
return false
|
||||
}
|
||||
# is any full-OR-one-way tile present in row r over [c0, c1]? (downward landings)
|
||||
function phys_row_land(r: int, c0: int, c1: int) -> bool {
|
||||
var c = c0
|
||||
while c <= c1 {
|
||||
if phys_tile_solid(c, r) { return true }
|
||||
if phys_tile_oneway(c, r) { return true }
|
||||
c = c + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
# two colliders interact if each side's mask sees the other's layer; a 0 layer or
|
||||
# 0 mask reads as "all" (all-ones), so default-0 components still collide.
|
||||
function phys_match(la: int, ma: int, lb: int, mb: int) -> bool {
|
||||
var al = la; var bl = lb; var am = ma; var bm = mb
|
||||
if al == 0 { al = 0 - 1 }
|
||||
if bl == 0 { bl = 0 - 1 }
|
||||
if am == 0 { am = 0 - 1 }
|
||||
if bm == 0 { bm = 0 - 1 }
|
||||
if (am & bl) == 0 { return false }
|
||||
if (bm & al) == 0 { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
# ---- reflected accessors ----------------------------------------------------
|
||||
function phys_pos_x(e: int) -> int { return World.get(e, pm_pos, pm_pos_x) }
|
||||
function phys_pos_y(e: int) -> int { return World.get(e, pm_pos, pm_pos_y) }
|
||||
function phys_set_x(e: int, v: int) -> void { World.set(e, pm_pos, pm_pos_x, v) }
|
||||
function phys_set_y(e: int, v: int) -> void { World.set(e, pm_pos, pm_pos_y, v) }
|
||||
|
||||
function phys_c_int(e: int, f: int) -> int {
|
||||
if f < 0 { return 0 }
|
||||
return World.get(e, pm_col, f)
|
||||
}
|
||||
# a collider's AABB left / top edge (Position anchor + offset).
|
||||
function phys_aabb_x(e: int) -> int { return phys_pos_x(e) + phys_c_int(e, pm_c_offx) }
|
||||
function phys_aabb_y(e: int) -> int { return phys_pos_y(e) + phys_c_int(e, pm_c_offy) }
|
||||
|
||||
# ---- X sweep ----------------------------------------------------------------
|
||||
# Clamp a body's tentative dx so its AABB (px,py,w,h) stops at the first solid it
|
||||
# would cross this frame — entity colliders and (optionally) the tile grid — and
|
||||
# flag phys_hit_x on contact. One-way platforms never block horizontal motion.
|
||||
function phys_clamp_x(self_e: int, px: int, py: int, w: int, h: int, dx: int) -> int {
|
||||
if dx == 0 { return 0 }
|
||||
var best = dx
|
||||
|
||||
# --- entity solids: every other non-trigger collider we match against
|
||||
if pm_col >= 0 {
|
||||
var s = World.query_next(pm_col, 0)
|
||||
while s >= 0 {
|
||||
if s != self_e {
|
||||
if phys_c_int(s, pm_c_trig) == 0 {
|
||||
if phys_match(phys_cur_layer, phys_cur_mask, phys_c_int(s, pm_c_layer), phys_c_int(s, pm_c_mask)) {
|
||||
if phys_c_int(s, pm_c_oneway) == 0 { # one-way: pass horizontally
|
||||
let sx = phys_aabb_x(s); let sy = phys_aabb_y(s)
|
||||
let sw = phys_c_int(s, pm_c_w); let sh = phys_c_int(s, pm_c_h)
|
||||
if (py < sy + sh) and (sy < py + h) { # vertical spans overlap
|
||||
if dx > 0 {
|
||||
if px + w <= sx { # currently left of s
|
||||
let allow = sx - (px + w)
|
||||
if allow < best { best = allow }
|
||||
}
|
||||
} else {
|
||||
if px >= sx + sw { # currently right of s
|
||||
let allow = (sx + sw) - px
|
||||
if allow > best { best = allow }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
s = World.query_next(pm_col, s + 1)
|
||||
}
|
||||
}
|
||||
|
||||
# --- tile grid: nearest solid column in the swept path over the row span
|
||||
if phys_ts > 0 {
|
||||
let ts = phys_ts
|
||||
let r0 = phys_floordiv(py, ts)
|
||||
let r1 = phys_floordiv(py + h - 1, ts)
|
||||
if dx > 0 {
|
||||
let startc = phys_floordiv(px + w - 1, ts)
|
||||
let endc = phys_floordiv(px + w - 1 + best, ts)
|
||||
var c = startc + 1
|
||||
while c <= endc {
|
||||
if phys_col_solid(c, r0, r1) {
|
||||
let allow = c * ts - (px + w)
|
||||
if allow < best { best = allow }
|
||||
c = endc + 1 # nearest wins; stop
|
||||
} else { c = c + 1 }
|
||||
}
|
||||
} else {
|
||||
let startc = phys_floordiv(px, ts)
|
||||
let endc = phys_floordiv(px + best, ts)
|
||||
var c = startc - 1
|
||||
while c >= endc {
|
||||
if phys_col_solid(c, r0, r1) {
|
||||
let allow = (c + 1) * ts - px
|
||||
if allow > best { best = allow }
|
||||
c = endc - 1 # nearest wins; stop
|
||||
} else { c = c - 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if best != dx { phys_hit_x = 1 }
|
||||
return best
|
||||
}
|
||||
|
||||
# ---- Y sweep ----------------------------------------------------------------
|
||||
# Clamp a body's tentative dy against solids (and one-way platforms on a downward
|
||||
# landing), flagging phys_hit_down / phys_hit_up. Must run after the X sweep so
|
||||
# px is already resolved for this frame.
|
||||
function phys_clamp_y(self_e: int, px: int, py: int, w: int, h: int, dy: int) -> int {
|
||||
if dy == 0 { return 0 }
|
||||
var best = dy
|
||||
|
||||
if pm_col >= 0 {
|
||||
var s = World.query_next(pm_col, 0)
|
||||
while s >= 0 {
|
||||
if s != self_e {
|
||||
if phys_c_int(s, pm_c_trig) == 0 {
|
||||
if phys_match(phys_cur_layer, phys_cur_mask, phys_c_int(s, pm_c_layer), phys_c_int(s, pm_c_mask)) {
|
||||
let sx = phys_aabb_x(s); let sy = phys_aabb_y(s)
|
||||
let sw = phys_c_int(s, pm_c_w); let sh = phys_c_int(s, pm_c_h)
|
||||
let oneway = phys_c_int(s, pm_c_oneway)
|
||||
if (px < sx + sw) and (sx < px + w) { # horizontal spans overlap
|
||||
if dy > 0 {
|
||||
# a one-way platform blocks only when the body starts on/above it
|
||||
var ok = true
|
||||
if oneway != 0 { if py + h > sy { ok = false } }
|
||||
if ok {
|
||||
if py + h <= sy {
|
||||
let allow = sy - (py + h)
|
||||
if allow < best { best = allow }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if oneway == 0 { # one-way: pass upward
|
||||
if py >= sy + sh {
|
||||
let allow = (sy + sh) - py
|
||||
if allow > best { best = allow }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
s = World.query_next(pm_col, s + 1)
|
||||
}
|
||||
}
|
||||
|
||||
if phys_ts > 0 {
|
||||
let ts = phys_ts
|
||||
let c0 = phys_floordiv(px, ts)
|
||||
let c1 = phys_floordiv(px + w - 1, ts)
|
||||
if dy > 0 {
|
||||
let startr = phys_floordiv(py + h - 1, ts)
|
||||
let endr = phys_floordiv(py + h - 1 + best, ts)
|
||||
var r = startr + 1
|
||||
while r <= endr {
|
||||
var block = phys_row_solid(r, c0, c1)
|
||||
# one-way tiles only catch a body whose feet start at/above the tile top
|
||||
if not block {
|
||||
if phys_row_land(r, c0, c1) { if py + h <= r * ts { block = true } }
|
||||
}
|
||||
if block {
|
||||
let allow = r * ts - (py + h)
|
||||
if allow < best { best = allow }
|
||||
r = endr + 1
|
||||
} else { r = r + 1 }
|
||||
}
|
||||
} else {
|
||||
let startr = phys_floordiv(py, ts)
|
||||
let endr = phys_floordiv(py + best, ts)
|
||||
var r = startr - 1
|
||||
while r >= endr {
|
||||
if phys_row_solid(r, c0, c1) { # one-way never blocks upward
|
||||
let allow = (r + 1) * ts - py
|
||||
if allow > best { best = allow }
|
||||
r = endr - 1
|
||||
} else { r = r - 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if best != dy {
|
||||
if dy > 0 { phys_hit_down = 1 } else { phys_hit_up = 1 }
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
# ---- ground probe -----------------------------------------------------------
|
||||
# on_ground is a 1px downward overlap test, independent of this frame's velocity,
|
||||
# so a body resting under sub-pixel gravity still reads grounded.
|
||||
function phys_grounded(self_e: int, px: int, py: int, w: int, h: int) -> bool {
|
||||
# entity solids one pixel below
|
||||
if pm_col >= 0 {
|
||||
var s = World.query_next(pm_col, 0)
|
||||
while s >= 0 {
|
||||
if s != self_e {
|
||||
if phys_c_int(s, pm_c_trig) == 0 {
|
||||
if phys_match(phys_cur_layer, phys_cur_mask, phys_c_int(s, pm_c_layer), phys_c_int(s, pm_c_mask)) {
|
||||
let sx = phys_aabb_x(s); let sy = phys_aabb_y(s)
|
||||
let sw = phys_c_int(s, pm_c_w); let sh = phys_c_int(s, pm_c_h)
|
||||
if (px < sx + sw) and (sx < px + w) {
|
||||
if (py + h <= sy) and (py + h + 1 > sy) { return true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
s = World.query_next(pm_col, s + 1)
|
||||
}
|
||||
}
|
||||
if phys_ts > 0 {
|
||||
let ts = phys_ts
|
||||
let c0 = phys_floordiv(px, ts)
|
||||
let c1 = phys_floordiv(px + w - 1, ts)
|
||||
let footr = phys_floordiv(py + h, ts) # tile row just below the feet
|
||||
if (py + h) == footr * ts { # feet flush on a tile boundary
|
||||
if phys_row_solid(footr, c0, c1) { return true }
|
||||
if phys_row_land(footr, c0, c1) { return true }
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
# ---- the system ------------------------------------------------------------
|
||||
function esys_move() -> void {
|
||||
let pb = World.prop_id("Body")
|
||||
if pb < 0 { return }
|
||||
|
||||
# Body fields
|
||||
let f_vx = World.field_id(pb, "vx")
|
||||
let f_vy = World.field_id(pb, "vy")
|
||||
let f_grav = World.field_id(pb, "gravity")
|
||||
let f_maxf = World.field_id(pb, "max_fall")
|
||||
let f_rx = World.field_id(pb, "rx")
|
||||
let f_ry = World.field_id(pb, "ry")
|
||||
let f_pol = World.field_id(pb, "policy")
|
||||
let f_grd = World.field_id(pb, "on_ground")
|
||||
let f_hw = World.field_id(pb, "hit_wall")
|
||||
let f_hc = World.field_id(pb, "hit_ceiling")
|
||||
let f_nx = World.field_id(pb, "hit_nx")
|
||||
let f_ny = World.field_id(pb, "hit_ny")
|
||||
|
||||
# shared / config props (into module globals the helpers read)
|
||||
pm_pos = World.prop_id("Position")
|
||||
if pm_pos >= 0 {
|
||||
pm_pos_x = World.field_id(pm_pos, "x")
|
||||
pm_pos_y = World.field_id(pm_pos, "y")
|
||||
}
|
||||
pm_col = World.prop_id("Collider")
|
||||
if pm_col >= 0 {
|
||||
pm_c_w = World.field_id(pm_col, "w")
|
||||
pm_c_h = World.field_id(pm_col, "h")
|
||||
pm_c_offx = World.field_id(pm_col, "offx")
|
||||
pm_c_offy = World.field_id(pm_col, "offy")
|
||||
pm_c_trig = World.field_id(pm_col, "is_trigger")
|
||||
pm_c_oneway = World.field_id(pm_col, "one_way")
|
||||
pm_c_layer = World.field_id(pm_col, "layer")
|
||||
pm_c_mask = World.field_id(pm_col, "mask")
|
||||
}
|
||||
phys_ts = 0; phys_wall = 0; phys_oneway = 0
|
||||
let ps = World.prop_id("Solids")
|
||||
if ps >= 0 {
|
||||
let se = World.query_next(ps, 0)
|
||||
if se >= 0 {
|
||||
let f_tile = World.field_id(ps, "tile")
|
||||
let f_wall = World.field_id(ps, "wall")
|
||||
let f_ow = World.field_id(ps, "oneway")
|
||||
if f_tile >= 0 { phys_ts = World.get(se, ps, f_tile) }
|
||||
if f_wall >= 0 { phys_wall = World.get(se, ps, f_wall) }
|
||||
if f_ow >= 0 { phys_oneway = World.get(se, ps, f_ow) }
|
||||
}
|
||||
}
|
||||
|
||||
# a Position is required to move a body; a Body with none is inert.
|
||||
if pm_pos < 0 { return }
|
||||
if pm_pos_x < 0 { return }
|
||||
if pm_pos_y < 0 { return }
|
||||
|
||||
var e = World.query_next(pb, 0)
|
||||
while e >= 0 {
|
||||
if World.has(e, pm_pos) != 0 {
|
||||
# 1. integrate velocity (+ gravity for a platformer policy)
|
||||
var vx = 0; if f_vx >= 0 { vx = World.get(e, pb, f_vx) }
|
||||
var vy = 0; if f_vy >= 0 { vy = World.get(e, pb, f_vy) }
|
||||
var policy = 0; if f_pol >= 0 { policy = World.get(e, pb, f_pol) }
|
||||
if policy != 1 {
|
||||
if f_grav >= 0 { vy = vy + World.get(e, pb, f_grav) }
|
||||
if f_maxf >= 0 {
|
||||
let mf = World.get(e, pb, f_maxf)
|
||||
if (mf > 0) and (vy > mf) { vy = mf }
|
||||
}
|
||||
}
|
||||
|
||||
# 2. accumulate subpixels, extract whole-pixel steps (remainder kept exact)
|
||||
var rx = vx; if f_rx >= 0 { rx = World.get(e, pb, f_rx) + vx }
|
||||
var ry = vy; if f_ry >= 0 { ry = World.get(e, pb, f_ry) + vy }
|
||||
var dx = rx / PHYS_ONE
|
||||
var dy = ry / PHYS_ONE
|
||||
let remx = rx - dx * PHYS_ONE
|
||||
let remy = ry - dy * PHYS_ONE
|
||||
|
||||
var on_ground = 0
|
||||
var hit_wall = 0
|
||||
var hit_ceiling = 0
|
||||
var nx = 0
|
||||
var ny = 0
|
||||
|
||||
# 3. does this body have a solid shape? (a trigger-only body moves freely)
|
||||
var has_shape = false
|
||||
if pm_col >= 0 {
|
||||
if World.has(e, pm_col) != 0 { if phys_c_int(e, pm_c_trig) == 0 { has_shape = true } }
|
||||
}
|
||||
|
||||
if has_shape {
|
||||
phys_cur_layer = phys_c_int(e, pm_c_layer)
|
||||
phys_cur_mask = phys_c_int(e, pm_c_mask)
|
||||
let w = phys_c_int(e, pm_c_w)
|
||||
let h = phys_c_int(e, pm_c_h)
|
||||
var px = phys_aabb_x(e)
|
||||
let py0 = phys_aabb_y(e)
|
||||
|
||||
# X sweep, commit, then Y sweep from the resolved x
|
||||
phys_hit_x = 0
|
||||
let cdx = phys_clamp_x(e, px, py0, w, h, dx)
|
||||
phys_set_x(e, phys_pos_x(e) + cdx)
|
||||
px = phys_aabb_x(e)
|
||||
|
||||
phys_hit_up = 0; phys_hit_down = 0
|
||||
let py = phys_aabb_y(e)
|
||||
let cdy = phys_clamp_y(e, px, py, w, h, dy)
|
||||
phys_set_y(e, phys_pos_y(e) + cdy)
|
||||
|
||||
# derived flags + contact normal
|
||||
if phys_hit_x == 1 { hit_wall = 1; vx = 0; if dx > 0 { nx = 0 - 1 } else { nx = 1 } }
|
||||
if phys_hit_up == 1 { hit_ceiling = 1; vy = 0; ny = 1 }
|
||||
if phys_hit_down == 1 { vy = 0; ny = 0 - 1 }
|
||||
let gy = phys_aabb_y(e)
|
||||
if phys_grounded(e, px, gy, w, h) { on_ground = 1 }
|
||||
# a body pressed onto the ground carries no downward velocity, whatever
|
||||
# the gravity step was this frame (so on_ground implies a settled vy).
|
||||
if (on_ground == 1) and (vy > 0) { vy = 0 }
|
||||
} else {
|
||||
# free integration: apply the whole-pixel step straight to Position
|
||||
phys_set_x(e, phys_pos_x(e) + dx)
|
||||
phys_set_y(e, phys_pos_y(e) + dy)
|
||||
}
|
||||
|
||||
# 4. write velocity + subpixel remainder + derived outputs back
|
||||
if f_vx >= 0 { World.set(e, pb, f_vx, vx) }
|
||||
if f_vy >= 0 { World.set(e, pb, f_vy, vy) }
|
||||
if f_rx >= 0 {
|
||||
if hit_wall == 1 { World.set(e, pb, f_rx, 0) } else { World.set(e, pb, f_rx, remx) }
|
||||
}
|
||||
if f_ry >= 0 {
|
||||
if (hit_ceiling == 1) or (on_ground == 1) { World.set(e, pb, f_ry, 0) } else { World.set(e, pb, f_ry, remy) }
|
||||
}
|
||||
if f_grd >= 0 { World.set(e, pb, f_grd, on_ground) }
|
||||
if f_hw >= 0 { World.set(e, pb, f_hw, hit_wall) }
|
||||
if f_hc >= 0 { World.set(e, pb, f_hc, hit_ceiling) }
|
||||
if f_nx >= 0 { World.set(e, pb, f_nx, nx) }
|
||||
if f_ny >= 0 { World.set(e, pb, f_ny, ny) }
|
||||
}
|
||||
e = World.query_next(pb, e + 1)
|
||||
}
|
||||
|
||||
# 5. triggers/sensors — overlap that reports but never resolves. For each
|
||||
# trigger collider, find the lowest-id body-shaped collider it overlaps and
|
||||
# edge-detect enter/exit against last frame (SpriteAnim.event_fired shape).
|
||||
esys_triggers()
|
||||
}
|
||||
|
||||
function esys_triggers() -> void {
|
||||
if pm_col < 0 { return }
|
||||
if pm_c_trig < 0 { return }
|
||||
let f_hit = World.field_id(pm_col, "hit")
|
||||
let f_ent = World.field_id(pm_col, "entered")
|
||||
let f_ext = World.field_id(pm_col, "exited")
|
||||
if (f_hit < 0) and (f_ent < 0) and (f_ext < 0) { return } # no trigger outputs declared
|
||||
|
||||
if phys_trig_prev == null { phys_trig_prev = words(1024) }
|
||||
|
||||
var t = World.query_next(pm_col, 0)
|
||||
while t >= 0 {
|
||||
if phys_c_int(t, pm_c_trig) != 0 {
|
||||
if World.has(t, pm_pos) != 0 {
|
||||
let tx = phys_aabb_x(t); let ty = phys_aabb_y(t)
|
||||
let tw = phys_c_int(t, pm_c_w); let th = phys_c_int(t, pm_c_h)
|
||||
let tl = phys_c_int(t, pm_c_layer); let tm = phys_c_int(t, pm_c_mask)
|
||||
var cur = 0 - 1
|
||||
var b = World.query_next(pm_col, 0)
|
||||
while b >= 0 {
|
||||
if b != t {
|
||||
if phys_c_int(b, pm_c_trig) == 0 {
|
||||
if World.has(b, pm_pos) != 0 {
|
||||
if phys_match(tl, tm, phys_c_int(b, pm_c_layer), phys_c_int(b, pm_c_mask)) {
|
||||
let bx = phys_aabb_x(b); let by = phys_aabb_y(b)
|
||||
let bw = phys_c_int(b, pm_c_w); let bh = phys_c_int(b, pm_c_h)
|
||||
if (tx < bx + bw) and (bx < tx + tw) and (ty < by + bh) and (by < ty + th) {
|
||||
if cur < 0 { cur = b } # lowest id wins (deterministic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
b = World.query_next(pm_col, b + 1)
|
||||
}
|
||||
let prev = phys_trig_prev[t] - 1 # stored as id+1, 0 = none
|
||||
var entered = 0
|
||||
var exited = 0
|
||||
if (prev < 0) and (cur >= 0) { entered = 1 }
|
||||
if (prev >= 0) and (cur < 0) { exited = 1 }
|
||||
phys_trig_prev[t] = cur + 1
|
||||
if f_hit >= 0 { World.set(t, pm_col, f_hit, cur) }
|
||||
if f_ent >= 0 { World.set(t, pm_col, f_ent, entered) }
|
||||
if f_ext >= 0 { World.set(t, pm_col, f_ext, exited) }
|
||||
}
|
||||
}
|
||||
t = World.query_next(pm_col, t + 1)
|
||||
}
|
||||
}
|
||||
|
|
@ -776,6 +776,13 @@ function maybe_splice_runtime() -> void {
|
|||
if (find_comp("SpriteAnim") != null) or (find_comp("Motion") != null) {
|
||||
do_import("runtime/native/systems.ludic")
|
||||
}
|
||||
# Body -> the movement + collision system (#65). It reads the tilemap
|
||||
# (rt_tile / rt_mapw / rt_maph) for the tile-grid broadphase, so it links
|
||||
# against core.ludic; do_import dedupes when a game already pulled core in.
|
||||
if find_comp("Body") != null {
|
||||
do_import("runtime/native/core.ludic")
|
||||
do_import("runtime/native/systems_move.ludic")
|
||||
}
|
||||
# Light2D / Occluder -> the lighting render system, which links against the
|
||||
# 2D light pass (light.ludic). do_import dedupes, so this is a no-op when the
|
||||
# game also uses Light.* directly (g_uses_light already pulled it in).
|
||||
|
|
@ -824,6 +831,7 @@ function parse_program() -> void {
|
|||
g_esys_comp = new []pointer; g_esys_fn = new []pointer; g_esys_phase = new []pointer
|
||||
push(g_esys_comp, "SpriteAnim"); push(g_esys_fn, "esys_spriteanim"); push(g_esys_phase, "Update")
|
||||
push(g_esys_comp, "Motion"); push(g_esys_fn, "esys_motion"); push(g_esys_phase, "Update")
|
||||
push(g_esys_comp, "Body"); push(g_esys_fn, "esys_move"); push(g_esys_phase, "Update")
|
||||
push(g_esys_comp, "Light2D"); push(g_esys_fn, "esys_light2d"); push(g_esys_phase, "Render")
|
||||
g_namespaces = new []pointer
|
||||
loaded_paths = new []pointer
|
||||
|
|
|
|||
33623
selfhost/ludicc.seed.ll
33623
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -208,6 +208,8 @@ function cmd_test() -> int {
|
|||
feat_case("library/render", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18", "render.ludic (Screen pixel/oval/camera/clip/blend_mode/measure_text + Camera set/follow/shake, verified by pixel readback)")
|
||||
feat_case("library/lighting", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "lighting.ludic (Light ambient/point radial falloff + occluder hard shadows — 2D light accumulation, verified by pixel readback)")
|
||||
feat_case("library/light_tiers", "", "1 1 1 1 1 1 1 1 1", "light_tiers.ludic (Light spot/falloff/soft/gel/normal/time_of_day — render-quality tiers 3-4; issue #49)")
|
||||
feat_case("library/physics", "", "184 1 1 284 1 1 50 51 104 1 134 1 120 0 1 1 1 0 1 1 1", "physics.ludic (Body + Collider + esys_move: gravity/land, wall stop, sub-pixel, entity platform, one-way, triggers — entity broadphase; issue #65)")
|
||||
feat_case("library/physics_tiles", "", "112 1 80 1 48 1 60", "physics_tiles.ludic (esys_move tile-grid broadphase: tile floor/wall + one-way tile land-from-above / rise-through; issue #65)")
|
||||
spec_case("library/testing", "== 6 passed, 0 failed ==")
|
||||
spec_case("library/coverage", "== 3 passed, 0 failed ==")
|
||||
feat_case("library/errors", "", "5 10 0 7 1", "errors.ludic (assert guards an invariant, holds -> runs to the end; issue #8 success path)")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue