ludic/runtime/native/systems_bounds.ludic
Orkuncakilkaya bf36bc8a8f refactor(runtime,packages,examples): named constants, package enums, idiom sweep
- runtime: HEADLESS_FRAME_PATH, STICK_DEADZONE / STICK_LEFT_X/Y, key and
  byte codes as char literals throughout (`k == 'w'`, `fill(rt_map, ' ', …)`)
- ludic.gameplay/stats: drop the duplicate `stat_field` (it answered "atk"
  for every build stat); Stats.base uses stats_field_name
- ludic.shooter: compare aim modes and fire patterns with AimMode.* and
  WeaponPattern.* instead of raw ints; STICK_RIGHT_X/Y
- ludic.npcai: DecisionMade / brain_set_state use AiState.*
- examples/games/menu.ludic uses Font.load / Ui.* with FONT_PATH and
  BACKDROP named; strings.ludic header says what it prints
- whole tree: `x = x + 1` → `x += 1` (single-term right-hand sides only),
  `0 - x` → `-x`, ASCII codes → char literals; every .ludic and every
  ```ludic fence reformatted with the fixed formatter (whitespace only)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 01:12:26 +03:00

106 lines
3.9 KiB
Text

# ============================================================================
# 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, -World.get(e, PBody, f_vx)) }
if (hity == 1) and (f_vy >= 0) { World.set(e, PBody, f_vy, -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)
}
}