diff --git a/packages/ludic.needs/README.md b/packages/ludic.needs/README.md new file mode 100644 index 00000000..df5aead8 --- /dev/null +++ b/packages/ludic.needs/README.md @@ -0,0 +1,55 @@ +# ludic.needs + +A body's four needs - warmth, food, water and energy, each a bar of 0..100 - what an hour of +doing something costs them, and the countdown to a collapse when one runs out. Uses +[`ludic.base`](../ludic.base/README.md) and nothing else. + +```ludic +import "ludic.needs" +``` + +The package owns the rates (a full bar is a day at a real trip's median, about fourteen hours at +the hardest sustained work, two days at rest) and the countdown. It never decides what a collapse +COSTS: a need at zero is `NEEDS_CRITICAL`, ten seconds later it is `NEEDS_COLLAPSED`, and the game +walks the player out, takes their money, or puts them down for a teammate. Say it happened with +`needs_went_down()`, which counts it. + +## The port + +```ludic +export port NeedsWorld { # every member has a default, so any may be left out + activity: fn() -> int # NEEDS_STILL, _WALK, _RUN, _SIT, _SWIM, _SWIM_HARD, _FLOAT, _DIVE + resting: fn() -> float # energy an hour sitting gives back (unbound: 40) + warmth: fn() -> float # warmth an hour from the surroundings: sun, night, weather, a fire, the water (unbound: -2) + keep: fn() -> float # the fraction of warmth being lost that clothing keeps (unbound: 0) + hot: fn() -> bool # the midday sun: thirst half again faster + effort: fn() -> float # walking and running energy, times (the boots) + drain: fn(int) -> float # a need's drain, times (difficulty, a survey's rule) + bonus: fn(int) -> float # a need's gain an hour from outside (the effects) + thirst: fn() -> bool # thirst is a need at all (unbound: yes); off, water holds full + gentle: fn() -> bool # the needs stop at 22: no countdown, no collapse + held: fn() -> bool # the body is already down: the countdown waits +} +``` + +## API + +| | | +| --- | --- | +| `NEED_WARMTH`, `NEED_FOOD`, `NEED_WATER`, `NEED_ENERGY`, `NEED_COUNT` | the needs | +| `needs_level(k)`, `needs_warmth()`, `needs_food()`, `needs_water()`, `needs_energy()`, `needs_lowest()` | the bars | +| `needs_last_legs()`, `needs_last_need()`, `needs_collapses()` | the countdown's seconds (0: none), its need, and how many times the body went down | +| `needs_pace()` | the walk's pace, times: full above fifty on each need, 0.6 at nothing, and 0.6 more on last legs | +| `needs_can_run()` | energy over 8, and warmth, food and water over 20 | +| `needs_feed(k, n)`, `needs_meal(food, water, energy)`, `needs_set(k, v)`, `needs_floor(k, v)` | the verbs; every bar is held to 0..100 | +| `needs_after_night(rested, food_lost, water_lost)` | energy to `rested`, food never under 6, water never under 5 | +| `needs_went_down()`, `needs_helped_up()`, `needs_calm()` | a collapse counted; a teammate's hand (nothing under 20); the countdown off, quietly | +| `needs_run(gh)`, `needs_watch(dt)` | the drain over `gh` game hours, and the countdown over `dt` real seconds (the system's tick runs both) | +| `needs_facts() -> Queue` | `{ what, need }`: `NEEDS_CRITICAL`, `NEEDS_RESTORED`, `NEEDS_COLLAPSED` | +| `needs_system() -> System` | `"needs"`, `PH_SIMULATE`: reset, tick, and its save section (`needs_save`, `needs_load`: the four bars and the collapses) | + +## Tests + +```bash +ludic test packages/ludic.needs +``` diff --git a/packages/ludic.needs/index.ludic b/packages/ludic.needs/index.ludic new file mode 100644 index 00000000..f36e8859 --- /dev/null +++ b/packages/ludic.needs/index.ludic @@ -0,0 +1,12 @@ +# ludic.needs - four bars of 0..100 that the body's work drains and meals fill; one at zero starts +# a countdown, and the countdown running out is a fact (a collapse) the game acts on +module ludic_needs uses ludic_base +numbers float +import "ludic.base" +import "state.ludic" +import "rates.ludic" +import "run.ludic" +import "watch.ludic" +import "queries.ludic" +import "verbs.ludic" +import "system.ludic" diff --git a/packages/ludic.needs/package.ludic b/packages/ludic.needs/package.ludic new file mode 100644 index 00000000..a8b3c3cb --- /dev/null +++ b/packages/ludic.needs/package.ludic @@ -0,0 +1,5 @@ +# ludic.needs - a body's four needs (warmth, food, water, energy), what an hour of doing something +# costs them, and the countdown to a collapse when one runs out. Uses ludic.base and nothing else. +package "ludic.needs" +version "0.1.0" +kind source diff --git a/packages/ludic.needs/queries.ludic b/packages/ludic.needs/queries.ludic new file mode 100644 index 00000000..25761ccc --- /dev/null +++ b/packages/ludic.needs/queries.ludic @@ -0,0 +1,36 @@ +# queries.ludic - what the world asks the body +export function needs_level(k: int) -> float { + if not nd_ok(k) { return 0.0 } + return nd_lv()[k] +} + +export function needs_warmth() -> float { return nd_lv()[NEED_WARMTH] } +export function needs_food() -> float { return nd_lv()[NEED_FOOD] } +export function needs_water() -> float { return nd_lv()[NEED_WATER] } +export function needs_energy() -> float { return nd_lv()[NEED_ENERGY] } + +# seconds left before a collapse (0: no countdown), and the need it is for +export function needs_last_legs() -> float { return nd_legs } +export function needs_last_need() -> int { return nd_last } +export function needs_collapses() -> int { return nd_collapses } + +# the walk's pace, times: the ramp of each need, and 0.6 more on last legs +export function needs_pace() -> float { + let lv = nd_lv() + var m = nd_pace(lv[NEED_WARMTH]) * nd_pace(lv[NEED_FOOD]) + if NeedsWorld.thirst() { m = m * nd_pace(lv[NEED_WATER]) } + if nd_legs > 0.0 { m = m * 0.6 } + return m +} + +# running needs legs, calories and a body that is not already in trouble +export function needs_can_run() -> bool { + let lv = nd_lv() + return lv[NEED_ENERGY] > 8.0 and lv[NEED_FOOD] > 20.0 and lv[NEED_WARMTH] > 20.0 and lv[NEED_WATER] > 20.0 +} + +# the lowest of the four +export function needs_lowest() -> float { + let lv = nd_lv() + return Math.min(Math.min(lv[NEED_WARMTH], lv[NEED_FOOD]), Math.min(lv[NEED_WATER], lv[NEED_ENERGY])) +} diff --git a/packages/ludic.needs/rates.ludic b/packages/ludic.needs/rates.ludic new file mode 100644 index 00000000..063c8d8e --- /dev/null +++ b/packages/ludic.needs/rates.ludic @@ -0,0 +1,65 @@ +# rates.ludic - what an hour costs, by what the body is doing. A full bar is a DAY at the median +# of a real trip (walking, some standing, an hour by the fire): 100 / 24 is about 4.2 an hour. +# The hardest sustained work (running, the midday sun, the lake) empties it in about fourteen, +# so the caps are the product of the multipliers, never each of them; at rest it lasts two days. +const NEED_FOOD_CAP: float = 7.2 +const NEED_WATER_CAP: float = 7.5 + +function nd_hard(a: int) -> bool { return a == NEEDS_RUN or a == NEEDS_SWIM_HARD } +function nd_swim(a: int) -> bool { return a >= NEEDS_SWIM } + +function nd_food_rate(a: int) -> float { + var eat = 4.0 + if nd_hard(a) { eat = 6.8 } + if a == NEEDS_SIT { eat = eat * 0.75 } + return Math.min(eat, NEED_FOOD_CAP) +} + +# thirst follows the work rather than the clock: a run, the sun and the lake (you sweat +# swimming and never notice) all pull it down, up to the day's cap +function nd_water_rate(a: int) -> float { + var drink = 3.8 + if a == NEEDS_WALK or a == NEEDS_SWIM or a == NEEDS_DIVE { drink = 4.6 } + if nd_hard(a) { drink = 7.0 } + if nd_swim(a) { drink = drink * 1.6 } + if NeedsWorld.hot() { drink = drink * 1.5 } + if a == NEEDS_SIT { drink = drink * 0.6 } + return Math.min(drink, NEED_WATER_CAP) +} + +# the water costs more than running, and the cold is the real limit rather than the legs +function nd_energy_rate(a: int) -> float { + if a == NEEDS_SIT { return NeedsWorld.resting() } + if a == NEEDS_WALK { return -5.0 * NeedsWorld.effort() } + if a == NEEDS_RUN { return -22.0 * NeedsWorld.effort() } + if a == NEEDS_SWIM { return -28.0 } + if a == NEEDS_SWIM_HARD { return -38.0 } + if a == NEEDS_FLOAT { return -6.0 } + if a == NEEDS_DIVE { return -34.0 } + return -2.0 +} + +# the surroundings, then the lake taking it, the effort giving a little back, clothing keeping +# some of what is lost, and a dry body holding heat badly +function nd_warmth_rate(a: int, water: float) -> float { + var wr = NeedsWorld.warmth() + if nd_swim(a) { + wr = Math.min(wr, 0.0) - 24.0 + if a == NEEDS_SWIM_HARD { wr = wr + 2.0 } + if a == NEEDS_FLOAT { wr = wr - 2.0 } + if a == NEEDS_DIVE { wr = wr - 6.0 } + } + if nd_hard(a) { wr = wr + 3.0 } + let keep = Math.clamp(NeedsWorld.keep(), 0.0, 1.0) + if keep > 0.0 and wr < 0.0 { wr = wr * (1.0 - keep) } + if water < 25.0 and wr < 0.0 { wr = wr * 1.25 } + return wr + NeedsWorld.bonus(NEED_WARMTH) +} + +# under fifty a need is felt in the legs before it is a crisis: full pace above fifty, three +# quarters at twenty, 0.6 at nothing - one continuous line, steeper below twenty +function nd_pace(v: float) -> float { + if not (v < 50.0) { return 1.0 } + if v < 20.0 { return 0.75 - 0.15 * ((20.0 - v) / 20.0) } + return 1.0 - 0.25 * ((50.0 - v) / 30.0) +} diff --git a/packages/ludic.needs/run.ludic b/packages/ludic.needs/run.ludic new file mode 100644 index 00000000..5585eff6 --- /dev/null +++ b/packages/ludic.needs/run.ludic @@ -0,0 +1,15 @@ +# run.ludic - `gh` game hours of whatever the body is doing, taken out of the four bars +function nd_add(k: int, d: float) -> void { + let lv = nd_lv() + lv[k] = Math.clamp(lv[k] + d, 0.0, 100.0) +} + +export function needs_run(gh: float) -> void { + let a = NeedsWorld.activity() + nd_add(NEED_FOOD, (NeedsWorld.bonus(NEED_FOOD) - nd_food_rate(a) * NeedsWorld.drain(NEED_FOOD)) * gh) + if NeedsWorld.thirst() { + nd_add(NEED_WATER, (NeedsWorld.bonus(NEED_WATER) - nd_water_rate(a) * NeedsWorld.drain(NEED_WATER)) * gh) + } else { nd_lv()[NEED_WATER] = 100.0 } + nd_add(NEED_ENERGY, (nd_energy_rate(a) + NeedsWorld.bonus(NEED_ENERGY)) * gh) + nd_add(NEED_WARMTH, nd_warmth_rate(a, nd_lv()[NEED_WATER]) * gh) +} diff --git a/packages/ludic.needs/state.ludic b/packages/ludic.needs/state.ludic new file mode 100644 index 00000000..33d88d6c --- /dev/null +++ b/packages/ludic.needs/state.ludic @@ -0,0 +1,88 @@ +# state.ludic - the needs, what the body is doing, the port, the facts, and what the body holds +export const NEED_WARMTH: int = 0 +export const NEED_FOOD: int = 1 +export const NEED_WATER: int = 2 +export const NEED_ENERGY: int = 3 +export const NEED_COUNT: int = 4 + +# what the body is doing, which is what an hour costs +export const NEEDS_STILL: int = 0 +export const NEEDS_WALK: int = 1 +export const NEEDS_RUN: int = 2 +export const NEEDS_SIT: int = 3 +export const NEEDS_SWIM: int = 4 +export const NEEDS_SWIM_HARD: int = 5 +export const NEEDS_FLOAT: int = 6 +export const NEEDS_DIVE: int = 7 + +# what the needs ask the world. Every member has a default: standing still in mild air, no +# clothing, no multipliers, no bonuses, thirst on, a collapse possible, the body up. +export port NeedsWorld { + activity: fn() -> int = fn nd_still # NEEDS_* above + resting: fn() -> float = fn nd_rest # energy an hour sitting gives back (the seat's say) + warmth: fn() -> float = fn nd_mild # warmth an hour from the surroundings: sun, night, weather, a fire, the water + keep: fn() -> float = fn nd_none # the fraction of warmth being lost that clothing keeps + hot: fn() -> bool = fn nd_no # the midday sun: thirst runs half again faster + effort: fn() -> float = fn nd_one # what moving costs in energy, times (the boots) + drain: fn(int) -> float = fn nd_one_of # a need's drain, times (the difficulty, a survey's rule) + bonus: fn(int) -> float = fn nd_none_of # a need's gain an hour from outside (a meal's warmth, a drink's legs) + thirst: fn() -> bool = fn nd_yes # thirst is a need at all; off, water holds full + gentle: fn() -> bool = fn nd_no # the needs stop above trouble: no countdown, no collapse + held: fn() -> bool = fn nd_no # the body is already down: the countdown waits +} +function nd_still() -> int { return NEEDS_STILL } +function nd_rest() -> float { return 40.0 } +function nd_mild() -> float { return -2.0 } +function nd_none() -> float { return 0.0 } +function nd_no() -> bool { return false } +function nd_yes() -> bool { return true } +function nd_one() -> float { return 1.0 } +function nd_one_of(k: int) -> float { return 1.0 } +function nd_none_of(k: int) -> float { return 0.0 } + +# what happened: a need ran out and the countdown began, every need came back above the line, or +# the countdown ran out - the game decides what a collapse costs and says so +export const NEEDS_CRITICAL: int = 0 +export const NEEDS_RESTORED: int = 1 +export const NEEDS_COLLAPSED: int = 2 + +export property NeedFact { + what: int = 0 + need: int = 0 +} + +var nd_level: []float = null +var nd_legs: float = 0.0 # seconds left before a collapse, 0 = none +var nd_last: int = 0 # the need the countdown is for +var nd_collapses: int = 0 +var nd_facts: Queue = null + +function nd_lv() -> []float { + if nd_level == null { + nd_level = floats(NEED_COUNT) + nd_fill() + } + return nd_level +} + +function nd_fill() -> void { + nd_level[NEED_WARMTH] = 100.0 + nd_level[NEED_FOOD] = 70.0 + nd_level[NEED_WATER] = 85.0 + nd_level[NEED_ENERGY] = 100.0 +} + +# the facts, oldest first; the game drains them into its notices and its collapse +export function needs_facts() -> Queue { + if nd_facts == null { nd_facts = queue_new("needs.facts") } + return nd_facts +} + +function nd_fact(what: int, need: int) -> void { + let f = new NeedFact + f.what = what + f.need = need + q_push(needs_facts(), f) +} + +function nd_ok(k: int) -> bool { return k >= 0 and k < NEED_COUNT } diff --git a/packages/ludic.needs/system.ludic b/packages/ludic.needs/system.ludic new file mode 100644 index 00000000..365282d1 --- /dev/null +++ b/packages/ludic.needs/system.ludic @@ -0,0 +1,34 @@ +# system.ludic - the needs as a system: PH_SIMULATE, the drain then the watch, and its own save +# section (the countdown is not saved: a loaded body starts it again if a need is still empty) +function needs_tick(t: Tick) -> void { + needs_run(t.hours) + needs_watch(t.dt) +} + +export function needs_save() -> Val { + let v = Value.object() + sv_put_float(v, "warmth", needs_warmth()) + sv_put_float(v, "food", needs_food()) + sv_put_float(v, "water", needs_water()) + sv_put_float(v, "energy", needs_energy()) + sv_put_int(v, "collapses", nd_collapses) + return v +} + +export function needs_load(v: Val, version: int) -> void { + needs_reset() + needs_set(NEED_WARMTH, sv_float(v, "warmth", 100.0)) + needs_set(NEED_FOOD, sv_float(v, "food", 70.0)) + needs_set(NEED_WATER, sv_float(v, "water", 85.0)) + needs_set(NEED_ENERGY, sv_float(v, "energy", 100.0)) + nd_collapses = sv_int(v, "collapses", 0) +} + +export function needs_system() -> System { + let s = system_new("needs", PH_SIMULATE) + s.reset = fn needs_reset + s.tick = fn needs_tick + s.save = fn needs_save + s.load = fn needs_load + return s +} diff --git a/packages/ludic.needs/tests/needs_test.ludic b/packages/ludic.needs/tests/needs_test.ludic new file mode 100644 index 00000000..334728b2 --- /dev/null +++ b/packages/ludic.needs/tests/needs_test.ludic @@ -0,0 +1,210 @@ +# needs_test.ludic - a full bar is a day, work drains faster, the port's multipliers and bonuses +# count, meals fill, a need at zero counts down to a collapse as a fact, and the save reads back +import "ludic.needs" +import "ludic.base" +program NeedsTest { + numbers float + var act: int = 0 + var air: float = -2.0 + var keep: float = 0.0 + var hot: bool = false + var thirst: bool = true + var gentle: bool = false + var held: bool = false + var food_mult: float = 1.0 + var warm_bonus: float = 0.0 + + function fake_act() -> int { return act } + function fake_air() -> float { return air } + function fake_keep() -> float { return keep } + function fake_hot() -> bool { return hot } + function fake_thirst() -> bool { return thirst } + function fake_gentle() -> bool { return gentle } + function fake_held() -> bool { return held } + function fake_drain(k: int) -> float { + if k == NEED_FOOD { return food_mult } + return 1.0 + } + function fake_bonus(k: int) -> float { + if k == NEED_WARMTH { return warm_bonus } + return 0.0 + } + + bind NeedsWorld { + activity: fn fake_act + warmth: fn fake_air + keep: fn fake_keep + hot: fn fake_hot + thirst: fn fake_thirst + gentle: fn fake_gentle + held: fn fake_held + drain: fn fake_drain + bonus: fn fake_bonus + } + + function fresh() -> void { needs_reset() } + function hours(h: float) -> void { for i in 0 .. int(h * 10.0) { needs_run(0.1) } } + function facts() -> []NeedFact { return q_drain(needs_facts()) } + + test "a new body is warm, rested, fed and watered" { + fresh() + expect(needs_warmth() == 100.0) + expect(needs_energy() == 100.0) + expect(needs_food() == 70.0) + expect(needs_water() == 85.0) + expect(needs_pace() == 1.0) + expect(needs_can_run()) + } + + test "a walking day empties a full bar of food in about a day, a run much sooner" { + fresh() + needs_set(NEED_FOOD, 100.0) + act = NEEDS_WALK + hours(12.0) + expect_near(needs_food(), 52.0, 0.5) + needs_set(NEED_FOOD, 100.0) + act = NEEDS_RUN + hours(12.0) + expect(needs_food() < 20.0) + } + + test "the sun and the lake make thirst faster, but never past the cap" { + fresh() + needs_set(NEED_WATER, 100.0) + act = NEEDS_SWIM_HARD + hot = true + hours(1.0) + expect_near(needs_water(), 92.5, 0.01) + } + + test "thirst off holds water full" { + fresh() + thirst = false + needs_set(NEED_WATER, 10.0) + hours(1.0) + expect(needs_water() == 100.0) + } + + test "the port's drain and bonus are what the bars get" { + fresh() + food_mult = 2.0 + hours(1.0) + expect_near(needs_food(), 62.0, 0.01) + warm_bonus = 10.0 + air = 0.0 + needs_set(NEED_WARMTH, 50.0) + hours(1.0) + expect_near(needs_warmth(), 60.0, 0.01) + } + + test "clothing keeps what is being lost, and a dry body loses more" { + fresh() + air = -20.0 + keep = 0.5 + hours(1.0) + expect_near(needs_warmth(), 90.0, 0.01) + needs_set(NEED_WARMTH, 100.0) + needs_set(NEED_WATER, 10.0) + keep = 0.0 + hours(1.0) + expect_near(needs_warmth(), 75.0, 0.01) + } + + test "sitting gives energy back, swimming takes it" { + fresh() + needs_set(NEED_ENERGY, 40.0) + act = NEEDS_SIT + hours(1.0) + expect_near(needs_energy(), 80.0, 0.01) + act = NEEDS_SWIM + hours(1.0) + expect_near(needs_energy(), 52.0, 0.01) + } + + test "meals fill, and every bar is held to 0..100" { + fresh() + needs_meal(50.0, 50.0, 10.0) + expect(needs_food() == 100.0) + expect(needs_water() == 100.0) + needs_feed(NEED_ENERGY, -150.0) + expect(needs_energy() == 0.0) + needs_floor(NEED_ENERGY, 30.0) + expect(needs_energy() == 30.0) + } + + test "a need at zero is a countdown, then a collapse, as facts" { + fresh() + needs_set(NEED_WATER, 0.0) + needs_watch(0.1) + let a = facts() + expect_eq(len(a), 1) + expect_eq(a[0].what, NEEDS_CRITICAL) + expect_eq(a[0].need, NEED_WATER) + expect(needs_last_legs() > 0.0) + expect(needs_pace() < 0.6) + var i = 0 + while q_len(needs_facts()) == 0 and i < 200 { + needs_watch(0.1) + i += 1 + } + let b = facts() + expect_eq(len(b), 1) + expect_eq(b[0].what, NEEDS_COLLAPSED) + expect_eq(b[0].need, NEED_WATER) + expect(i >= 99 and i <= 101) + expect_eq(needs_collapses(), 0) + needs_went_down() + expect_eq(needs_collapses(), 1) + } + + test "a drink in time calls the countdown off" { + fresh() + needs_set(NEED_FOOD, 0.0) + needs_watch(0.1) + needs_feed(NEED_FOOD, 30.0) + needs_watch(0.1) + let f = facts() + expect_eq(len(f), 2) + expect_eq(f[1].what, NEEDS_RESTORED) + expect(needs_last_legs() == 0.0) + } + + test "gentle never counts down, and a body already down waits" { + fresh() + gentle = true + needs_set(NEED_WARMTH, 0.0) + needs_watch(0.1) + expect(needs_warmth() == 22.0) + expect_eq(q_len(needs_facts()), 0) + gentle = false + held = true + needs_set(NEED_WARMTH, 0.0) + needs_watch(0.1) + expect_eq(q_len(needs_facts()), 0) + } + + test "a hand up costs the body but leaves nothing below twenty" { + fresh() + needs_set(NEED_FOOD, 25.0) + needs_helped_up() + expect(needs_food() == 20.0) + expect(needs_warmth() == 85.0) + expect(needs_energy() == 75.0) + } + + test "a save section reads back the same body" { + fresh() + needs_set(NEED_FOOD, 33.5) + needs_set(NEED_WARMTH, 12.25) + needs_went_down() + let root = save_tree() + save_section(root, "needs", 1, needs_save()) + let text = save_encode(root) + needs_reset() + let n = load_section(save_decode(text), "needs") + needs_load(n.data, n.version) + expect(needs_food() == 33.5) + expect(needs_warmth() == 12.25) + expect_eq(needs_collapses(), 1) + } +} diff --git a/packages/ludic.needs/verbs.ludic b/packages/ludic.needs/verbs.ludic new file mode 100644 index 00000000..d0632389 --- /dev/null +++ b/packages/ludic.needs/verbs.ludic @@ -0,0 +1,58 @@ +# verbs.ludic - the only way the needs change from outside; each is held to 0..100 +# `n` more of a need (less, when negative): a meal, a drink, a shove from a bear +export function needs_feed(k: int, n: float) -> void { + if nd_ok(k) { nd_add(k, n) } +} + +# a meal: food, water and energy at once +export function needs_meal(food: float, water: float, energy: float) -> void { + nd_add(NEED_FOOD, food) + nd_add(NEED_WATER, water) + nd_add(NEED_ENERGY, energy) +} + +export function needs_set(k: int, v: float) -> void { + if nd_ok(k) { nd_lv()[k] = Math.clamp(v, 0.0, 100.0) } +} + +# at least `v` +export function needs_floor(k: int, v: float) -> void { + if nd_ok(k) and nd_lv()[k] < v { needs_set(k, v) } +} + +# a night: rested to what it was worth, hungrier and thirstier for it, but never empty +export function needs_after_night(rested: float, food_lost: float, water_lost: float) -> void { + let lv = nd_lv() + lv[NEED_ENERGY] = Math.clamp(rested, 0.0, 100.0) + lv[NEED_FOOD] = Math.max(lv[NEED_FOOD] - food_lost, 6.0) + if water_lost > 0.0 { lv[NEED_WATER] = Math.max(lv[NEED_WATER] - water_lost, 5.0) } +} + +# the countdown called off, and nothing said (a wake, a load, a test) +export function needs_calm() -> void { nd_legs = 0.0 } + +# the body went down, however: one more collapse, and no countdown running +export function needs_went_down() -> void { + nd_collapses += 1 + nd_legs = 0.0 +} + +# a hand up: the cost is in the body, and nothing goes below twenty +export function needs_helped_up() -> void { + let lv = nd_lv() + lv[NEED_WARMTH] = Math.max(20.0, lv[NEED_WARMTH] - 15.0) + lv[NEED_FOOD] = Math.max(20.0, lv[NEED_FOOD] - 15.0) + lv[NEED_WATER] = Math.max(20.0, lv[NEED_WATER] - 15.0) + lv[NEED_ENERGY] = Math.max(20.0, lv[NEED_ENERGY] - 25.0) + nd_legs = 0.0 +} + +# a new trip: fed, watered, rested and warm, and never walked out +export function needs_reset() -> void { + nd_lv() + nd_fill() + nd_legs = 0.0 + nd_last = 0 + nd_collapses = 0 + q_clear(needs_facts()) +} diff --git a/packages/ludic.needs/watch.ludic b/packages/ludic.needs/watch.ludic new file mode 100644 index 00000000..22f86f57 --- /dev/null +++ b/packages/ludic.needs/watch.ludic @@ -0,0 +1,53 @@ +# watch.ludic - last legs: warmth, food or water at zero starts ten seconds of countdown; anything +# above five on all three calls it off; the countdown running out is a collapse, as a fact +const LAST_LEGS: float = 10.0 +const GENTLE_FLOOR: float = 22.0 + +function nd_empty() -> int { + let lv = nd_lv() + if lv[NEED_WARMTH] == 0.0 { return NEED_WARMTH } + if lv[NEED_FOOD] == 0.0 { return NEED_FOOD } + if lv[NEED_WATER] == 0.0 { return NEED_WATER } + return -1 +} + +function nd_clear() -> bool { + let lv = nd_lv() + return lv[NEED_WARMTH] > 5.0 and lv[NEED_FOOD] > 5.0 and lv[NEED_WATER] > 5.0 +} + +# Gentle: a need that runs down stops just above where the legs start to go +function nd_gentle() -> void { + let lv = nd_lv() + lv[NEED_WARMTH] = Math.max(lv[NEED_WARMTH], GENTLE_FLOOR) + lv[NEED_FOOD] = Math.max(lv[NEED_FOOD], GENTLE_FLOOR) + lv[NEED_WATER] = Math.max(lv[NEED_WATER], GENTLE_FLOOR) + nd_legs = 0.0 +} + +# `dt` real seconds; run it after everything this frame that could fill a need +export function needs_watch(dt: float) -> void { + if NeedsWorld.held() { return } + if NeedsWorld.gentle() { + nd_gentle() + return + } + let need = nd_empty() + if need < 0 { + if nd_legs > 0.0 and nd_clear() { + nd_legs = 0.0 + nd_fact(NEEDS_RESTORED, nd_last) + } + return + } + if nd_legs == 0.0 { + nd_legs = LAST_LEGS + nd_last = need + nd_fact(NEEDS_CRITICAL, need) + return + } + nd_legs = nd_legs - dt + if nd_legs > 0.0 { return } + nd_legs = 0.0 + nd_fact(NEEDS_COLLAPSED, nd_last) +}