feat(ludic.effects): timed modifiers as a mechanic package; ludic.base's q_new is queue_new

ludic.effects: effects_add / effects_add_after / effects_clear / effects_run,
effects_sum / effects_has / effects_at / effects_live, an EffectEnded queue
(ran out, pushed out by the bonus cap, crowded out of a full ring) and a
System with its own save section. Nine test blocks in tests/effects_test.ludic.

ludic.base: q_new collided with render3d's quaternion q_new the moment a game
imported both, so the queue constructor is queue_new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 04:14:09 +03:00
parent 70d05bd504
commit d0ad364ad4
12 changed files with 419 additions and 9 deletions

View file

@ -35,7 +35,7 @@ import "ludic.base"
| --- | --- |
| `Tick { dt, frame, hours }`, `tick_new(dt, frame, hours)` | what a system's tick receives |
| `PH_INPUT`, `PH_SENSE`, `PH_SIMULATE`, `PH_RESOLVE`, `PH_COMMIT`, `PH_PRESENT`, `PH_COUNT` | the phases, in the order they run |
| `Queue<T>`, `q_new<T>(name)`, `q_push(q, v)`, `q_drain(q) -> []T`, `q_len(q)`, `q_clear(q)` | facts, first in first out |
| `Queue<T>`, `queue_new<T>(name)`, `q_push(q, v)`, `q_drain(q) -> []T`, `q_len(q)`, `q_clear(q)` | facts, first in first out (`queue_new`, not `q_new`: that name is render3d's quaternion) |
| `core_undrained() -> []string` | the names of queues still holding facts; ask it at the end of a frame |
| `Rng`, `rng_new(seed)`, `rng_seed(r, seed)`, `rng_next(r)`, `rng_float(r)`, `rng_between(r, lo, hi)`, `rng_span(r, lo, hi)` | a xorshift32 stream of its own (`rng_between` is inclusive; `rng_range` is taken by `Random.range`) |
| `SaveNode { found, version, data }`, `save_tree()`, `save_section(root, key, version, v)`, `load_section(root, key) -> SaveNode`, `save_encode`, `save_decode` | the save tree: `{"fishing": {"v": 3, ...}}`; `v` is reserved in a section, and a non-object value rides under `data` |
@ -66,7 +66,7 @@ export function fishing_cast(x: float, z: float) -> bool { # its
function fishing_reset() -> void {
casts = 0
dice = rng_new(7)
caught = q_new("fishing.caught")
caught = queue_new("fishing.caught")
}
function fishing_tick(t: Tick) -> void {
while casts > 0 {

View file

@ -16,7 +16,7 @@ function qb_init() -> void {
}
# a queue; the name is only for core_undrained
export function q_new<T>(name: string) -> Queue<T> {
export function queue_new<T>(name: string) -> Queue<T> {
qb_init()
let q = new Queue<T>
q.items = new []T

View file

@ -13,7 +13,7 @@ program QueueTest {
}
function fifo_case() -> void {
let q: Queue<int> = q_new("ints")
let q: Queue<int> = queue_new("ints")
q_push(q, 3)
q_push(q, 1)
q_push(q, 2)
@ -28,7 +28,7 @@ program QueueTest {
}
function record_case() -> void {
let q: Queue<Caught> = q_new("caught")
let q: Queue<Caught> = queue_new("caught")
q_push(q, caught(2, 1.5))
q_push(q, caught(4, 0.25))
let cs = q_drain(q)
@ -37,15 +37,15 @@ program QueueTest {
}
function clear_case() -> void {
let q: Queue<string> = q_new("words")
let q: Queue<string> = queue_new("words")
q_push(q, "a")
q_clear(q)
expect_eq(q_len(q), 0)
}
function undrained_case() -> void {
let a: Queue<int> = q_new("left_behind")
let b: Queue<int> = q_new("read")
let a: Queue<int> = queue_new("left_behind")
let b: Queue<int> = queue_new("read")
q_push(a, 1)
q_push(b, 2)
q_drain(b)

View file

@ -17,7 +17,7 @@ export function fishing_cast(x: float, z: float) -> bool { # its
function fishing_reset() -> void {
casts = 0
dice = rng_new(7)
caught = q_new("fishing.caught")
caught = queue_new("fishing.caught")
}
function fishing_tick(t: Tick) -> void {
while casts > 0 {