feat(ludic.weather): spells of weather as a mechanic package

The kinds and their odds are data the game hands in; a port asks whether this
machine owns the world, the hour, the night, the odds from a kind, what the
story wants of the sky (a front, a hold, a clear night), the front's kind and
length, and a seed. Its own dice; a new spell, a clearing, lightning and a
wanted front are facts on weather_facts(); its own save section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 04:48:38 +03:00
parent a2012a5afe
commit 093ca0d260
12 changed files with 746 additions and 0 deletions

View file

@ -0,0 +1,80 @@
# ludic.weather
The sky as a chain of spells: clear, cloud, rain, a storm, snow - or whatever kinds the game
hands in. Each spell lasts a few game hours, eases in and out at its edges, sets the wind and
wets the ground, and gives way to the next by odds the game may lean on (a season, a wet week).
A forecast is drawn each morning, and the story can ask for a front or for a clear night. Uses
[`ludic.base`](../ludic.base/README.md) and nothing else.
```ludic
import "ludic.weather"
```
A kind is the game's number: its place in the list handed to `weather_kinds`. The package knows
what a kind DOES (it is wet, it throws lightning, it holds the cold) and never what it is called
in a sentence; the game draws the sky and says the notices.
Only the machine that owns the world runs the spells. Every machine eases the ground's wetness
and throws its own lightning out of a storm it is told of (`weather_told`), from the package's
own dice - never the world's.
## The kinds
```ludic
property WeatherKind {
name: string # for weather_name
clear: bool # the kind a sky eases out to (amount 0); the first one starts a trip
wet: bool # rain or snow is falling (weather_wet)
soak: float # how wet the ground gets under it, 0..1
overcast: float # times the amount: weather_overcast
cold: float # times the amount: weather_cold, in the game's units
wind: float # the strength the wind builds toward
wind_dir: float # radians the wind blows toward; below 0, the valley's daily turn
thunder: bool # a storm: lightning every 9-34 s while the amount is over a half
spell_lo, spell_hi # game hours a spell lasts
odds: []float # weights for the next spell, one per kind
}
```
## The port
```ludic
property WeatherWorld {
owns_world: fn() -> bool # does this machine run the world? (unbound: yes)
hours: fn() -> float # the hour, for the wind's daily turn (unbound: noon)
is_night: fn() -> bool # a front clearing at night is no WEATHER_CLEARED
odds: fn(int) -> []float # the next spell's weights from a kind (unbound: the kind's own)
bias: fn() -> int # what the story wants now: WEATHER_NO_BIAS, _WANT_WET, _HOLD, _WANT_CLEAR
front: fn() -> int # the kind a wanted front comes as (unbound: the first wet kind)
front_hours: fn() -> float # the least a wanted front lasts (never under 5)
seed: fn() -> int # a new trip's dice
}
```
The bias is asked every tick on the owner. `WANT_WET` on a dry sky forecasts a front
(`WEATHER_FRONT`), due within an hour and a half; `HOLD` keeps the current spell going at least
1.2 hours more; `WANT_CLEAR` clears within half an hour and keeps it clear. The game decides WHEN
it wants them (daylight, a tarp up, a stars job open).
## API
| | |
| --- | --- |
| `weather_bind(w)`, `weather_kinds(ks)`, `weather_config_wind(down, up, from, to)` | the port, the kinds, and the valley's wind: toward `down` by night, toward `up` from `from` to `to` |
| `weather_run(dt, gh)` | one step (the system's tick does this from `Tick.dt` and `Tick.hours`) |
| `weather_morning()` | a new day's forecast: seven times in ten it becomes the next spell |
| `weather_reset()` | a new trip: the first clear kind, nothing forecast, dice from the seed |
| `weather_set(kind, next, left)`, `weather_set_wind(s, dir)`, `weather_set_ground(w)`, `weather_set_flash(f)` | the sky outright (staging, tests) |
| `weather_told(kind, next, left, amount, wind, dir, forecast)` | what the owner says the sky is; a new spell is a fact here too |
| `weather_state()`, `weather_next()`, `weather_left()`, `weather_amount()`, `weather_forecast()`, `weather_forced()` | the spell |
| `weather_wet()`, `weather_overcast()`, `weather_cold()`, `weather_wind()`, `weather_dir()`, `weather_ground_wet()`, `weather_flash()` | what it does |
| `weather_after_front()`, `weather_wet_yesterday()` | the half hour of clear light after a front; whether yesterday had rain or snow in it |
| `weather_kind(k)`, `weather_kind_count()`, `weather_name(k)`, `weather_draw_from(w, from, roll)` | the kinds, and the draw itself |
| `weather_facts() -> Queue<WeatherFact>` | `{ what, kind, from }`: `WEATHER_BEGAN` (a new spell), `WEATHER_CLEARED` (a wet spell gave way to clear sky by day), `WEATHER_THUNDER`, `WEATHER_FRONT` (a wanted front is forecast) |
| `weather_system() -> System` | `"weather"`, `PH_SIMULATE`: reset, tick, and its save section (`weather_save`, `weather_load`: the spell, the forecast, the wind's direction, the ground, the dice) |
## Tests
```bash
ludic build packages/ludic.weather/tests/weather_test.ludic --headless -o /tmp/weather_test && /tmp/weather_test
```

View file

@ -0,0 +1,43 @@
# bias.ludic - the story leaning on the sky. Weather is a chain of chances, and a goal that needs
# rain could wait a week for it; so while the world says it wants a front, one is forecast and
# arrives within the hour and a half, and while it wants clear sky, the sky clears and holds.
function wx_front_kind() -> int {
if wx_world != null and wx_world.front != null { return wx_world.front() }
return wx_first_wet()
}
function wx_want_wet() -> void {
if wx_kinds[wx_state].wet or wx_forced { return }
let front = wx_front_kind()
wx_next = front
wx_forecast = front
wx_forced = true
if wx_left > 1.5 { wx_left = 1.5 }
wx_fact(WEATHER_FRONT, front, wx_state)
}
function wx_hold() -> void {
if wx_left < 1.2 {
wx_left = 1.2
wx_next = wx_state
}
}
function wx_want_clear() -> void {
let clear = wx_clear_kind()
if wx_state != clear and not wx_forced {
if wx_left > 0.5 { wx_left = 0.5 }
wx_next = clear
}
if wx_state == clear and wx_left < 1.0 {
wx_left = 1.0
wx_next = clear
}
}
function wx_apply_bias() -> void {
let b = wx_bias()
if b == WEATHER_WANT_WET { wx_want_wet() }
if b == WEATHER_HOLD { wx_hold() }
if b == WEATHER_WANT_CLEAR { wx_want_clear() }
}

View file

@ -0,0 +1,60 @@
# config.ludic - the kinds the game hands in, the wind's daily turn, and the port's defaults
var wx_down_dir: float = 0.0 # the wind overnight and into the morning
var wx_up_dir: float = 3.1415927 # the wind while the slopes are warm
var wx_up_from: float = 10.0
var wx_up_to: float = 19.0
# the kinds, in the game's order; the first `clear` one is where a new trip starts
export function weather_kinds(kinds: []WeatherKind) -> void { wx_kinds = kinds }
export function weather_kind_count() -> int {
if wx_kinds == null { return 0 }
return len(wx_kinds)
}
export function weather_kind(k: int) -> WeatherKind { return wx_kinds[k] }
# a valley's wind: down it by night, up it from `up_from` to `up_to` while the slopes are warm
export function weather_config_wind(down_dir: float, up_dir: float, up_from: float, up_to: float) -> void {
wx_down_dir = down_dir
wx_up_dir = up_dir
wx_up_from = up_from
wx_up_to = up_to
}
function wx_clear_kind() -> int {
for k in 0 .. weather_kind_count() { if wx_kinds[k].clear { return k } }
return 0
}
function wx_first_wet() -> int {
for k in 0 .. weather_kind_count() { if wx_kinds[k].wet { return k } }
return 0
}
function wx_owns() -> bool {
if wx_world == null or wx_world.owns_world == null { return true }
return wx_world.owns_world()
}
function wx_hours() -> float {
if wx_world == null or wx_world.hours == null { return 12.0 }
return wx_world.hours()
}
function wx_night() -> bool {
if wx_world == null or wx_world.is_night == null { return false }
return wx_world.is_night()
}
function wx_bias() -> int {
if wx_world == null or wx_world.bias == null { return WEATHER_NO_BIAS }
return wx_world.bias()
}
function wx_wrap(a: float) -> float {
var d = a
while d > 3.1415927 { d = d - 6.2831853 }
while d < -3.1415927 { d = d + 6.2831853 }
return d
}

View file

@ -0,0 +1,13 @@
# ludic.weather - the sky as a chain of spells: each kind is data the game hands in, the next is
# drawn from odds the game may lean on, and what changed is a fact on a queue
module ludic_weather
numbers float
import "ludic.base"
import "state.ludic"
import "config.ludic"
import "pick.ludic"
import "run.ludic"
import "bias.ludic"
import "queries.ludic"
import "verbs.ludic"
import "system.ludic"

View file

@ -0,0 +1,5 @@
# ludic.weather - spells of weather the game describes as data: odds, wetness, wind, storms and a
# forecast, leaning on the story when asked. Uses ludic.base and nothing else. See README.md.
package "ludic.weather"
version "0.1.0"
kind source

View file

@ -0,0 +1,31 @@
# pick.ludic - the next spell, drawn from weights, and how long one lasts. The weights are the
# kind's own unless the world leans on them (a season, a wet week); every draw is this package's
function wx_odds(from: int) -> []float {
if wx_world != null and wx_world.odds != null { return wx_world.odds(from) }
return wx_kinds[from].odds
}
# the kind a set of weights lands on; no weights (or all zero) keeps the kind it came from
export function weather_draw_from(w: []float, from: int, roll: float) -> int {
if w == null { return from }
var total = 0.0
for k in 0 .. len(w) { if w[k] > 0.0 { total = total + w[k] } }
if not (total > 0.0) { return from }
var at = roll * total
for k in 0 .. len(w) {
if w[k] > 0.0 {
if at < w[k] { return k }
at = at - w[k]
}
}
return len(w) - 1
}
function wx_pick_next(from: int) -> int {
return weather_draw_from(wx_odds(from), from, rng_float(wx_dice))
}
function wx_spell_hours(k: int) -> float {
let kd = wx_kinds[k]
return rng_span(wx_dice, kd.spell_lo, kd.spell_hi)
}

View file

@ -0,0 +1,38 @@
# queries.ludic - what the world asks the sky
export function weather_state() -> int { return wx_state }
export function weather_next() -> int { return wx_next }
export function weather_left() -> float { return wx_left }
export function weather_amount() -> float { return wx_amount }
export function weather_forecast() -> int { return wx_forecast }
export function weather_forced() -> bool { return wx_forced }
export function weather_wind() -> float { return wx_wind }
export function weather_dir() -> float { return wx_dir }
export function weather_ground_wet() -> float { return wx_ground }
export function weather_flash() -> float { return wx_flash }
# rain or snow is falling, and more than a spit of it
export function weather_wet() -> bool {
if weather_kind_count() == 0 { return false }
return wx_kinds[wx_state].wet and wx_amount > 0.3
}
export function weather_overcast() -> float {
if weather_kind_count() == 0 { return 0.0 }
return wx_kinds[wx_state].overcast * wx_amount
}
export function weather_cold() -> float {
if weather_kind_count() == 0 { return 0.0 }
return wx_kinds[wx_state].cold * wx_amount
}
export function weather_name(k: int) -> string {
if k < 0 or k >= weather_kind_count() { return "" }
return wx_kinds[k].name
}
# the half hour after a front goes through, when the air is glass-clear
export function weather_after_front() -> bool { return wx_clear_t > 0.0 }
# yesterday had rain or snow in it (the valley's fog pools after a wet day)
export function weather_wet_yesterday() -> bool { return wx_wet_yesterday }

View file

@ -0,0 +1,76 @@
# run.ludic - the sky moving on. Only the machine that owns the world runs the spells; every
# machine eases the ground's wetness and makes its own lightning out of a storm it is told of.
function wx_turn() -> void {
let was = wx_state
wx_state = wx_next
wx_next = wx_pick_next(wx_state)
wx_left = wx_spell_hours(wx_state)
if wx_forced {
wx_forced = false
var least = 5.0
if wx_world != null and wx_world.front_hours != null { least = Math.max(least, wx_world.front_hours()) }
if wx_left < least { wx_left = least }
}
if wx_state == was { return }
# a front going through and leaving clear sky behind it: the light people get up for
if wx_kinds[wx_state].clear and wx_kinds[was].wet and not wx_night() {
wx_clear_t = 0.6
wx_fact(WEATHER_CLEARED, wx_state, was)
}
wx_fact(WEATHER_BEGAN, wx_state, was)
}
function wx_wind_tick(dt: float) -> void {
let kd = wx_kinds[wx_state]
wx_wind = wx_wind + (kd.wind - wx_wind) * Math.min(1.0, dt * 0.3)
var want = kd.wind_dir
if want < 0.0 {
let h = wx_hours()
want = wx_down_dir
if h > wx_up_from and h < wx_up_to { want = wx_up_dir }
}
let r = dt * 0.12
wx_dir = wx_wrap(wx_dir + Math.clamp(wx_wrap(want - wx_dir), -r, r))
}
# the spells: the story's lean, the clock running down, the edges eased, the wind
function wx_spells(dt: float, gh: float) -> void {
wx_apply_bias()
wx_left = wx_left - gh
if wx_left < 0.0 { wx_turn() }
var want = 1.0
if wx_kinds[wx_state].clear { want = 0.0 }
if wx_left < 0.5 and wx_kinds[wx_next].clear { want = 0.0 }
wx_amount = wx_amount + (want - wx_amount) * Math.min(1.0, dt * 0.5)
wx_wind_tick(dt)
if wx_clear_t > 0.0 { wx_clear_t = wx_clear_t - gh }
}
# soaking is faster than drying: a downpour wets the ground in under a minute, and it takes
# several to give it back
function wx_ground_tick(dt: float) -> void {
let want = wx_kinds[wx_state].soak * wx_amount
var k = dt * 0.10
if want < wx_ground { k = dt * 0.020 }
wx_ground = wx_ground + (want - wx_ground) * Math.clamp(k, 0.0, 1.0)
}
function wx_storm_tick(dt: float) -> void {
wx_flash = wx_flash * Math.max(0.0, 1.0 - dt * 9.0)
if not wx_kinds[wx_state].thunder or wx_amount < 0.5 { return }
wx_thunder_t = wx_thunder_t - dt
if wx_thunder_t < 0.0 {
wx_thunder_t = rng_span(wx_dice, 9.0, 34.0)
wx_flash = 1.0
wx_fact(WEATHER_THUNDER, wx_state, wx_state)
}
}
# one step: `dt` real seconds and `gh` game hours
export function weather_run(dt: float, gh: float) -> void {
if weather_kind_count() == 0 { return }
if wx_owns() { wx_spells(dt, gh) }
wx_ground_tick(dt)
wx_storm_tick(dt)
if weather_wet() { wx_wet_today = true }
}

View file

@ -0,0 +1,81 @@
# state.ludic - the port, the kinds, the facts, and what the sky holds
# what the weather asks the world. Any field may be left null: the answer is then "yes, it is
# mine", noon and daylight, the kind's own odds, no bias, the first wet kind, and a seed of 1.
export property WeatherWorld {
owns_world: fn() -> bool = null
hours: fn() -> float = null
is_night: fn() -> bool = null
odds: fn(int) -> []float = null # weights for the next spell, per kind, from this kind
bias: fn() -> int = null # what the story wants now: WEATHER_* below
front: fn() -> int = null # the kind a wanted front comes as
front_hours: fn() -> float = null # the least a wanted front lasts once it arrives
seed: fn() -> int = null
}
# what the story may ask of the sky
export const WEATHER_NO_BIAS: int = 0
export const WEATHER_WANT_WET: int = 1 # send a front, soon
export const WEATHER_HOLD: int = 2 # keep this spell going a while yet
export const WEATHER_WANT_CLEAR: int = 3 # clear the sky and keep it clear
# one kind of weather; its place in the list the game hands in is its number
export property WeatherKind {
name: string = ""
clear: bool = false # the kind a sky eases out to: nothing falls, amount goes to 0
wet: bool = false # rain or snow is falling (weather_wet)
soak: float = 0.0 # how wet the ground gets under it, 0..1
overcast: float = 0.0
cold: float = 0.0 # extra cold per game hour, in the game's units
wind: float = 0.2 # the strength the wind builds toward, 0..1
wind_dir: float = -1.0 # radians the wind blows toward; below 0, the valley's daily turn
thunder: bool = false
spell_lo: float = 2.0 # game hours a spell lasts, drawn in [lo, hi)
spell_hi: float = 8.0
odds: []float = null # weights for the next spell, per kind
}
export const WEATHER_BEGAN: int = 0 # a spell began (kind, from)
export const WEATHER_CLEARED: int = 1 # a wet spell gave way to clear sky in daylight
export const WEATHER_THUNDER: int = 2 # a lightning strike, and its thunder
export const WEATHER_FRONT: int = 3 # a front the story asked for is forecast (kind)
export property WeatherFact {
what: int = 0
kind: int = 0
from: int = 0
}
var wx_world: WeatherWorld = null
var wx_kinds: []WeatherKind = null
var wx_state: int = 0
var wx_next: int = 0
var wx_left: float = 0.0 # game hours left in this spell
var wx_amount: float = 0.0 # 0..1, how far in (eases at the edges)
var wx_forecast: int = 0
var wx_forced: bool = false # the next spell is a front the story asked for
var wx_wind: float = 0.2
var wx_dir: float = 0.0 # radians the wind blows toward
var wx_ground: float = 0.0 # 0 dry .. 1 soaked
var wx_flash: float = 0.0
var wx_thunder_t: float = 0.0 # real seconds to the next strike
var wx_clear_t: float = 0.0 # game hours of after-the-front light left
var wx_wet_today: bool = false
var wx_wet_yesterday: bool = false
var wx_dice: Rng = null
var wx_facts: Queue<WeatherFact> = null
export function weather_bind(w: WeatherWorld) -> void { wx_world = w }
# the facts, oldest first; the game drains them into its notices and sounds
export function weather_facts() -> Queue<WeatherFact> {
if wx_facts == null { wx_facts = queue_new("weather.facts") }
return wx_facts
}
function wx_fact(what: int, kind: int, from: int) -> void {
let f = new WeatherFact
f.what = what
f.kind = kind
f.from = from
q_push(weather_facts(), f)
}

View file

@ -0,0 +1,50 @@
# system.ludic - the weather as a system: PH_SIMULATE, and its own save section
function weather_tick(t: Tick) -> void { weather_run(t.dt, t.hours) }
export function weather_save() -> Val {
let v = Value.object()
sv_put_int(v, "state", wx_state)
sv_put_int(v, "next", wx_next)
sv_put_float(v, "left", wx_left)
sv_put_int(v, "forecast", wx_forecast)
sv_put_bool(v, "forced", wx_forced)
sv_put_float(v, "dir", wx_dir)
sv_put_float(v, "ground", wx_ground)
sv_put_float(v, "clear_t", wx_clear_t)
sv_put_bool(v, "wet_today", wx_wet_today)
sv_put_bool(v, "wet_yesterday", wx_wet_yesterday)
sv_put_int(v, "dice", wx_dice.s)
return v
}
function wx_kind_ok(k: int) -> int {
if k < 0 or k >= weather_kind_count() { return wx_clear_kind() }
return k
}
export function weather_load(v: Val, version: int) -> void {
weather_reset()
wx_state = wx_kind_ok(sv_int(v, "state", 0))
wx_next = wx_kind_ok(sv_int(v, "next", 0))
wx_left = sv_float(v, "left", 10.0)
wx_forecast = wx_kind_ok(sv_int(v, "forecast", 0))
wx_forced = sv_bool(v, "forced", false)
wx_dir = sv_float(v, "dir", 0.0)
wx_ground = sv_float(v, "ground", 0.0)
wx_clear_t = sv_float(v, "clear_t", 0.0)
wx_wet_today = sv_bool(v, "wet_today", false)
wx_wet_yesterday = sv_bool(v, "wet_yesterday", false)
wx_dice.s = sv_int(v, "dice", wx_dice.s)
if weather_kind_count() == 0 { return }
if not wx_kinds[wx_state].clear { wx_amount = 1.0 }
wx_wind = wx_kinds[wx_state].wind
}
export function weather_system() -> System {
let s = system_new("weather", PH_SIMULATE)
s.reset = fn weather_reset
s.tick = fn weather_tick
s.save = fn weather_save
s.load = fn weather_load
return s
}

View file

@ -0,0 +1,201 @@
# weather_test.ludic - spells change on their clock and by the kinds' odds, the edges ease, the
# story leans when asked, only the owner runs the spells, and the save reads back
import "ludic.weather"
import "ludic.base"
program WeatherTest {
numbers float
var owns: bool = true
var hours: float = 12.0
var bias: int = 0
var lean: []float = null
function fake_owns() -> bool { return owns }
function fake_hours() -> float { return hours }
function fake_night() -> bool { return hours < 6.0 or hours > 20.0 }
function fake_bias() -> int { return bias }
function fake_seed() -> int { return 7 }
function fake_front_hours() -> float { return 6.0 }
function fake_odds(from: int) -> []float {
if lean != null { return lean }
return weather_kind(from).odds
}
function odds3(a: float, b: float, c: float) -> []float {
let w = new []float
push(w, a)
push(w, b)
push(w, c)
return w
}
function kind(name: string, wet: bool, thunder: bool, odds: []float) -> WeatherKind {
let k = new WeatherKind
k.name = name
k.clear = not wet
k.wet = wet
k.thunder = thunder
k.odds = odds
if wet {
k.soak = 1.0
k.overcast = 0.8
k.cold = 5.0
k.wind = 1.0
}
k.spell_lo = 1.0
k.spell_hi = 3.0
return k
}
# clear, rain and storm: clear always goes to rain, rain to storm, storm to clear
function fresh() -> void {
owns = true
hours = 12.0
bias = WEATHER_NO_BIAS
lean = null
let ks = new []WeatherKind
push(ks, kind("clear", false, false, odds3(0.0, 1.0, 0.0)))
push(ks, kind("rain", true, false, odds3(0.0, 0.0, 1.0)))
push(ks, kind("storm", true, true, odds3(1.0, 0.0, 0.0)))
weather_kinds(ks)
let w = new WeatherWorld
w.owns_world = fn fake_owns
w.hours = fn fake_hours
w.is_night = fn fake_night
w.bias = fn fake_bias
w.seed = fn fake_seed
w.odds = fn fake_odds
w.front_hours = fn fake_front_hours
weather_bind(w)
weather_config_wind(0.5, 2.5, 10.0, 19.0)
weather_reset()
}
function facts() -> []WeatherFact { return q_drain(weather_facts()) }
function step(dt: float, gh: float, n: int) -> void { for i in 0 .. n { weather_run(dt, gh) } }
function count(fs: []WeatherFact, what: int) -> int {
var n = 0
for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } }
return n
}
test "a new trip is clear, dry and forecast clear" {
fresh()
expect_eq(weather_state(), 0)
expect_eq(weather_forecast(), 0)
expect(not weather_wet())
expect(weather_cold() == 0.0)
}
test "a spell that runs out gives way to the next, as a fact" {
fresh()
weather_set(0, 1, 0.1)
step(0.1, 0.2, 1)
expect_eq(weather_state(), 1)
expect_eq(weather_next(), 2)
let fs = facts()
expect_eq(count(fs, WEATHER_BEGAN), 1)
expect(weather_left() >= 1.0 and weather_left() < 3.0)
}
test "the amount eases in, and wet follows it" {
fresh()
weather_set(0, 1, 0.01)
step(0.1, 0.02, 1)
expect(not weather_wet())
step(0.1, 0.0, 200)
expect(weather_wet())
expect(weather_cold() > 4.0)
expect(weather_ground_wet() > 0.5)
}
test "a storm throws lightning, and a clearing in daylight is a fact" {
fresh()
weather_set(2, 0, 2.0)
step(1.0, 0.0, 60)
expect(count(facts(), WEATHER_THUNDER) > 0)
weather_set(2, 0, 0.05)
step(0.1, 0.1, 1)
expect_eq(weather_state(), 0)
expect(weather_after_front())
expect_eq(count(facts(), WEATHER_CLEARED), 1)
}
test "the world's odds overrule the kind's" {
fresh()
lean = odds3(0.0, 0.0, 1.0)
weather_set(0, 0, 0.01)
step(0.1, 0.1, 1)
expect_eq(weather_next(), 2)
}
test "wanting rain forecasts a front and holds it long enough" {
fresh()
bias = WEATHER_WANT_WET
step(0.1, 0.01, 1)
expect(weather_forced())
expect_eq(weather_forecast(), 1)
expect_eq(count(facts(), WEATHER_FRONT), 1)
step(0.1, 2.0, 1)
expect_eq(weather_state(), 1)
expect(not weather_forced())
expect(weather_left() >= 6.0)
}
test "wanting clear sky clears it and keeps it" {
fresh()
weather_set(1, 1, 5.0)
bias = WEATHER_WANT_CLEAR
step(0.1, 0.6, 1)
expect_eq(weather_state(), 0)
step(0.1, 0.9, 5)
expect_eq(weather_state(), 0)
}
test "a machine that does not own the world runs no spells, but is told them" {
fresh()
owns = false
weather_set(0, 1, 0.1)
step(0.1, 5.0, 3)
expect_eq(weather_state(), 0)
weather_told(2, 0, 3.0, 1.0, 0.9, 1.0, 2)
expect_eq(weather_state(), 2)
expect_eq(count(facts(), WEATHER_BEGAN), 1)
step(1.0, 0.0, 60)
expect(count(facts(), WEATHER_THUNDER) > 0)
}
test "the wind turns with the hours" {
fresh()
hours = 3.0
step(1.0, 0.0, 100)
expect(Math.abs(weather_dir() - 0.5) < 0.01)
hours = 12.0
step(1.0, 0.0, 100)
expect(Math.abs(weather_dir() - 2.5) < 0.01)
}
test "a morning after rain remembers it was wet" {
fresh()
weather_set(1, 1, 5.0)
step(0.1, 0.0, 1)
weather_morning()
expect(weather_wet_yesterday())
}
test "a save section reads back the same sky" {
fresh()
weather_set(1, 2, 4.25)
weather_morning()
let fc = weather_forecast()
let root = save_tree()
save_section(root, "weather", 1, weather_save())
let text = save_encode(root)
weather_reset()
let n = load_section(save_decode(text), "weather")
weather_load(n.data, n.version)
expect_eq(weather_state(), 1)
expect(weather_left() == 4.25)
expect_eq(weather_forecast(), fc)
expect(weather_wet_yesterday())
}
}

View file

@ -0,0 +1,68 @@
# verbs.ludic - the only way the sky changes from outside: a morning, a reset, the host's word
# a new trip: clear sky, a still-ish wind, nothing forecast; the dice from the world's seed
export function weather_reset() -> void {
var seed = 1
if wx_world != null and wx_world.seed != null { seed = wx_world.seed() }
wx_dice = rng_new(seed)
let clear = wx_clear_kind()
wx_state = clear
wx_next = clear
wx_forecast = clear
wx_left = 30.0
wx_amount = 0.0
wx_forced = false
wx_wind = 0.2
wx_dir = 0.0
wx_ground = 0.0
wx_flash = 0.0
wx_thunder_t = 0.0
wx_clear_t = 0.0
wx_wet_today = false
wx_wet_yesterday = false
q_clear(weather_facts())
}
# a new day's forecast: seven times in ten it is the next spell. A front already on its way
# stays on its way.
export function weather_morning() -> void {
if weather_kind_count() == 0 { return }
wx_wet_yesterday = wx_wet_today or wx_kinds[wx_state].wet
wx_wet_today = false
if wx_forced {
wx_forecast = wx_next
return
}
wx_forecast = wx_pick_next(wx_state)
if rng_float(wx_dice) < 0.7 { wx_next = wx_forecast }
}
# the sky outright: a spell of `kind` now, `left` hours of it, at full strength when not clear
# (staging, a test, a story beat)
export function weather_set(kind: int, next: int, left: float) -> void {
wx_state = kind
wx_next = next
wx_left = left
wx_amount = 1.0
if wx_kinds[kind].clear { wx_amount = 0.0 }
}
# what the machine that owns the world says the sky is; a new spell is a fact here too
export function weather_told(kind: int, next: int, left: float, amount: float, wind: float, dir: float, forecast: int) -> void {
let was = wx_state
wx_state = kind
wx_next = next
wx_left = left
wx_amount = amount
wx_wind = wind
wx_dir = dir
wx_forecast = forecast
if kind != was { wx_fact(WEATHER_BEGAN, kind, was) }
}
export function weather_set_wind(strength: float, dir: float) -> void {
wx_wind = strength
wx_dir = dir
}
export function weather_set_ground(wet: float) -> void { wx_ground = wet }
export function weather_set_flash(f: float) -> void { wx_flash = f }