#79 — Input.axis_i(neg,pos) -> int returns a -1/0/1 movement intent from the multi-key device set, so WASD-to-movement needs no bool->int glue and feeds an int mover directly (dx = Input.axis_i('a','d')). #84 — a Bounds config entity (rect + policy, from ludic.core) drives the engine-owned world-bounds system (LateUpdate): clamp / wrap / bounce (clamp + flip Body velocity) / kill (despawn a body fully outside). Reads the Collider size so the box stays inside; off by default, spliced only when Bounds is declared (byte-identical otherwise). Adds World.despawn(e) — the by-id reflective despawn (runs @OnDespawn + frees) via a new world_despawn intrinsic whose @fn_world_despawn helper is emitted in emit_program's tail once a use is seen (the g_uses_* prelude pattern), used by the kill policy and callable from any system. Examples input_movement + world_bounds. Full suite 112/0, goldens byte-identical, fixpoint holds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0497029dae
commit
ad3be0c53c
17 changed files with 30435 additions and 29731 deletions
3
changes/input-axis-i.md
Normal file
3
changes/input-axis-i.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
Directional int input (#79) — `Input.axis_i(neg, pos) -> int` returns a -1/0/1 movement intent (`+1` positive key held, `-1` negative, `0` neither or both) read from the multi-key device set, so turning WASD into movement no longer needs the `ki(key_down('d')) - ki(key_down('a'))` bool-to-int glue and feeds an int mover directly: `dx = Input.axis_i('a', 'd')`, `dy = Input.axis_i('w', 's')`. Complements `Input.axis` (fixed) / `Input.vector` (normalized). Example: `examples/library/input_movement.ludic`.
|
||||||
3
changes/world-bounds.md
Normal file
3
changes/world-bounds.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
Optional world boundaries (#84) — declare a single `Bounds` config entity (a rect `x, y, w, h` plus a policy, shipped from **ludic.core**) and the engine-owned world-bounds system keeps every moving `Body` inside the play area each frame, so a game no longer hand-clamps `Position`. Four policies: `0` clamp (walls), `1` wrap (toroidal), `2` bounce (clamp + flip the Body velocity on the axis that hit), `3` kill (despawn a body fully outside). It reads each body's `Collider` size so the whole box stays inside; off by default (no `Bounds` entity = open world). Registered on the engine-system registry for LateUpdate (after movement integrates) and spliced only when a game declares `Bounds`, so a game that never does compiles byte-identically. Also adds `World.despawn(entity)` — the reflective, by-id form of the `despawn` statement (runs `@OnDespawn` + frees the slot), which the kill policy uses and any system can call. Example: `examples/library/world_bounds.ludic`.
|
||||||
24
docs/language/input/input-axis_i.md
Normal file
24
docs/language/input/input-axis_i.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
id: input-axis_i
|
||||||
|
name: Input.axis_i
|
||||||
|
category: input
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Input.axis_i
|
||||||
|
sig: Input.axis_i(neg, pos) -> int
|
||||||
|
tip: A -1/0/1 directional intent as a plain int, no bool->int glue.
|
||||||
|
order: 14
|
||||||
|
ns: Input
|
||||||
|
member: axis_i
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns a directional intent as a plain <code>int</code>: <code>+1</code> when the positive key is held, <code>-1</code> when the negative one is, <code>0</code> for neither or both. It reads the multi-key device set, so it replaces the <code>ki(key_down('d')) - ki(key_down('a'))</code> boolean-to-int boilerplate and feeds an int mover directly — <code>dx = Input.axis_i('a', 'd')</code>, <code>dy = Input.axis_i('w', 's')</code>. For an analog / normalized value use <a href="input-axis.html"><code>Input.axis</code></a> (fixed) or <a href="input-vector.html"><code>Input.vector</code></a>.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
entry {
|
||||||
|
Input.press('d')
|
||||||
|
Input.poll()
|
||||||
|
print(Input.axis_i('a', 'd')) # 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
28
docs/language/world/world-despawn.md
Normal file
28
docs/language/world/world-despawn.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
id: world-despawn
|
||||||
|
name: World.despawn
|
||||||
|
category: world
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: World.despawn
|
||||||
|
sig: World.despawn(entity)
|
||||||
|
tip: Despawn an entity by id — runs its @OnDespawn hooks and frees the slot.
|
||||||
|
order: 16
|
||||||
|
ns: World
|
||||||
|
member: despawn
|
||||||
|
---
|
||||||
|
|
||||||
|
Despawns an entity by id: it runs the entity's model <code>@OnDespawn</code> hook (with reason <code>Despawned</code>), sweeps any entity-scoped event listeners, and frees the slot back to the pool for reuse — the reflective, by-id form of the <code>despawn</code> statement. Use it from a system that has an entity id in hand (the engine world-bounds kill policy uses it, #84) or wherever you would write <code>despawn e</code> but only have the id.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
property Mob { hp: int = 0 }
|
||||||
|
model M { Mob }
|
||||||
|
entry {
|
||||||
|
spawn M { Mob { hp: 1 } }
|
||||||
|
let pm = World.prop_id("Mob")
|
||||||
|
let e = World.query_next(pm, 0)
|
||||||
|
World.despawn(e)
|
||||||
|
print(World.query_next(pm, 0)) # -1 — none left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
25
examples/library/input_movement.ludic
Normal file
25
examples/library/input_movement.ludic
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# input_movement.ludic — Input.axis_i (#79): turn WASD into a -1/0/1 movement
|
||||||
|
# intent as a plain int, with no bool->int glue (`ki(key_down('d')) - ...`). It
|
||||||
|
# reads the multi-key device set and feeds an int mover directly. Driven headless
|
||||||
|
# by injection, the same way the #50 device tests are.
|
||||||
|
#
|
||||||
|
# Deterministic; a full run prints: 1 0 -1 -1 0
|
||||||
|
program InputMovement {
|
||||||
|
entry {
|
||||||
|
Input.press('d') # hold right
|
||||||
|
Input.poll()
|
||||||
|
print(Input.axis_i('a', 'd')) # 1 — dx = right
|
||||||
|
print(Input.axis_i('w', 's')) # 0 — dy = none
|
||||||
|
|
||||||
|
Input.release('d')
|
||||||
|
Input.press('a') # hold left
|
||||||
|
Input.press('w') # and up
|
||||||
|
Input.poll()
|
||||||
|
print(Input.axis_i('a', 'd')) # -1 — dx = left
|
||||||
|
print(Input.axis_i('w', 's')) # -1 — dy = up
|
||||||
|
|
||||||
|
Input.press('d') # now both left AND right held
|
||||||
|
Input.poll()
|
||||||
|
print(Input.axis_i('a', 'd')) # 0 — opposing keys cancel
|
||||||
|
}
|
||||||
|
}
|
||||||
63
examples/library/world_bounds.ludic
Normal file
63
examples/library/world_bounds.ludic
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
# world_bounds.ludic — the engine world-bounds system (#84): declare one Bounds
|
||||||
|
# config entity (rect + policy) and the engine keeps every moving Body inside the
|
||||||
|
# play area each frame — clamp / wrap / bounce / kill — instead of the game
|
||||||
|
# hand-clamping Position. Bounds/Body/Collider come from ludic.core. Driven by
|
||||||
|
# tick_fixed (Update integrates, LateUpdate applies the bounds).
|
||||||
|
#
|
||||||
|
# Deterministic; a full run prints: 184 -16 184 1 1
|
||||||
|
program WorldBounds {
|
||||||
|
import "ludic.core/components.ludic"
|
||||||
|
|
||||||
|
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 }
|
||||||
|
|
||||||
|
function clear_all() -> void {
|
||||||
|
let pb = World.prop_id("Body")
|
||||||
|
var e = World.query_next(pb, 0)
|
||||||
|
while e >= 0 { despawn e; e = World.query_next(pb, 0) }
|
||||||
|
let pn = World.prop_id("Bounds")
|
||||||
|
var c = World.query_next(pn, 0)
|
||||||
|
while c >= 0 { despawn c; c = World.query_next(pn, 0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
entry {
|
||||||
|
let PP = World.prop_id("Position")
|
||||||
|
let fx = World.field_id(PP, "x")
|
||||||
|
let PBody = World.prop_id("Body")
|
||||||
|
let f_vx = World.field_id(PBody, "vx")
|
||||||
|
|
||||||
|
# A. clamp (policy 0): a body running right is stopped at the right wall.
|
||||||
|
spawn B0 { Bounds { x: 0, y: 0, w: 200, h: 200, policy: 0 } }
|
||||||
|
spawn M0 { Position { x: 180, y: 50 }, Body { vx: fixed(10), policy: 1 }, Collider { w: 16, h: 16 } }
|
||||||
|
var a = World.query_next(PBody, 0)
|
||||||
|
tick_n(10)
|
||||||
|
print(World.get(a, PP, fx)) # 184 = 200 - 16 (box right edge at the wall)
|
||||||
|
clear_all()
|
||||||
|
|
||||||
|
# B. wrap (policy 1): past the right edge, it reappears on the left.
|
||||||
|
spawn B1 { Bounds { x: 0, y: 0, w: 200, h: 200, policy: 1 } }
|
||||||
|
spawn M1 { Position { x: 195, y: 50 }, Body { vx: fixed(10), policy: 1 }, Collider { w: 16, h: 16 } }
|
||||||
|
a = World.query_next(PBody, 0)
|
||||||
|
tick_n(1)
|
||||||
|
print(World.get(a, PP, fx)) # -16 = bx - w (wrapped to the far side)
|
||||||
|
clear_all()
|
||||||
|
|
||||||
|
# C. bounce (policy 2): clamp at the wall AND flip the velocity.
|
||||||
|
spawn B2 { Bounds { x: 0, y: 0, w: 200, h: 200, policy: 2 } }
|
||||||
|
spawn M2 { Position { x: 190, y: 50 }, Body { vx: fixed(10), policy: 1 }, Collider { w: 16, h: 16 } }
|
||||||
|
a = World.query_next(PBody, 0)
|
||||||
|
tick_n(1)
|
||||||
|
print(World.get(a, PP, fx)) # 184 (clamped at the wall)
|
||||||
|
print(bi(World.get(a, PBody, f_vx) < 0)) # 1 (vx flipped negative)
|
||||||
|
clear_all()
|
||||||
|
|
||||||
|
# D. kill (policy 3): a body fully outside is despawned.
|
||||||
|
spawn B3 { Bounds { x: 0, y: 0, w: 200, h: 200, policy: 3 } }
|
||||||
|
spawn M3 { Position { x: 300, y: 50 }, Body { vx: fixed(10), policy: 1 }, Collider { w: 16, h: 16 } }
|
||||||
|
tick_n(1)
|
||||||
|
var live = 0
|
||||||
|
var q = World.query_next(PBody, 0)
|
||||||
|
while q >= 0 { live = live + 1; q = World.query_next(PBody, q + 1) }
|
||||||
|
print(live) # 0 — the out-of-bounds body was killed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -54,3 +54,9 @@ property Sprite { id: int = 0, offx: int = 0, offy: int = 0, scale: int = 1, fli
|
||||||
# read from the Map.* tilemap. tile > 0 sets the tile size in px; wall is the solid
|
# read from the Map.* tilemap. tile > 0 sets the tile size in px; wall is the solid
|
||||||
# glyph; oneway is an optional one-way-platform glyph (0 = none).
|
# glyph; oneway is an optional one-way-platform glyph (0 = none).
|
||||||
property Solids { tile: int = 0, wall: int = 0, oneway: int = 0 }
|
property Solids { tile: int = 0, wall: int = 0, oneway: int = 0 }
|
||||||
|
|
||||||
|
# Optional single config entity defining a world boundary / play area (#84): a rect
|
||||||
|
# (x, y, w, h) and a policy the engine applies to every moving Body each frame —
|
||||||
|
# 0 clamp (walls), 1 wrap (toroidal), 2 bounce (flip velocity), 3 kill (despawn when
|
||||||
|
# fully outside). Off by default (no Bounds entity = open world).
|
||||||
|
property Bounds { x: int = 0, y: int = 0, w: int = 0, h: int = 0, policy: int = 0 }
|
||||||
|
|
|
||||||
|
|
@ -21,3 +21,4 @@ provides "Body"
|
||||||
provides "Collider"
|
provides "Collider"
|
||||||
provides "Solids"
|
provides "Solids"
|
||||||
provides "Sprite"
|
provides "Sprite"
|
||||||
|
provides "Bounds"
|
||||||
|
|
|
||||||
|
|
@ -370,6 +370,17 @@ function input_axis(neg: int, pos: int) -> fixed {
|
||||||
if in_bit_get(in_held, neg) { v = v - fixed(1) }
|
if in_bit_get(in_held, neg) { v = v - fixed(1) }
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
# #79 — a directional intent as a plain int: +1 if the positive key is held, -1 if
|
||||||
|
# the negative, 0 if neither or both. Reads the multi-key device set, so it needs
|
||||||
|
# no bool->int glue (the `dx = ki(key_down('d')) - ki(key_down('a'))` boilerplate)
|
||||||
|
# and feeds an int mover (TopDown.move) straight: dx = Input.axis_i('a','d').
|
||||||
|
function input_axis_i(neg: int, pos: int) -> int {
|
||||||
|
in_init()
|
||||||
|
var v = 0
|
||||||
|
if in_bit_get(in_held, pos) { v = v + 1 }
|
||||||
|
if in_bit_get(in_held, neg) { v = v - 1 }
|
||||||
|
return v
|
||||||
|
}
|
||||||
# A 2D vector from four direction keys, normalized so a diagonal is not faster.
|
# A 2D vector from four direction keys, normalized so a diagonal is not faster.
|
||||||
function input_vector(left: int, right: int, up: int, down: int) -> Vector {
|
function input_vector(left: int, right: int, up: int, down: int) -> Vector {
|
||||||
in_init()
|
in_init()
|
||||||
|
|
|
||||||
106
runtime/native/systems_bounds.ludic
Normal file
106
runtime/native/systems_bounds.ludic
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
# ============================================================================
|
||||||
|
# systems_bounds.ludic — the engine-owned world-bounds system (#84).
|
||||||
|
#
|
||||||
|
# Games used to hand-roll clamping (clamp Position every frame) to fake walls or
|
||||||
|
# keep entities inside a play area. Declare a single `Bounds` config entity — a
|
||||||
|
# rect (x, y, w, h) plus a policy — and the engine keeps every moving `Body`
|
||||||
|
# inside it each frame, applying the policy at the edge:
|
||||||
|
#
|
||||||
|
# policy 0 clamp — stop at the edge (walls)
|
||||||
|
# policy 1 wrap — reappear on the opposite side (asteroids / toroidal world)
|
||||||
|
# policy 2 bounce — clamp and flip the Body velocity on the axis that hit
|
||||||
|
# policy 3 kill — despawn an entity fully outside the bounds (offscreen cleanup)
|
||||||
|
#
|
||||||
|
# Off by default (no Bounds entity = open world). Registered on the engine-system
|
||||||
|
# registry for LateUpdate, so it runs after the movement system has integrated the
|
||||||
|
# frame; spliced only when a game declares `Bounds`, so a game that never does
|
||||||
|
# compiles byte-identically. Everything integer + deterministic.
|
||||||
|
#
|
||||||
|
# Component contract (ship it from ludic.core):
|
||||||
|
# property Bounds { x: int, y: int, w: int, h: int, policy: int }
|
||||||
|
#
|
||||||
|
# It reads each Body entity's Position and (if present) its Collider size so the
|
||||||
|
# whole box stays inside; a Body with no Collider is treated as a point.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
const BND_CLAMP: int = 0
|
||||||
|
const BND_WRAP: int = 1
|
||||||
|
const BND_BOUNCE: int = 2
|
||||||
|
const BND_KILL: int = 3
|
||||||
|
|
||||||
|
# read an int field by name off a component, or 0 if absent.
|
||||||
|
function bnd_get(e: int, p: int, name: pointer) -> int {
|
||||||
|
let f = World.field_id(p, name)
|
||||||
|
if f < 0 { return 0 }
|
||||||
|
return World.get(e, p, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
function esys_bounds() -> void {
|
||||||
|
let PB = World.prop_id("Bounds")
|
||||||
|
if PB < 0 { return }
|
||||||
|
let cfg = World.query_next(PB, 0) # the single bounds config entity
|
||||||
|
if cfg < 0 { return }
|
||||||
|
let bx = bnd_get(cfg, PB, "x")
|
||||||
|
let by = bnd_get(cfg, PB, "y")
|
||||||
|
let bw = bnd_get(cfg, PB, "w")
|
||||||
|
let bh = bnd_get(cfg, PB, "h")
|
||||||
|
let pol = bnd_get(cfg, PB, "policy")
|
||||||
|
if bw <= 0 { return }
|
||||||
|
if bh <= 0 { return }
|
||||||
|
let x1 = bx + bw # right / bottom edges (exclusive of box size)
|
||||||
|
let y1 = by + bh
|
||||||
|
|
||||||
|
let PP = World.prop_id("Position")
|
||||||
|
if PP < 0 { return }
|
||||||
|
let fx = World.field_id(PP, "x")
|
||||||
|
let fy = World.field_id(PP, "y")
|
||||||
|
let PBody = World.prop_id("Body")
|
||||||
|
if PBody < 0 { return }
|
||||||
|
let f_vx = World.field_id(PBody, "vx")
|
||||||
|
let f_vy = World.field_id(PBody, "vy")
|
||||||
|
let PC = World.prop_id("Collider")
|
||||||
|
|
||||||
|
var e = World.query_next(PBody, 0)
|
||||||
|
while e >= 0 {
|
||||||
|
if World.has(e, PP) != 0 {
|
||||||
|
var x = World.get(e, PP, fx)
|
||||||
|
var y = World.get(e, PP, fy)
|
||||||
|
var w = 0
|
||||||
|
var h = 0
|
||||||
|
if PC >= 0 {
|
||||||
|
if World.has(e, PC) != 0 { w = bnd_get(e, PC, "w"); h = bnd_get(e, PC, "h") }
|
||||||
|
}
|
||||||
|
|
||||||
|
var killed = 0
|
||||||
|
if pol == BND_KILL {
|
||||||
|
if (x + w < bx) or (x > x1) or (y + h < by) or (y > y1) {
|
||||||
|
world_despawn(e) # fully outside -> remove it
|
||||||
|
killed = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if killed == 0 {
|
||||||
|
if pol == BND_WRAP {
|
||||||
|
if x + w < bx { x = x1 }
|
||||||
|
if x > x1 { x = bx - w }
|
||||||
|
if y + h < by { y = y1 }
|
||||||
|
if y > y1 { y = by - h }
|
||||||
|
} else {
|
||||||
|
# clamp (also the position half of bounce): keep [x, x+w] inside [bx, x1]
|
||||||
|
var hitx = 0
|
||||||
|
var hity = 0
|
||||||
|
if x < bx { x = bx; hitx = 1 }
|
||||||
|
if x + w > x1 { x = x1 - w; hitx = 1 }
|
||||||
|
if y < by { y = by; hity = 1 }
|
||||||
|
if y + h > y1 { y = y1 - h; hity = 1 }
|
||||||
|
if pol == BND_BOUNCE {
|
||||||
|
if (hitx == 1) and (f_vx >= 0) { World.set(e, PBody, f_vx, 0 - World.get(e, PBody, f_vx)) }
|
||||||
|
if (hity == 1) and (f_vy >= 0) { World.set(e, PBody, f_vy, 0 - World.get(e, PBody, f_vy)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
World.set(e, PP, fx, x)
|
||||||
|
World.set(e, PP, fy, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e = World.query_next(PBody, e + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -224,6 +224,7 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
||||||
if (meth == "press") { bare = "input_press"; push(labels, "key") }
|
if (meth == "press") { bare = "input_press"; push(labels, "key") }
|
||||||
if (meth == "release") { bare = "input_release"; push(labels, "key") }
|
if (meth == "release") { bare = "input_release"; push(labels, "key") }
|
||||||
if (meth == "axis") { bare = "input_axis"; push(labels, "neg"); push(labels, "pos") }
|
if (meth == "axis") { bare = "input_axis"; push(labels, "neg"); push(labels, "pos") }
|
||||||
|
if (meth == "axis_i") { bare = "input_axis_i"; push(labels, "neg"); push(labels, "pos") } # #79 directional int (-1/0/1)
|
||||||
if (meth == "vector") { bare = "input_vector"; push(labels, "left"); push(labels, "right"); push(labels, "up"); push(labels, "down") }
|
if (meth == "vector") { bare = "input_vector"; push(labels, "left"); push(labels, "right"); push(labels, "up"); push(labels, "down") }
|
||||||
if (meth == "strength") { bare = "input_strength"; push(labels, "action") }
|
if (meth == "strength") { bare = "input_strength"; push(labels, "action") }
|
||||||
if (meth == "mouse_x") { bare = "input_mouse_x" }
|
if (meth == "mouse_x") { bare = "input_mouse_x" }
|
||||||
|
|
@ -287,6 +288,7 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
||||||
if (meth == "count") { bare = "world_count" }
|
if (meth == "count") { bare = "world_count" }
|
||||||
if (meth == "size") { bare = "world_size" }
|
if (meth == "size") { bare = "world_size" }
|
||||||
if (meth == "spawn") { bare = "world_spawn" }
|
if (meth == "spawn") { bare = "world_spawn" }
|
||||||
|
if (meth == "despawn") { bare = "world_despawn" } # #84 — despawn an entity by id (runs @OnDespawn + frees)
|
||||||
if (meth == "save") { bare = "world_save" }
|
if (meth == "save") { bare = "world_save" }
|
||||||
if (meth == "load") { bare = "world_load" }
|
if (meth == "load") { bare = "world_load" }
|
||||||
if (meth == "prop_id") { bare = "world_prop_id" }
|
if (meth == "prop_id") { bare = "world_prop_id" }
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,7 @@ function emit_program() -> void {
|
||||||
g_uses_panic = false
|
g_uses_panic = false
|
||||||
g_uses_result = false
|
g_uses_result = false
|
||||||
g_uses_option = false
|
g_uses_option = false
|
||||||
|
g_uses_world_despawn = false # #84: set when a world_despawn call is emitted (below)
|
||||||
g_cov_lines = new []int
|
g_cov_lines = new []int
|
||||||
g_cov_active = true
|
g_cov_active = true
|
||||||
loc_name = new []pointer; loc_reg = new []pointer; loc_ty = new []pointer; loc_mut = new []int
|
loc_name = new []pointer; loc_reg = new []pointer; loc_ty = new []pointer; loc_mut = new []int
|
||||||
|
|
@ -184,6 +185,7 @@ function emit_program() -> void {
|
||||||
i = 0
|
i = 0
|
||||||
while i < len(prog) { if prog[i].kind == N_MAIN { emit_main(prog[i]) }; i = i + 1 }
|
while i < len(prog) { if prog[i].kind == N_MAIN { emit_main(prog[i]) }; i = i + 1 }
|
||||||
} } }
|
} } }
|
||||||
|
if g_uses_world_despawn { emit_world_despawn_fn() } # #84: @fn_world_despawn, after all World.despawn / esys_bounds-kill uses are seen
|
||||||
if g_uses_loopback { emit_loopback() } # built-in transport, after all net_send/net_poll uses are seen
|
if g_uses_loopback { emit_loopback() } # built-in transport, after all net_send/net_poll uses are seen
|
||||||
if g_uses_str { emit_str_prelude() } # @fn_str_eq / @fn_str_concat, after all uses are seen
|
if g_uses_str { emit_str_prelude() } # @fn_str_eq / @fn_str_concat, after all uses are seen
|
||||||
if g_uses_intstr { emit_int_str() } # @fn_int_str, for string(int) in interpolation
|
if g_uses_intstr { emit_int_str() } # @fn_int_str, for string(int) in interpolation
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ function is_intrinsic(name: pointer) -> bool {
|
||||||
if (name == "arg_count") or (name == "arg") or (name == "exit") { return true }
|
if (name == "arg_count") or (name == "arg") or (name == "exit") { return true }
|
||||||
if (name == "file_stderr") or (name == "file_stdout") { return true }
|
if (name == "file_stderr") or (name == "file_stdout") { return true }
|
||||||
if (name == "run") or (name == "getenv") { return true }
|
if (name == "run") or (name == "getenv") { return true }
|
||||||
|
if (name == "world_despawn") { return true } # #84 — despawn an entity by id from a system
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
# emit " <r> = <rest>\n" and return r
|
# emit " <r> = <rest>\n" and return r
|
||||||
|
|
@ -56,6 +57,15 @@ function emit_intrinsic(name: pointer, e: Node) -> Val {
|
||||||
emit(" call i32 @fclose(ptr "); emit(f); emit(")\n")
|
emit(" call i32 @fclose(ptr "); emit(f); emit(")\n")
|
||||||
return val("0", "void")
|
return val("0", "void")
|
||||||
}
|
}
|
||||||
|
# #84 — despawn an entity by id (runs its @OnDespawn hooks + frees the slot),
|
||||||
|
# callable from a system (esys_bounds kill) or as World.despawn(e). The helper
|
||||||
|
# @fn_world_despawn is emitted with the ECS defs when g_uses_world_despawn.
|
||||||
|
if (name == "world_despawn") {
|
||||||
|
g_uses_world_despawn = true # emit @fn_world_despawn in the tail (after all uses seen)
|
||||||
|
let ent = arg_code(e, 0)
|
||||||
|
emit(" call void @fn_world_despawn(i32 "); emit(ent); emit(")\n")
|
||||||
|
return val("0", "void")
|
||||||
|
}
|
||||||
# print_str / print_int are the polymorphic `print(x)` builtin now (emit_call).
|
# print_str / print_int are the polymorphic `print(x)` builtin now (emit_call).
|
||||||
if (name == "arg_count") { return val(emit_bind("load i32, ptr @L_argc"), "int") }
|
if (name == "arg_count") { return val(emit_bind("load i32, ptr @L_argc"), "int") }
|
||||||
if (name == "arg") {
|
if (name == "arg") {
|
||||||
|
|
|
||||||
|
|
@ -293,6 +293,37 @@ function emit_despawn_hooks() -> void {
|
||||||
emit_despawn_all_fn()
|
emit_despawn_all_fn()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# #84 — @fn_world_despawn(e): the runtime-callable despawn, used by the
|
||||||
|
# world-bounds kill policy and World.despawn(e). Same per-kind @OnDespawn dispatch
|
||||||
|
# as the `despawn` statement (emit_despawn) but reading a runtime %e, then the
|
||||||
|
# entity-scoped-listener sweep and the slot free. Emitted from emit_program's tail
|
||||||
|
# only when a world_despawn call was emitted (g_uses_world_despawn), after all uses
|
||||||
|
# are seen — so a program that never despawns by id is byte-identical.
|
||||||
|
function emit_world_despawn_fn() -> void {
|
||||||
|
emit("define void @fn_world_despawn(i32 %e) {\nentry:\n")
|
||||||
|
if len(g_ondespawn) > 0 {
|
||||||
|
let me = itoa(MAX_ENT)
|
||||||
|
emit(" %kp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\n")
|
||||||
|
emit(" %k = load i32, ptr %kp\n")
|
||||||
|
var i = 0
|
||||||
|
while i < len(g_ondespawn) {
|
||||||
|
let mname = g_ondespawn[i].s
|
||||||
|
let si = itoa(i)
|
||||||
|
emit(" %c"); emit(si); emit(" = icmp eq i32 %k, "); emit(itoa(find_arch_id(mname))); emit("\n")
|
||||||
|
emit(" br i1 %c"); emit(si); emit(", label %hit"); emit(si); emit(", label %next"); emit(si); emit("\n")
|
||||||
|
emit("hit"); emit(si); emit(":\n call void @on_despawn_"); emit(mname); emit("(i32 %e, i32 0)\n") # reason = Despawned
|
||||||
|
let dev = `model_{mname}_despawn`
|
||||||
|
if (find_event(dev) != null) { emit(" call void @ev_"); emit(dev); emit("(i32 %e, i32 0)\n") }
|
||||||
|
emit(" br label %next"); emit(si); emit("\n")
|
||||||
|
emit("next"); emit(si); emit(":\n")
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(g_events) > 0 { emit(" call void @ludic_sweep_entity(i32 %e)\n") } # EV5: drop entity-scoped listeners
|
||||||
|
emit(" call void @L_free_entity(i32 %e)\n")
|
||||||
|
emit(" ret void\n}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
# LC1 "no silent deaths": at program shutdown every still-live entity's despawn
|
# LC1 "no silent deaths": at program shutdown every still-live entity's despawn
|
||||||
# hook fires with reason Quit, so teardown that must run on exit is not skipped.
|
# hook fires with reason Quit, so teardown that must run on exit is not skipped.
|
||||||
# @L_despawn_all(reason) walks the live set and dispatches each entity by kind —
|
# @L_despawn_all(reason) walks the live set and dispatches each entity by kind —
|
||||||
|
|
|
||||||
|
|
@ -192,7 +192,7 @@ function p_postfix() -> Node {
|
||||||
if e.a.kind == E_ID and e.a.s == "Input" and (e.s == "bind" or e.s == "rebind" or e.s == "poll" or e.s == "down" or e.s == "pressed" or e.s == "record" or e.s == "replay" or e.s == "action" or e.s == "bind_pad" or e.s == "active" or e.s == "just_pressed" or e.s == "just_released") { g_uses_input = true }
|
if e.a.kind == E_ID and e.a.s == "Input" and (e.s == "bind" or e.s == "rebind" or e.s == "poll" or e.s == "down" or e.s == "pressed" or e.s == "record" or e.s == "replay" or e.s == "action" or e.s == "bind_pad" or e.s == "active" or e.s == "just_pressed" or e.s == "just_released") { g_uses_input = true }
|
||||||
# #50 device layer — any of the multi-key / analog / mouse / gamepad / touch
|
# #50 device layer — any of the multi-key / analog / mouse / gamepad / touch
|
||||||
# methods also splices input.ludic (Input.key stays bare, no runtime).
|
# methods also splices input.ludic (Input.key stays bare, no runtime).
|
||||||
if e.a.kind == E_ID and e.a.s == "Input" and (e.s == "key_down" or e.s == "key_pressed" or e.s == "key_released" or e.s == "press" or e.s == "release" or e.s == "axis" or e.s == "vector" or e.s == "strength" or e.s == "mouse_x" or e.s == "mouse_y" or e.s == "mouse_dx" or e.s == "mouse_dy" or e.s == "mouse_down" or e.s == "wheel" or e.s == "set_mouse" or e.s == "pad_connected" or e.s == "pad_button" or e.s == "pad_axis" or e.s == "set_pad" or e.s == "touch_count" or e.s == "touch_x" or e.s == "touch_y" or e.s == "set_touch") { g_uses_input = true }
|
if e.a.kind == E_ID and e.a.s == "Input" and (e.s == "key_down" or e.s == "key_pressed" or e.s == "key_released" or e.s == "press" or e.s == "release" or e.s == "axis" or e.s == "axis_i" or e.s == "vector" or e.s == "strength" or e.s == "mouse_x" or e.s == "mouse_y" or e.s == "mouse_dx" or e.s == "mouse_dy" or e.s == "mouse_down" or e.s == "wheel" or e.s == "set_mouse" or e.s == "pad_connected" or e.s == "pad_button" or e.s == "pad_axis" or e.s == "set_pad" or e.s == "touch_count" or e.s == "touch_x" or e.s == "touch_y" or e.s == "set_touch") { g_uses_input = true }
|
||||||
# Anim.play/clip/on_frame/fired + Motion.to (#48): the ergonomic writes over
|
# Anim.play/clip/on_frame/fired + Motion.to (#48): the ergonomic writes over
|
||||||
# the SpriteAnim/Motion components live in systems.ludic and use the world
|
# the SpriteAnim/Motion components live in systems.ludic and use the world
|
||||||
# table, so splice it and force the reflection ABI even if the game leaves
|
# table, so splice it and force the reflection ABI even if the game leaves
|
||||||
|
|
@ -448,6 +448,7 @@ var g_uses_input: bool = false # a program used Input.bind/down/poll/… (act
|
||||||
var g_has_clear_color: bool = false # @ClearColor(N) declared -> the Render phase auto-clears + auto-presents (#86)
|
var g_has_clear_color: bool = false # @ClearColor(N) declared -> the Render phase auto-clears + auto-presents (#86)
|
||||||
var g_clear_color: int = 0 # the declared clear colour (0x00RRGGBB)
|
var g_clear_color: int = 0 # the declared clear colour (0x00RRGGBB)
|
||||||
var g_warned_draw_sprite: bool = false # emit the bare-draw_sprite deprecation note once (#85)
|
var g_warned_draw_sprite: bool = false # emit the bare-draw_sprite deprecation note once (#85)
|
||||||
|
var g_uses_world_despawn: bool = false # World.despawn / esys_bounds kill -> emit @fn_world_despawn (#84)
|
||||||
|
|
||||||
# #85 — steer a direct bare draw_sprite / draw_sprite_scaled call to the namespaced
|
# #85 — steer a direct bare draw_sprite / draw_sprite_scaled call to the namespaced
|
||||||
# Screen.sprite (or the engine sprite-render system). Warns once, non-fatally — the
|
# Screen.sprite (or the engine sprite-render system). Warns once, non-fatally — the
|
||||||
|
|
@ -864,6 +865,12 @@ function maybe_splice_runtime() -> void {
|
||||||
do_import("runtime/native/core.ludic")
|
do_import("runtime/native/core.ludic")
|
||||||
do_import("runtime/native/systems_sprite.ludic")
|
do_import("runtime/native/systems_sprite.ludic")
|
||||||
}
|
}
|
||||||
|
# Bounds -> the engine world-bounds system (#84). Self-contained (reflection
|
||||||
|
# ABI only); its kill policy calls world_despawn, so force the @fn_world_despawn
|
||||||
|
# helper to be emitted.
|
||||||
|
if find_comp("Bounds") != null {
|
||||||
|
do_import("runtime/native/systems_bounds.ludic")
|
||||||
|
}
|
||||||
# Light2D / Occluder -> the lighting render system, which links against the
|
# 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
|
# 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).
|
# game also uses Light.* directly (g_uses_light already pulled it in).
|
||||||
|
|
@ -903,6 +910,7 @@ function parse_program() -> void {
|
||||||
g_has_clear_color = false
|
g_has_clear_color = false
|
||||||
g_clear_color = 0
|
g_clear_color = 0
|
||||||
g_warned_draw_sprite = false
|
g_warned_draw_sprite = false
|
||||||
|
g_uses_world_despawn = false
|
||||||
g_uses_light = false
|
g_uses_light = false
|
||||||
g_uses_value = false
|
g_uses_value = false
|
||||||
g_uses_xml = false
|
g_uses_xml = false
|
||||||
|
|
@ -920,6 +928,7 @@ function parse_program() -> void {
|
||||||
push(g_esys_comp, "SpriteAnim"); push(g_esys_fn, "esys_spriteanim"); push(g_esys_phase, "Update")
|
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, "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, "Body"); push(g_esys_fn, "esys_move"); push(g_esys_phase, "Update")
|
||||||
|
push(g_esys_comp, "Bounds"); push(g_esys_fn, "esys_bounds"); push(g_esys_phase, "LateUpdate")
|
||||||
push(g_esys_comp, "Sprite"); push(g_esys_fn, "esys_sprite"); push(g_esys_phase, "Render")
|
push(g_esys_comp, "Sprite"); push(g_esys_fn, "esys_sprite"); push(g_esys_phase, "Render")
|
||||||
push(g_esys_comp, "Light2D"); push(g_esys_fn, "esys_light2d"); push(g_esys_phase, "Render")
|
push(g_esys_comp, "Light2D"); push(g_esys_fn, "esys_light2d"); push(g_esys_phase, "Render")
|
||||||
g_namespaces = new []pointer
|
g_namespaces = new []pointer
|
||||||
|
|
|
||||||
59838
selfhost/ludicc.seed.ll
59838
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -244,6 +244,7 @@ function cmd_test() -> int {
|
||||||
# builtin gameplay controllers, consumed as in-repo packages (issues #57-#61)
|
# builtin gameplay controllers, consumed as in-repo packages (issues #57-#61)
|
||||||
controller_case("library/core_components", "", "110 30 184 1", "core_components.ludic (ludic.core #77: canonical Position/Body/Collider imported from a package, driving esys_move, composed with a game Health component)")
|
controller_case("library/core_components", "", "110 30 184 1", "core_components.ludic (ludic.core #77: canonical Position/Body/Collider imported from a package, driving esys_move, composed with a game Health component)")
|
||||||
controller_case("library/sprite_render", "q", "1 0", "sprite_render.ludic (ludic.core #85: engine sprite-render system auto-draws a Sprite entity from its Position; respects hidden)")
|
controller_case("library/sprite_render", "q", "1 0", "sprite_render.ludic (ludic.core #85: engine sprite-render system auto-draws a Sprite entity from its Position; respects hidden)")
|
||||||
|
controller_case("library/world_bounds", "", "184 -16 184 1 0", "world_bounds.ludic (ludic.core #84: engine world-bounds system — clamp/wrap/bounce/kill keep a Body in the play area)")
|
||||||
controller_case("library/gameplay_foundation", "", "2 0 1 5 24 0 2 1 15 85 7 1 1", "gameplay_foundation.ludic (ludic.gameplay #57: Cooldown timer + Stats/modifier stack + Faction table + Combat cancel/mutable pipeline)")
|
controller_case("library/gameplay_foundation", "", "2 0 1 5 24 0 2 1 15 85 7 1 1", "gameplay_foundation.ludic (ludic.gameplay #57: Cooldown timer + Stats/modifier stack + Faction table + Combat cancel/mutable pipeline)")
|
||||||
controller_case("games/platformer_demo", "", "1 1 1 1 1 1 1 1", "platformer_demo.ludic (ludic.platformer #58: gravity/land, jump apex, coyote, veto-gated double jump, wall collision + disable-system lever)")
|
controller_case("games/platformer_demo", "", "1 1 1 1 1 1 1 1", "platformer_demo.ludic (ludic.platformer #58: gravity/land, jump apex, coyote, veto-gated double jump, wall collision + disable-system lever)")
|
||||||
controller_case("games/platformer_scaffolding", "", "1 1 1 1 1 1", "platformer_scaffolding.ludic (ludic.platformer #58 layers 4-5: moving-platform rider carry, pickup->score, spring, hazard+Life i-frames)")
|
controller_case("games/platformer_scaffolding", "", "1 1 1 1 1 1", "platformer_scaffolding.ludic (ludic.platformer #58 layers 4-5: moving-platform rider carry, pickup->score, spring, hazard+Life i-frames)")
|
||||||
|
|
@ -306,6 +307,7 @@ function cmd_test() -> int {
|
||||||
feat_case("library/input_manager", "", "1 1 1 0 1 0 0 1", "input_manager.ludic (#83 Input Manager: default bindings + device-agnostic actions across keyboard/pad + on-press/on-release edges)")
|
feat_case("library/input_manager", "", "1 1 1 0 1 0 0 1", "input_manager.ludic (#83 Input Manager: default bindings + device-agnostic actions across keyboard/pad + on-press/on-release edges)")
|
||||||
feat_case("library/input_auto", "llq", "2", "input_auto.ludic (#83 the frame loop auto-commits the device layer: Input.active fires with no manual Input.poll)")
|
feat_case("library/input_auto", "llq", "2", "input_auto.ludic (#83 the frame loop auto-commits the device layer: Input.active fires with no manual Input.poll)")
|
||||||
feat_case("library/input_edge", "xkkxq", "0 11 1 100 0", "input_edge.ludic (#87 Input.key_pressed/key_released edges fire on the transition frame; no double-commit under the frame loop)")
|
feat_case("library/input_edge", "xkkxq", "0 11 1 100 0", "input_edge.ludic (#87 Input.key_pressed/key_released edges fire on the transition frame; no double-commit under the frame loop)")
|
||||||
|
feat_case("library/input_movement", "", "1 0 -1 -1 0", "input_movement.ludic (#79 Input.axis_i turns WASD into a -1/0/1 movement intent, no bool->int glue)")
|
||||||
feat_case("library/audio", "", "0 0 0 0 1", "audio.ludic (#22 Audio.* load/play/music/volume/pitch/stop/is_playing — headless no-op)")
|
feat_case("library/audio", "", "0 0 0 0 1", "audio.ludic (#22 Audio.* load/play/music/volume/pitch/stop/is_playing — headless no-op)")
|
||||||
# #6 Http.* — the client links Foundation (macOS-only), so build it through the
|
# #6 Http.* — the client links Foundation (macOS-only), so build it through the
|
||||||
# canonical `ludicc -o` path (which wires the framework) and gate on Darwin. The
|
# canonical `ludicc -o` path (which wires the framework) and gate on Darwin. The
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue