ludic.needs: a body's four needs, their rates, meals and the countdown to a collapse as a fact
Warmth, food, water and energy as bars of 0..100; what an hour of doing something costs them (the package's rates: a full bar is a day at a trip's median); the countdown when one runs out, reported as NEEDS_CRITICAL / NEEDS_RESTORED / NEEDS_COLLAPSED facts. The body's work, the air, clothing, the sun, the difficulty and the effects come through the NeedsWorld port. Its own save section. Uses ludic_base only; 13 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
31e6740033
commit
cf42214974
11 changed files with 631 additions and 0 deletions
55
packages/ludic.needs/README.md
Normal file
55
packages/ludic.needs/README.md
Normal file
|
|
@ -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<NeedFact>` | `{ 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
|
||||
```
|
||||
12
packages/ludic.needs/index.ludic
Normal file
12
packages/ludic.needs/index.ludic
Normal file
|
|
@ -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"
|
||||
5
packages/ludic.needs/package.ludic
Normal file
5
packages/ludic.needs/package.ludic
Normal file
|
|
@ -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
|
||||
36
packages/ludic.needs/queries.ludic
Normal file
36
packages/ludic.needs/queries.ludic
Normal file
|
|
@ -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]))
|
||||
}
|
||||
65
packages/ludic.needs/rates.ludic
Normal file
65
packages/ludic.needs/rates.ludic
Normal file
|
|
@ -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)
|
||||
}
|
||||
15
packages/ludic.needs/run.ludic
Normal file
15
packages/ludic.needs/run.ludic
Normal file
|
|
@ -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)
|
||||
}
|
||||
88
packages/ludic.needs/state.ludic
Normal file
88
packages/ludic.needs/state.ludic
Normal file
|
|
@ -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<NeedFact> = 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<NeedFact> {
|
||||
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 }
|
||||
34
packages/ludic.needs/system.ludic
Normal file
34
packages/ludic.needs/system.ludic
Normal file
|
|
@ -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
|
||||
}
|
||||
210
packages/ludic.needs/tests/needs_test.ludic
Normal file
210
packages/ludic.needs/tests/needs_test.ludic
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
58
packages/ludic.needs/verbs.ludic
Normal file
58
packages/ludic.needs/verbs.ludic
Normal file
|
|
@ -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())
|
||||
}
|
||||
53
packages/ludic.needs/watch.ludic
Normal file
53
packages/ludic.needs/watch.ludic
Normal file
|
|
@ -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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue