feat(packages): ludic.wildlife - a valley's animals: species handed in as data, ranges placed against the ground and moved with the season, the day's steering, the senses and a net alert that settles, feeding, lures and snares, birds, spawning and repopulation, its own dice, facts and a save section

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 06:54:58 +03:00
parent 80f3468ff8
commit c9dc592dcc
28 changed files with 1956 additions and 0 deletions

View file

@ -0,0 +1,77 @@
# ludic.wildlife
The animals of a valley: the ranges each species keeps to, and every animal's day - wander about
home, go down to water and forage when thirst and hunger climb, notice the players and decide what
to make of them, flee, charge, come to food and lures, walk into snares; birds circle, glide down,
peck and lift off. Uses [`ludic.base`](../ludic.base/README.md) and nothing else.
```ludic
import "ludic.wildlife"
```
The package owns the animals' state, the steering, the senses, the alert, feeding and lures,
spawning within ranges, its own dice (an `Rng`, never `Random.*`) and its own save section. It
never draws anything: the game makes an actor when the port says an animal was `born` and keys its
drawing by the animal's `key`, and it never decides what anything is worth - a feeding, a catch, a
shove, a print are facts the game drains.
## Species are data
The game hands the table in, once: `wildlife_species_add(s)` per species, in its own order, so the
entry's place is the species number. A `WildSpecies` is `key`, `walk`, `run`, `flee` (its nerve,
m), `home` (how far it roams), `habitat` (`WILD_MEADOW`, `_FOREST`, `_SHORE`, `_CLIFF`), `tracks`,
`size` (0 small - a snare holds it), `diet` (`WILD_PLANTS`, `_FISH`, `_HAY`, `_SEED`), `flies`,
`tame`, `charges`, `forget` (alert points a second it lets go of), `land_shy`, `land_chance`,
`keep_away`, `range` (how wide one of its ranges is) and `scale`.
## The port
```ludic
export port WildlifeWorld { # every member has a default: a flat dry meadow at noon, one silent player
ground, water, slope: fn(float, float) -> float # the ground, the water's surface, the gradient
forest: fn(float, float) -> bool
sight: fn(ax, ay, az, bx, by, bz) -> bool # nothing between the two points
players: fn() -> int # slots; player_on(p), player_x / _y / _z(p)
hidden, noise, scent, stillness # per player: in a hide, 0..1, 0..1, WILD_MOVING / _STILL / _SITTING
hour, light, winterness, wind, wind_dir: fn() -> float # the clock, how well it sees now, the season, the weather
wariness, charge_reach: fn() -> float # every nerve times, how close a charger lets you come
taught: fn() -> bool # feeding is being taught: food carries 175 m, feeders mind less
about: fn(WildAnimal) -> int # WILD_ABOUT, WILD_AWAY (lying up) or WILD_GONE (a legend's hours ended)
ranges, per_range: fn(int) -> int # a species' ranges on this map, and how many live in each
spot: fn(WildAnimal, bool, WildSpot) -> bool # water (true) or forage for it, filled in
lures: fn() -> int # how many things to ask `lure` about
lure: fn(int, WildLure) -> bool # the i-th, filled in, when it is food, a lure or an empty snare
present: fn(int) -> bool # a spot or lure by uid is still there
next_id: fn() -> int # an animal's id across machines
born: fn(WildAnimal) -> void # draw it
placed: fn(WildRange, int) -> void # a range was set out: put its water and forage down
}
```
## API
| | |
| --- | --- |
| `wildlife_species_clear()`, `wildlife_species_add(s) -> int`, `wildlife_species(sp)`, `wildlife_species_count()` | the table |
| `WildAnimal`, `wildlife_all()`, `wildlife_count(sp)`, `wildlife_total()`, `wildlife_by_nid(nid)`, `wildlife_count_all()` | the animals; `state` is `WILD_IDLE` .. `WILD_LAND` |
| `wildlife_new(sp, x, z, legend)`, `wildlife_make(sp, x, z, legend, yaw, variant, scale, nid)`, `wildlife_remove(a)`, `wildlife_clear()` | making one off the package's dice or exactly as given (a guest's copy), and taking one away |
| `wildlife_spawn(x0, z0)`, `wildlife_morning() -> int`, `wildlife_repopulate() -> int`, `wildlife_seed(n)` | a valley filled; each morning the ranges move with the season and each range under its target gets one back, out of every player's sight |
| `WildRange`, `wildlife_ranges()`, `wildlife_range(i)`, `wildlife_range_dist(r, x, z)`, `wildlife_range_alive(i)`, `wildlife_ranges_visit(x, z)`, `wildlife_range_found_near(sp, x, z)`, `wildlife_ranges_migrate()` | ranges, found by standing in them |
| `wildlife_habitat_ok(hab, x, z)`, `wildlife_find_spot(hab, x0, z0, d0, d1)`, `wildlife_spot_x()`, `wildlife_spot_z()` | the right ground, and a spot of it |
| `wildlife_tick(dt, gh)`, `wildlife_tick_ground(a, dt, gh)`, `wildlife_tick_bird(a, dt)`, `wildlife_stride(a, dt)`, `wildlife_hold(on)` | the step; `pinned` on an animal leaves it where it stands |
| `wildlife_alert_rate(a, p, d)`, `wildlife_alert_decay(a, p)`, `wildlife_downwind(a, p)`, `wildlife_can_see(a, p)` | what it makes of player p |
| `wildlife_in_snare(uid)`, `wildlife_release(a)` | a snare's catch |
| `wildlife_saw(sp) -> bool`, `wildlife_seen(sp)`, `wildlife_seen_mask()`, `wildlife_set_seen(m)`, `wildlife_mark_fed(sp)`, `wildlife_fed_mask()`, `wildlife_fed_any()`, `wildlife_set_fed_any(b)` | this player's record |
| `wildlife_put_away()`, `wildlife_bring_back()`, `wildlife_away()` | a machine drawing another's animals keeps its own aside |
| `wildlife_facts() -> Queue<WildFact>` | `WILD_SPOTTED`, `_FLED`, `_FED` (`lure`, `owner`, `diet`), `_CAUGHT` (`lure`, `owner`), `_CHARGED`, `_SHOVED` (`player`), `_PRINT`, `_SCAT`, `_BED`, `_WENT` |
| `wildlife_system() -> System` | `"wildlife"`, `PH_SIMULATE`, no tick (the game steps it where the world is owned): reset, and its save section - the record and the found ranges |
The alert is net: what a player's sight, scent and noise add is weighed against what the animal
forgets every tick, so it SETTLES at a level the distance sets rather than climbing to 100 and
sticking there - `wildlife_test` holds where it settles, not only how it rises.
## Tests
```bash
ludic test packages/ludic.wildlife
```

View file

@ -0,0 +1,64 @@
# animal.ludic - one animal: where it is and where it lives, what it is doing, what it wants, and
# what it makes of the players. The game draws it from these and keeps its own drawing by `key`.
export const WILD_IDLE: int = 0
export const WILD_WANDER: int = 1
export const WILD_FLEE: int = 2
export const WILD_APPROACH: int = 3 # walking to food, a lure or a snare
export const WILD_EAT: int = 4
export const WILD_TRAPPED: int = 5
export const WILD_CHARGE: int = 6
export const WILD_FLY: int = 7
export const WILD_PERCH: int = 8
export const WILD_GO_DRINK: int = 9
export const WILD_GO_FEED: int = 10
export const WILD_DRINK: int = 11 # drinking or grazing at its spot
export const WILD_ALERT: int = 12 # head up, turned at what it noticed
export const WILD_WARY: int = 13 # turned away, a step or two of distance taken
export const WILD_LAND: int = 14 # a bird gliding down to a spot
export property WildAnimal {
key: int = 0 # unique for the run, never reused: the game's drawing is found by it
nid: int = 0 # its id across machines (the port's next_id)
sp: int = 0
legend: bool = false
variant: int = 0 # 0..11: which coat, the game's to read
scale: float = 1.0
alive: bool = true
shown: bool = true # about, and to be drawn
pinned: bool = false # the AI leaves it where it stands
x: float = 0.0
y: float = 0.0
z: float = 0.0
yaw: float = 0.0
speed: float = 0.0
phase: float = 0.0 # the gait, 0..2 pi, from the ground covered
fly_h: float = 0.0
bank: float = 0.0
state: int = 0
timer: float = 0.0
rg: int = -1 # the range it belongs to
home_x: float = 0.0
home_z: float = 0.0
tx: float = 0.0
tz: float = 0.0
thirst: float = 0.0 # 0..100: over 60 it goes looking for water
hunger: float = 0.0
scat_t: float = 0.0 # game minutes until it might leave sign
track_d: float = 0.0
spot: int = 0 # the uid of the water or forage it walks to, 0 none
spot_x: float = 0.0
spot_z: float = 0.0
spot_water: bool = false
lure: int = 0 # the uid of the food, lure or snare it walks to, 0 none
lure_kind: int = 0
lure_x: float = 0.0
lure_z: float = 0.0
lure_owner: int = -1
calm: float = 0.0 # game minutes of tolerating you, after being fed
fed: int = 0
alert: float = 0.0 # 0..100: what it makes of you
sus_x: float = 0.0 # where it thinks the trouble is
sus_z: float = 0.0
spook: float = 0.0 # game minutes of extra wariness after being flushed
seen: float = 0.0 # the alert it has already reacted to
}

View file

@ -0,0 +1,105 @@
# birds.ludic - a bird circles its home at its own height, comes down now and then (for seed from
# further off than for nothing), glides in rather than dropping, pecks and drinks, and lifts off
export function wildlife_tick_bird(a: WildAnimal, dt: float) -> void {
a.timer = a.timer - dt
let dh = wl_near(a.x, a.z)
if a.state == WILD_LAND {
wl_land(a, dh, dt)
return
}
if a.state == WILD_PERCH {
wl_perch(a, dh, dt)
return
}
wl_circle(a, dt)
let s = wl_sp(a.sp)
if wl_bird_seed(a) and dh > s.flee * 0.7 and wl_bird_spot_ok(wl_best.x, wl_best.z) {
let off = 1.0 + wl_rnd() * 2.0
let oa = wl_rnd() * WL_TAU
wl_come_down(a, wl_best.x + Math.sin(oa) * off, wl_best.z + Math.cos(oa) * off)
return
}
if a.timer < 0.0 and dh > s.land_shy {
let sx = a.x + (wl_rnd() - 0.5) * 24.0
let sz = a.z + (wl_rnd() - 0.5) * 24.0
if wl_rnd() < s.land_chance and wl_bird_spot_ok(sx, sz) {
wl_come_down(a, sx, sz)
return
}
}
if a.timer < 0.0 {
a.timer = 30.0
a.home_x = a.home_x + (wl_rnd() - 0.5) * 120.0
a.home_z = a.home_z + (wl_rnd() - 0.5) * 120.0
}
}
function wl_come_down(a: WildAnimal, x: float, z: float) -> void {
a.tx = x
a.tz = z
a.state = WILD_LAND
a.timer = 30.0
}
# a circle about home, climbing to its height: the eagle wide and high, the jay low
function wl_circle(a: WildAnimal, dt: float) -> void {
let s = wl_sp(a.sp)
a.fly_h = a.fly_h + (8.0 + s.home * 0.12 - a.fly_h) * Math.min(1.0, 0.4 * dt)
let r = s.home * 0.5
let ang = Math.atan2(a.z - a.home_z, a.x - a.home_x) + s.walk / r * dt
let nx = a.home_x + Math.cos(ang) * r
let nz = a.home_z + Math.sin(ang) * r
a.yaw = Math.atan2(-(nx - a.x), -(nz - a.z))
a.x = nx
a.z = nz
a.speed = s.walk
a.y = WildlifeWorld.ground(a.x, a.z) + a.fly_h
a.bank = 0.35
}
function wl_take_off(a: WildAnimal, h: float) -> void {
a.state = WILD_FLY
a.timer = 20.0 + wl_rnd() * 40.0
a.fly_h = Math.max(a.fly_h, h)
}
function wl_land(a: WildAnimal, dh: float, dt: float) -> void {
let s = wl_sp(a.sp)
a.fly_h = Math.max(0.0, a.fly_h - dt * 3.2)
a.speed = s.walk * 0.8
wl_face(a, a.tx, a.tz, 2.4, dt)
wl_step(a, dt)
a.y = WildlifeWorld.ground(a.x, a.z) + a.fly_h
a.bank = 0.2 * Math.clamp(a.fly_h / 4.0, 0.0, 1.0)
if dh < s.flee or a.timer < -20.0 {
wl_take_off(a, 0.0)
return
}
if a.fly_h < 0.15 and wl_at_target(a, 2.0) {
a.state = WILD_PERCH
a.fly_h = 0.0
a.timer = 8.0 + wl_rnd() * 16.0
a.bank = 0.0
}
}
# on the ground it pecks at seed or takes a drink at the water's edge; it leaves no droppings
function wl_perch(a: WildAnimal, dh: float, dt: float) -> void {
a.speed = 0.0
a.y = WildlifeWorld.ground(a.x, a.z)
a.bank = 0.0
if wl_bird_seed(a) and Math.sqrt((wl_best.x - a.x) * (wl_best.x - a.x) + (wl_best.z - a.z) * (wl_best.z - a.z)) < 3.0 {
a.hunger = Math.max(a.hunger - 30.0 * dt, 0.0)
a.timer = Math.max(a.timer, 1.0)
if wl_rnd() < 0.12 * dt and WildlifeWorld.present(wl_best.uid) { wl_ate(a, wl_best.uid, wl_best.owner, WILD_SEED) }
} else if wl_wet(a.x - Math.sin(a.yaw) * 2.0, a.z - Math.cos(a.yaw) * 2.0, 0.05) {
a.thirst = Math.max(a.thirst - 30.0 * dt, 0.0)
}
if a.timer < 0.0 or dh < wl_sp(a.sp).flee { wl_take_off(a, 0.5) }
}
# somewhere a bird would put down: flat and dry
function wl_bird_spot_ok(x: float, z: float) -> bool {
if wl_wet(x, z, 0.05) { return false }
return not (WildlifeWorld.slope(x, z) > 0.30)
}

View file

@ -0,0 +1,50 @@
# facts.ludic - what happened, oldest first, for the game to drain: it decides what a sighting is
# worth, who a fed animal credits, what a snare's catch does to the snare, and what a shove costs
export const WILD_SPOTTED: int = 0 # a species came within sight of player 0 for the first time
export const WILD_FLED: int = 1 # it was flushed and ran (from `player`)
export const WILD_FED: int = 2 # it ate food someone set out (`lure`, `owner`, `diet`)
export const WILD_CAUGHT: int = 3 # it walked into a snare (`lure`, `owner`)
export const WILD_CHARGED: int = 4 # it came at `player`
export const WILD_SHOVED: int = 5 # and reached them, from (x, z)
export const WILD_PRINT: int = 6 # a walker left a print at (x, z), heading `yaw`
export const WILD_SCAT: int = 7 # droppings, near enough to a player to be found
export const WILD_BED: int = 8 # a long sit left crushed grass
export const WILD_WENT: int = 9 # its hours ended and it went for good (a legend's)
export property WildFact {
what: int = 0
key: int = 0
nid: int = 0
sp: int = 0
legend: bool = false
x: float = 0.0
z: float = 0.0
yaw: float = 0.0
scale: float = 1.0
player: int = 0
lure: int = 0
owner: int = -1
diet: int = 0
}
var wl_facts: Queue<WildFact> = null
export function wildlife_facts() -> Queue<WildFact> {
if wl_facts == null { wl_facts = queue_new("wildlife.facts") }
return wl_facts
}
function wl_fact(what: int, a: WildAnimal) -> WildFact {
let f = new WildFact
f.what = what
f.key = a.key
f.nid = a.nid
f.sp = a.sp
f.legend = a.legend
f.x = a.x
f.z = a.z
f.yaw = a.yaw
f.scale = a.scale
q_push(wildlife_facts(), f)
return f
}

View file

@ -0,0 +1,89 @@
# ground.ludic - a walker's tick: its needs, the alert, then the first of charging, reacting, going
# to its spot, walking to something set out, eating, idling and wandering that has it this tick
export function wildlife_tick_ground(a: WildAnimal, dt: float, gh: float) -> void {
wl_needs(a, gh)
let dh = wl_near(a.x, a.z)
let np = wl_near_i
a.timer = a.timer - dt
if a.calm > 0.0 { a.calm = a.calm - gh * 60.0 }
if a.spook > 0.0 { a.spook = a.spook - gh * 60.0 }
wl_alert(a, np, dh, dt)
if a.state == WILD_TRAPPED {
a.speed = 0.0
return
}
if wl_charge(a, np, dh, dt) { return }
wl_posture(a, np, dt)
if wl_react(a, np, dt) { return }
if wl_seek(a, dt) { return }
if wl_drink(a, dt, gh) { return }
if wl_approach(a, dt) { return }
if wl_eat(a) { return }
let s = wl_sp(a.sp)
if a.state == WILD_IDLE {
a.speed = 0.0
if a.timer < 0.0 {
if a.timer > -6.0 and s.tracks and wl_rnd() < 0.06 { wl_fact(WILD_BED, a) } # a long sit leaves crushed grass
a.state = WILD_WANDER
a.timer = 6.0 + wl_rnd() * 10.0
wl_clamp(a)
wl_pick_target(a, s.home)
}
return
}
wl_face(a, a.tx, a.tz, 2.0, dt)
a.speed = s.walk
wl_step(a, dt)
if wl_at_target(a, 1.5) or a.timer < 0.0 {
a.state = WILD_IDLE
a.timer = 3.0 + wl_rnd() * 8.0
}
}
# a charger close enough comes at the nearest player; true while it is charging
function wl_charge(a: WildAnimal, np: int, dh: float, dt: float) -> bool {
let s = wl_sp(a.sp)
if s.charges and a.state != WILD_CHARGE and dh < WildlifeWorld.charge_reach() and not (a.calm > 0.0) {
a.state = WILD_CHARGE
a.timer = 2.5
wl_fact(WILD_CHARGED, a).player = np
}
if a.state != WILD_CHARGE { return false }
wl_face(a, WildlifeWorld.player_x(np), WildlifeWorld.player_z(np), 4.0, dt)
a.speed = s.run
wl_step(a, dt)
if dh < 2.6 {
wl_fact(WILD_SHOVED, a).player = np
wl_wander(a, 8.0)
a.calm = 30.0
}
if a.timer < 0.0 { wl_wander(a, 6.0) }
return true
}
# Four postures read off the alert. Staring is a REACTION, not a condition: each times out and
# hands the animal back to its business, and only a fresh rise above `seen` looks up again.
function wl_posture(a: WildAnimal, np: int, dt: float) -> void {
if wl_sp(a.sp).tame or a.state == WILD_EAT { return }
if a.alert > 95.0 {
if a.state != WILD_FLEE {
a.state = WILD_FLEE
a.timer = 3.0 + wl_rnd() * 3.0
a.spook = 120.0 + wl_rnd() * 180.0
a.alert = 70.0
wl_fact(WILD_FLED, a).player = np
}
} else if a.alert > 60.0 {
if a.state != WILD_WARY and a.state != WILD_FLEE and a.alert > a.seen {
a.state = WILD_WARY
a.timer = 2.0 + wl_rnd() * 2.0
}
} else if a.alert > 25.0 {
if a.state != WILD_ALERT and a.state != WILD_WARY and a.alert > a.seen {
a.state = WILD_ALERT
a.timer = 1.5 + wl_rnd() * 2.5
}
}
if a.alert > a.seen { a.seen = a.alert }
a.seen = Math.max(a.seen - 4.0 * dt, 0.0)
}

View file

@ -0,0 +1,17 @@
# guest.ludic - a machine that draws another's animals puts its own away whole, and they come back
# as they were when it stops; in between the list is the copies it is handed (wildlife_make)
var wl_own: []WildAnimal = null
export function wildlife_put_away() -> void {
if wl_own != null { return }
wl_own = wildlife_all()
wl_list = new []WildAnimal
}
export function wildlife_bring_back() -> void {
if wl_own == null { return }
wl_list = wl_own
wl_own = null
}
export function wildlife_away() -> bool { return wl_own != null }

View file

@ -0,0 +1,38 @@
# habitat.ludic - the right kind of ground for a species, asked of the port's ground, water, slope
# and forest; and a spot of it at a distance from a point, off the package's dice
var wl_px: float = 0.0
var wl_pz: float = 0.0
function wl_wet(x: float, z: float, depth: float) -> bool {
return WildlifeWorld.ground(x, z) < WildlifeWorld.water(x, z) - depth
}
export function wildlife_habitat_ok(hab: int, x: float, z: float) -> bool {
let h = WildlifeWorld.ground(x, z)
let w = WildlifeWorld.water(x, z)
if h < w + 0.8 { return false }
let s = WildlifeWorld.slope(x, z)
if hab == WILD_MEADOW { return s < 0.28 and not WildlifeWorld.forest(x, z) }
if hab == WILD_FOREST { return s < 0.5 and WildlifeWorld.forest(x, z) }
if hab == WILD_SHORE { return h < w + 4.0 and s < 0.4 }
return s > 0.25 or h > 80.0
}
# a spot of `hab` d0..d1 metres from (x0, z0); where it is, is wildlife_spot_x / _z
export function wildlife_find_spot(hab: int, x0: float, z0: float, d0: float, d1: float) -> bool {
for t in 0 .. 60 {
let ang = wl_rnd() * WL_TAU
let d = d0 + wl_rnd() * (d1 - d0)
let x = x0 + Math.cos(ang) * d
let z = z0 + Math.sin(ang) * d
if wildlife_habitat_ok(hab, x, z) {
wl_px = x
wl_pz = z
return true
}
}
return false
}
export function wildlife_spot_x() -> float { return wl_px }
export function wildlife_spot_z() -> float { return wl_pz }

View file

@ -0,0 +1,27 @@
# ludic.wildlife - species handed in by the game, ranges placed against its ground, and every animal's
# day: wander, graze, drink, notice the players, flee, come to food; what happened is a fact
module ludic_wildlife uses ludic_base
numbers float
import "ludic.base"
import "species.ludic"
import "animal.ludic"
import "places.ludic"
import "port.ludic"
import "facts.ludic"
import "store.ludic"
import "guest.ludic"
import "record.ludic"
import "habitat.ludic"
import "ranges.ludic"
import "migrate.ludic"
import "senses.ludic"
import "steer.ludic"
import "lures.ludic"
import "needs.ludic"
import "ground.ludic"
import "react.ludic"
import "want.ludic"
import "birds.ludic"
import "run.ludic"
import "spawn.ludic"
import "system.ludic"

View file

@ -0,0 +1,102 @@
# lures.ludic - what an animal would walk to: food of its diet, a lure of its diet, a snare it is
# small enough for (half the time: a curiosity), within 70 m - 175 m for food while feeding is taught
var wl_ask: WildLure = null
var wl_best: WildLure = null
var wl_seed_any: bool = false
var wl_seed_t: float = 0.0
function wl_lure_scratch() -> void {
if wl_ask == null { wl_ask = new WildLure }
if wl_best == null { wl_best = new WildLure }
}
function wl_copy_lure(from: WildLure, to: WildLure) -> void {
to.uid = from.uid
to.kind = from.kind
to.diet = from.diet
to.size = from.size
to.x = from.x
to.z = from.z
to.owner = from.owner
}
function wl_draws(a: WildAnimal, l: WildLure) -> bool {
let s = wl_sp(a.sp)
if l.kind == WILD_SNARE { return s.size <= l.size and wl_rnd() > 0.5 }
return l.diet == s.diet and l.diet != WILD_SEED
}
# the nearest thing set out that draws it, into `wl_best`; false when none does
function wl_attraction(a: WildAnimal) -> bool {
wl_lure_scratch()
var bd = 70.0
var got = false
let taught = WildlifeWorld.taught()
for i in 0 .. WildlifeWorld.lures() {
if not WildlifeWorld.lure(i, wl_ask) or not wl_draws(a, wl_ask) { continue }
let dx = wl_ask.x - a.x
let dz = wl_ask.z - a.z
var d = Math.sqrt(dx * dx + dz * dz)
if taught and wl_ask.kind == WILD_FEED { d = d * 0.4 }
if d < bd {
bd = d
got = true
wl_copy_lure(wl_ask, wl_best)
}
}
return got
}
function wl_take_lure(a: WildAnimal) -> void {
a.lure = wl_best.uid
a.lure_kind = wl_best.kind
a.lure_x = wl_best.x
a.lure_z = wl_best.z
a.lure_owner = wl_best.owner
a.state = WILD_APPROACH
}
# Is there ANY seed down? Asked once a second for the whole flock rather than per bird per tick.
function wl_seed_scan(dt: float) -> void {
wl_seed_t = wl_seed_t - dt
if wl_seed_t > 0.0 { return }
wl_seed_t = 1.0
wl_seed_any = false
wl_lure_scratch()
for i in 0 .. WildlifeWorld.lures() {
if WildlifeWorld.lure(i, wl_ask) and wl_ask.kind == WILD_FEED and wl_ask.diet == WILD_SEED {
wl_seed_any = true
return
}
}
}
# the nearest seed within 120 m, into `wl_best`: the one thing that brings an animal TO you
function wl_bird_seed(a: WildAnimal) -> bool {
if not wl_seed_any { return false }
wl_lure_scratch()
var bd = 120.0
var got = false
for i in 0 .. WildlifeWorld.lures() {
if not WildlifeWorld.lure(i, wl_ask) or wl_ask.kind != WILD_FEED or wl_ask.diet != WILD_SEED { continue }
let dx = wl_ask.x - a.x
let dz = wl_ask.z - a.z
let d = Math.sqrt(dx * dx + dz * dz)
if d < bd {
bd = d
got = true
wl_copy_lure(wl_ask, wl_best)
}
}
return got
}
# it ate what was set out: calm for three game hours, and the game credits whoever left it
function wl_ate(a: WildAnimal, uid: int, owner: int, diet: int) -> void {
a.calm = 180.0
a.fed += 1
let f = wl_fact(WILD_FED, a)
f.lure = uid
f.owner = owner
f.diet = diet
}

View file

@ -0,0 +1,66 @@
# migrate.ludic - each morning the ranges slide toward where the season wants them, and the animals
# that live in them are dragged along rather than teleported; an animal keeps inside its own
export function wildlife_ranges_migrate() -> void {
let rs = wildlife_ranges()
let w = WildlifeWorld.winterness()
for i in 0 .. len(rs) {
rs[i].x = Math.lerp(rs[i].sx, rs[i].wx, w)
rs[i].z = Math.lerp(rs[i].sz, rs[i].wz, w)
}
let all = wildlife_all()
for i in 0 .. len(all) {
let a = all[i]
let r = wildlife_range(a.rg)
if r == null { continue }
wl_clamp(a)
if wildlife_range_dist(r, a.home_x, a.home_z) > r.r * 1.2 {
a.home_x = r.x
a.home_z = r.z
a.x = r.x
a.z = r.z
a.y = WildlifeWorld.ground(a.x, a.z)
}
}
}
# its home kept inside its range
function wl_clamp(a: WildAnimal) -> void {
let r = wildlife_range(a.rg)
if r == null { return }
let d = wildlife_range_dist(r, a.home_x, a.home_z)
if d < r.r { return }
a.home_x = r.x + (a.home_x - r.x) / d * (r.r * 0.9)
a.home_z = r.z + (a.home_z - r.z) / d * (r.r * 0.9)
}
# how many live in range `i`, legends apart
export function wildlife_range_alive(i: int) -> int {
var n = 0
let all = wildlife_all()
for k in 0 .. len(all) { if all[k].alive and not all[k].legend and all[k].rg == i { n += 1 } }
return n
}
# how many are alive in the whole valley, legends apart
export function wildlife_total() -> int {
var n = 0
let all = wildlife_all()
for k in 0 .. len(all) { if all[k].alive and not all[k].legend { n += 1 } }
return n
}
# the nearest range of `sp` a player has found, to (x, z), or -1
export function wildlife_range_found_near(sp: int, x: float, z: float) -> int {
var best = -1
var bd = 99999.0
let rs = wildlife_ranges()
for i in 0 .. len(rs) {
if rs[i].sp != sp or not rs[i].found { continue }
let d = wildlife_range_dist(rs[i], x, z)
if d < bd {
bd = d
best = i
}
}
return best
}

View file

@ -0,0 +1,22 @@
# needs.ludic - thirst and hunger climb with the game hours (midday is thirsty work, a run costs more
# than a graze, dawn and dusk are when they go down to drink), and a walker leaves droppings
function wl_needs(a: WildAnimal, gh: float) -> void {
let h = WildlifeWorld.hour()
var thirst = 8.0
var hunger = 5.0
if h > 11.0 and h < 16.0 { thirst = 12.0 }
if a.state == WILD_FLEE or a.state == WILD_CHARGE {
thirst = thirst * 3.0
hunger = hunger * 2.0
}
if (h > 5.0 and h < 8.0) or (h > 18.0 and h < 21.0) { thirst = thirst * 1.8 }
a.thirst = Math.min(a.thirst + thirst * gh, 100.0)
a.hunger = Math.min(a.hunger + hunger * gh, 100.0)
if not wl_sp(a.sp).tracks { return }
a.scat_t = a.scat_t - gh * 60.0
if a.scat_t > 0.0 { return }
a.scat_t = 120.0 + wl_rnd() * 300.0
wl_near(a.x, a.z)
if wl_pdist(wl_near_i, a) > 500.0 { return } # further than 500 m from everyone: nobody would find it
wl_fact(WILD_SCAT, a)
}

View file

@ -0,0 +1,5 @@
# ludic.wildlife - the animals of a valley: where they live, what they do all day, what they make of
# the players, and what they come to. Uses ludic.base and nothing else.
package "ludic.wildlife"
version "0.1.0"
kind source

View file

@ -0,0 +1,38 @@
# places.ludic - the places an animal's day is made of: its range, the water and forage it walks
# to (the game's, found through the port), and food, lures and snares players set out
export const WILD_FEED: int = 0 # food put down: eaten, and the one who left it is credited
export const WILD_LURE: int = 1 # a lure: it comes, stands curious a while, and drifts off
export const WILD_SNARE: int = 2 # a snare: a small enough animal walking onto it is held
# a disc a species keeps to, with an address for each end of the year: `x`, `z` is where it is
# today, the summer and winter centres lerped by the season (WildlifeWorld.winterness)
export property WildRange {
sp: int = 0
x: float = 0.0
z: float = 0.0
r: float = 0.0
found: bool = false # a player has been inside it
sx: float = 0.0
sz: float = 0.0
wx: float = 0.0
wz: float = 0.0
}
# water or forage the game offers an animal (WildlifeWorld.spot fills one)
export property WildSpot {
uid: int = 0
x: float = 0.0
z: float = 0.0
water: bool = false
}
# something set out (WildlifeWorld.lure fills one): what it is, what eats it, where, whose
export property WildLure {
uid: int = 0
kind: int = 0 # WILD_FEED, WILD_LURE, WILD_SNARE
diet: int = 0 # the diet it draws (WILD_PLANTS .. WILD_SEED)
size: int = 0 # a snare: the largest size it holds
x: float = 0.0
z: float = 0.0
owner: int = -1 # whoever set it out; the game decides what it means
}

View file

@ -0,0 +1,73 @@
# port.ludic - what the animals ask the world. Every member has a default: unbound, the ground is a
# flat dry meadow at 0 in the middle of a summer's day, one player stands at the origin making no
# sound, nothing is set out and nothing blocks a look.
export const WILD_MOVING: int = 0 # a player's stillness
export const WILD_STILL: int = 1
export const WILD_SITTING: int = 2
export const WILD_ABOUT: int = 0 # what an animal is doing at this hour (WildlifeWorld.about)
export const WILD_AWAY: int = 1 # lying up: not drawn, not ticked
export const WILD_GONE: int = 2 # gone for good: a fact says so
export port WildlifeWorld {
ground: fn(float, float) -> float = fn wl_zero2
water: fn(float, float) -> float = fn wl_dry # the water's surface there
slope: fn(float, float) -> float = fn wl_zero2
forest: fn(float, float) -> bool = fn wl_no2
sight: fn(float, float, float, float, float, float) -> bool = fn wl_clear # nothing between
players: fn() -> int = fn wl_one # player slots
player_on: fn(int) -> bool = fn wl_first
player_x: fn(int) -> float = fn wl_zero1
player_y: fn(int) -> float = fn wl_zero1
player_z: fn(int) -> float = fn wl_zero1
hidden: fn(int) -> bool = fn wl_no1 # in a hide: nothing sees them
noise: fn(int) -> float = fn wl_zero1 # 0..1
scent: fn(int) -> float = fn wl_zero1 # 0..1
stillness: fn(int) -> int = fn wl_moving # WILD_MOVING, _STILL, _SITTING
hour: fn() -> float = fn wl_noon
light: fn() -> float = fn wl_one_f # how well it sees now: 1 by day
winterness: fn() -> float = fn wl_zero0 # 0 full summer .. 1 full winter
wind: fn() -> float = fn wl_zero0 # 0..1
wind_dir: fn() -> float = fn wl_zero0 # the yaw the wind blows toward
wariness: fn() -> float = fn wl_one_f # every nerve, times
charge_reach: fn() -> float = fn wl_nine # how close a charger lets you come, m
taught: fn() -> bool = fn wl_no0 # food carries further, feeders mind less
about: fn(WildAnimal) -> int = fn wl_about # WILD_ABOUT, _AWAY or _GONE
ranges: fn(int) -> int = fn wl_two # ranges a species gets on this map
per_range: fn(int) -> int = fn wl_three # and how many live in each
spot: fn(WildAnimal, bool, WildSpot) -> bool = fn wl_no_spot # water (true) or forage for it
lures: fn() -> int = fn wl_none0 # how many things to ask `lure` about
lure: fn(int, WildLure) -> bool = fn wl_no_lure # the i-th, if it is one and set out
present: fn(int) -> bool = fn wl_yes1 # a spot or lure by uid is still there
next_id: fn() -> int = fn wl_next_id
born: fn(WildAnimal) -> void = fn wl_nothing # a new animal: draw it
placed: fn(WildRange, int) -> void = fn wl_range_nothing # a range was set out
}
function wl_zero0() -> float { return 0.0 }
function wl_zero1(p: int) -> float { return 0.0 }
function wl_zero2(x: float, z: float) -> float { return 0.0 }
function wl_dry(x: float, z: float) -> float { return -1000.0 }
function wl_no0() -> bool { return false }
function wl_no1(p: int) -> bool { return false }
function wl_no2(x: float, z: float) -> bool { return false }
function wl_yes1(u: int) -> bool { return true }
function wl_clear(ax: float, ay: float, az: float, bx: float, by: float, bz: float) -> bool { return true }
function wl_one() -> int { return 1 }
function wl_none0() -> int { return 0 }
function wl_first(p: int) -> bool { return p == 0 }
function wl_moving(p: int) -> int { return WILD_MOVING }
function wl_noon() -> float { return 12.0 }
function wl_one_f() -> float { return 1.0 }
function wl_nine() -> float { return 9.0 }
function wl_about(a: WildAnimal) -> int { return WILD_ABOUT }
function wl_two(sp: int) -> int { return 2 }
function wl_three(sp: int) -> int { return 3 }
function wl_no_spot(a: WildAnimal, water: bool, s: WildSpot) -> bool { return false }
function wl_no_lure(i: int, l: WildLure) -> bool { return false }
function wl_nothing(a: WildAnimal) -> void { }
function wl_range_nothing(r: WildRange, i: int) -> void { }
var wl_ids: int = 0
function wl_next_id() -> int {
wl_ids += 1
return wl_ids
}

View file

@ -0,0 +1,97 @@
# ranges.ludic - a species keeps to discs placed once, against the ground, 120-700 m out from where
# the valley starts; each winters lower, toward the timber, as far down as it summered high
var wl_ranges: []WildRange = null
var wl_found: []int = null # found flags a load brought before the ranges were set out
export function wildlife_ranges() -> []WildRange {
if wl_ranges == null { wl_ranges = new []WildRange }
return wl_ranges
}
export function wildlife_range(i: int) -> WildRange {
if i < 0 or i >= len(wildlife_ranges()) { return null }
return wildlife_ranges()[i]
}
export function wildlife_range_dist(r: WildRange, x: float, z: float) -> float {
let dx = r.x - x
let dz = r.z - z
return Math.sqrt(dx * dx + dz * dz)
}
function wl_range_clashes(sp: int, x: float, z: float) -> bool {
let rs = wildlife_ranges()
for j in 0 .. len(rs) { if rs[j].sp == sp and wildlife_range_dist(rs[j], x, z) < wl_sp(sp).range * 1.6 { return true } }
return false
}
function wl_range_try(sp: int, x0: float, z0: float) -> bool {
let s = wl_sp(sp)
for tries in 0 .. 80 {
if not wildlife_find_spot(s.habitat, x0, z0, 120.0, 700.0) { continue }
let dx = wl_px - x0
let dz = wl_pz - z0
if Math.sqrt(dx * dx + dz * dz) < s.keep_away or wl_range_clashes(sp, wl_px, wl_pz) { continue }
let r = new WildRange
r.sp = sp
r.x = wl_px
r.z = wl_pz
r.r = s.range
r.sx = wl_px
r.sz = wl_pz
wl_pick_winter(r)
push(wildlife_ranges(), r)
return true
}
return false
}
# every species' ranges about (x0, z0), then the game told of each (WildlifeWorld.placed)
export function wildlife_ranges_setup(x0: float, z0: float) -> void {
wildlife_seed(613)
wl_ranges = new []WildRange
for sp in 0 .. wildlife_species_count() {
for k in 0 .. WildlifeWorld.ranges(sp) { if not wl_range_try(sp, x0, z0) { break } }
}
wl_found_apply()
let rs = wildlife_ranges()
for i in 0 .. len(rs) { WildlifeWorld.placed(rs[i], i) }
}
# downhill, toward the timber, never into the water: shelter is worth 25 m of height
function wl_pick_winter(r: WildRange) -> void {
let up = WildlifeWorld.ground(r.sx, r.sz) - WildlifeWorld.water(r.sx, r.sz)
let reach = Math.min(60.0 + up * 6.0, 520.0)
r.wx = r.sx
r.wz = r.sz
var best = up
for k in 0 .. 24 {
let ang = float(k) * Math.deg_to_rad(15.0)
for step in 1 .. 5 {
let d = reach * (float(step) / 4.0)
let px = r.sx + Math.cos(ang) * d
let pz = r.sz + Math.sin(ang) * d
let h = WildlifeWorld.ground(px, pz) - WildlifeWorld.water(px, pz)
if h < 3.0 or WildlifeWorld.slope(px, pz) > 0.6 { continue }
var score = h
if WildlifeWorld.forest(px, pz) { score = score - 25.0 }
if score < best {
best = score
r.wx = px
r.wz = pz
}
}
}
}
# a player stood at (x, z): any range they are inside is found
export function wildlife_ranges_visit(x: float, z: float) -> void {
let rs = wildlife_ranges()
for i in 0 .. len(rs) { if not rs[i].found and wildlife_range_dist(rs[i], x, z) < rs[i].r { rs[i].found = true } }
}
function wl_found_apply() -> void {
if wl_found == null { return }
let rs = wildlife_ranges()
for i in 0 .. len(rs) { if i < len(wl_found) { rs[i].found = wl_found[i] != 0 } }
}

View file

@ -0,0 +1,79 @@
# react.ludic - alert (it stops and looks: the window the camera wants), wary (it puts ground between
# you without running) and fleeing; true when it is doing one of them, which is all it does this tick
function wl_react(a: WildAnimal, np: int, dt: float) -> bool {
let s = wl_sp(a.sp)
if a.state == WILD_ALERT {
a.speed = 0.0
wl_face(a, a.sus_x, a.sus_z, 2.2, dt)
if a.timer < 0.0 { wl_wander(a, 3.0 + wl_rnd() * 4.0) }
return true
}
if a.state == WILD_WARY {
wl_face(a, a.x + (a.x - a.sus_x), a.z + (a.z - a.sus_z), 2.6, dt)
a.speed = s.walk * 0.8
wl_step(a, dt)
if a.timer < 0.0 { wl_wander(a, 3.0 + wl_rnd() * 4.0) }
return true
}
if a.state == WILD_FLEE {
wl_face(a, a.x + (a.x - WildlifeWorld.player_x(np)), a.z + (a.z - WildlifeWorld.player_z(np)), 5.0, dt)
a.speed = s.run
wl_step(a, dt)
if a.timer < 0.0 {
wl_rehome(a)
wl_wander(a, 5.0)
}
return true
}
return false
}
# walking to (or reached) something set out; true while it is
function wl_approach(a: WildAnimal, dt: float) -> bool {
if (a.state == WILD_IDLE or a.state == WILD_WANDER) and wl_attraction(a) { wl_take_lure(a) }
if a.state != WILD_APPROACH { return false }
if a.lure == 0 or not WildlifeWorld.present(a.lure) {
a.lure = 0
a.state = WILD_IDLE
a.timer = 2.0
return true
}
wl_face(a, a.lure_x, a.lure_z, 3.0, dt)
a.speed = wl_sp(a.sp).walk
let dx = a.lure_x - a.x
let dz = a.lure_z - a.z
if Math.sqrt(dx * dx + dz * dz) < 1.3 {
a.speed = 0.0
wl_arrive(a)
return true
}
wl_step(a, dt)
return true
}
# at a snare it is held; at food it eats for 14 s; at a lure it stands curious for 9
function wl_arrive(a: WildAnimal) -> void {
a.state = WILD_EAT
a.timer = 9.0
if a.lure_kind == WILD_FEED { a.timer = 14.0 }
if a.lure_kind != WILD_SNARE { return }
a.state = WILD_TRAPPED
a.x = a.lure_x
a.z = a.lure_z
a.y = WildlifeWorld.ground(a.x, a.z)
let f = wl_fact(WILD_CAUGHT, a)
f.lure = a.lure
f.owner = a.lure_owner
}
# eating what it came to; when it is done, food is credited and it wanders off
function wl_eat(a: WildAnimal) -> bool {
if a.state != WILD_EAT { return false }
a.speed = 0.0
if a.timer < 0.0 {
if a.lure != 0 and a.lure_kind == WILD_FEED and WildlifeWorld.present(a.lure) { wl_ate(a, a.lure, a.lure_owner, wl_sp(a.sp).diet) }
a.lure = 0
wl_wander(a, 4.0)
}
return true
}

View file

@ -0,0 +1,34 @@
# record.ludic - what this player has met: the species seen up close and the species fed, as masks
# by species number, and whether anything has been fed at all
var wl_seen: int = 0
var wl_fed: int = 0
var wl_fed_any: bool = false
export function wildlife_seen(sp: int) -> bool { return (wl_seen & (1 << sp)) != 0 }
export function wildlife_seen_mask() -> int { return wl_seen }
export function wildlife_fed_mask() -> int { return wl_fed }
export function wildlife_fed_any() -> bool { return wl_fed_any }
# seen up close (a guest's own pass, droppings read, a sighting here); true the first time
export function wildlife_saw(sp: int) -> bool {
if wildlife_seen(sp) { return false }
wl_seen = wl_seen | (1 << sp)
return true
}
export function wildlife_set_seen(mask: int) -> void { wl_seen = mask }
# a species fed, credited to this player
export function wildlife_mark_fed(sp: int) -> void {
wl_fed = wl_fed | (1 << sp)
wl_fed_any = true
}
# nothing fed yet (a test that feeds again), or the flag as given
export function wildlife_set_fed_any(on: bool) -> void { wl_fed_any = on }
function wl_record_reset() -> void {
wl_seen = 0
wl_fed = 0
wl_fed_any = false
}

View file

@ -0,0 +1,58 @@
# run.ludic - one step of every animal: the port says whether it is about at this hour; anything
# within 900 m of a player thinks; the gait follows the ground covered; player 0 sees what is near
export function wildlife_tick(dt: float, gh: float) -> void {
wl_seed_scan(dt)
let all = wildlife_all()
for i in 0 .. len(all) { wl_tick_one(all[i], dt, gh) }
}
function wl_tick_one(a: WildAnimal, dt: float, gh: float) -> void {
if not a.alive { return }
let about = WildlifeWorld.about(a)
if about == WILD_AWAY {
a.shown = false
return
}
if about == WILD_GONE {
wildlife_remove(a)
wl_fact(WILD_WENT, a)
return
}
a.shown = true
let dh = wl_near(a.x, a.z)
if dh > 900.0 { return }
let s = wl_sp(a.sp)
if wl_hold { }
else if a.pinned { a.speed = 0.0 }
else if s.flies { wildlife_tick_bird(a, dt) } else { wildlife_tick_ground(a, dt, gh) }
var stride = 1.2 * a.scale
if s.flies { stride = 0.6 }
a.phase = (a.phase + a.speed * dt / stride * WL_TAU) % WL_TAU
if WildlifeWorld.player_on(0) and wl_pdist(0, a) < 60.0 and wildlife_saw(a.sp) { wl_fact(WILD_SPOTTED, a) }
}
# the gait advanced by a copy the game moves itself (a guest's copy of the host's animal)
export function wildlife_stride(a: WildAnimal, dt: float) -> void {
var stride = 1.2 * a.scale
if wl_sp(a.sp).flies { stride = 0.6 }
a.phase = (a.phase + a.speed * dt / stride * WL_TAU) % WL_TAU
}
# a snare's catch let go: it bolts, and it holds nothing against anyone
export function wildlife_release(a: WildAnimal) -> void {
if a == null { return }
a.state = WILD_FLEE
a.timer = 4.0
a.calm = 0.0
a.lure = 0
}
# the animal a snare holds, by the snare's uid
export function wildlife_in_snare(uid: int) -> WildAnimal {
let all = wildlife_all()
for i in 0 .. len(all) {
let a = all[i]
if a.alive and a.state == WILD_TRAPPED and a.lure == uid { return a }
}
return null
}

View file

@ -0,0 +1,97 @@
# senses.ludic - what an animal makes of one player: sight (a clear line, closer is worse, the dark
# helps), scent (downwind, further than sight), noise; the rise is NET of how fast it forgets, so
# an alert settles where the distance puts it instead of climbing to the top and sticking
var wl_near_i: int = 0
# the nearest player's distance; `wl_near_i` says who
function wl_near(x: float, z: float) -> float {
var best = 999999999999.0
wl_near_i = 0
for p in 0 .. WildlifeWorld.players() {
if not WildlifeWorld.player_on(p) { continue }
let d = wl_pdist2(p, x, z)
if d < best {
best = d
wl_near_i = p
}
}
return Math.sqrt(best)
}
function wl_pdist2(p: int, x: float, z: float) -> float {
let dx = WildlifeWorld.player_x(p) - x
let dz = WildlifeWorld.player_z(p) - z
return dx * dx + dz * dz
}
function wl_pdist(p: int, a: WildAnimal) -> float { return Math.sqrt(wl_pdist2(p, a.x, a.z)) }
# -1 (it is upwind of player p: it cannot smell them) .. 1 (straight downwind, it gets everything)
export function wildlife_downwind(a: WildAnimal, p: int) -> float {
let dx = a.x - WildlifeWorld.player_x(p)
let dz = a.z - WildlifeWorld.player_z(p)
let d = Math.max(Math.sqrt(dx * dx + dz * dz), 0.01)
let dir = WildlifeWorld.wind_dir()
return dx / d * -Math.sin(dir) + dz / d * -Math.cos(dir)
}
export function wildlife_can_see(a: WildAnimal, p: int) -> bool {
if WildlifeWorld.hidden(p) { return false }
return WildlifeWorld.sight(a.x, a.y + 0.9, a.z, WildlifeWorld.player_x(p), WildlifeWorld.player_y(p) + 0.9, WildlifeWorld.player_z(p))
}
# 1 at the animal, 0 at `reach` and beyond, squared: the far half of a range is almost free
function wl_near_sq(d: float, reach: float) -> float {
let n = Math.max(1.0 - d / reach, 0.0)
return n * n
}
# alert points a second from player p at `dh` metres; 0 when nothing of them reaches it
export function wildlife_alert_rate(a: WildAnimal, p: int, dh: float) -> float {
let s = wl_sp(a.sp)
if s.tame or a.calm > 0.0 { return 0.0 }
if (a.state == WILD_APPROACH or a.state == WILD_EAT) and a.lure != 0 and a.lure_kind == WILD_FEED and dh > 5.0 and WildlifeWorld.taught() { return 0.0 }
var reach = s.flee * WildlifeWorld.wariness()
if a.spook > 0.0 { reach = reach * 1.25 }
var rate = 0.0
if dh < reach * 1.9 and wildlife_can_see(a, p) { rate = rate + 55.0 * wl_near_sq(dh, reach * 1.9) * WildlifeWorld.light() }
let wind = wildlife_downwind(a, p)
let ws = WildlifeWorld.wind()
if wind > 0.0 and ws > 0.08 and dh < reach * 3.2 {
rate = rate + 30.0 * wl_near_sq(dh, reach * 3.2) * wind * (WildlifeWorld.scent(p) * Math.min(1.0, 0.4 + ws))
}
let noise = WildlifeWorld.noise(p)
if noise > 0.12 and dh < reach * 1.5 * noise { rate = rate + 40.0 * wl_near_sq(dh, reach * 1.5 * noise) * noise }
return rate
}
# how fast it forgets player p: standing still buys it, sitting down buys more
export function wildlife_alert_decay(a: WildAnimal, p: int) -> float {
var d = wl_sp(a.sp).forget
let still = WildlifeWorld.stillness(p)
if still == WILD_SITTING { d = d * 2.5 } else if still == WILD_STILL { d = d * 1.6 }
if a.spook > 0.0 { d = d * 0.7 }
return d
}
# the alert rises and falls with the player who nets it the most, and it watches them
function wl_alert(a: WildAnimal, np: int, dh: float, dt: float) -> void {
var rise = wildlife_alert_rate(a, np, dh)
var fall = wildlife_alert_decay(a, np)
var who = np
for p in 0 .. WildlifeWorld.players() {
if p == np or not WildlifeWorld.player_on(p) { continue }
let r2 = wildlife_alert_rate(a, p, wl_pdist(p, a))
let f2 = wildlife_alert_decay(a, p)
if r2 - f2 > rise - fall {
rise = r2
fall = f2
who = p
}
}
if rise > 0.05 {
a.sus_x = WildlifeWorld.player_x(who)
a.sus_z = WildlifeWorld.player_z(who)
}
a.alert = Math.clamp(a.alert + (rise - fall) * dt, 0.0, 100.0)
}

View file

@ -0,0 +1,52 @@
# spawn.ludic - a valley filled: ranges set out, then each range to its target on its own habitat.
# Each morning the ranges move with the season and every range under its target gets ONE back,
# out of every player's sight: a valley recovers, it does not refill.
export function wildlife_spawn(x0: float, z0: float) -> void {
wildlife_ranges_setup(x0, z0)
wildlife_seed(97)
let rs = wildlife_ranges()
for i in 0 .. len(rs) { wl_fill(i, WildlifeWorld.per_range(rs[i].sp)) }
wildlife_ranges_migrate()
}
# up to `want` more in range i; returns how many went in
function wl_fill(i: int, want: int) -> int {
let r = wildlife_range(i)
var made = 0
for k in 0 .. want {
if not wildlife_find_spot(wl_sp(r.sp).habitat, r.x, r.z, 0.0, r.r) { continue }
let a = wildlife_new(r.sp, wl_px, wl_pz, false)
if a != null {
a.rg = i
made += 1
}
}
return made
}
# each morning: the ranges slide with the season, and the valley gets back what it lost
export function wildlife_morning() -> int {
wildlife_ranges_migrate()
return wildlife_repopulate()
}
# one newcomer for each range under its target, never within 200 m of a player; how many came
export function wildlife_repopulate() -> int {
let rs = wildlife_ranges()
var added = 0
for i in 0 .. len(rs) {
let want = WildlifeWorld.per_range(rs[i].sp)
if want == 0 or wildlife_range_alive(i) >= want { continue }
for t in 0 .. 12 {
if not wildlife_find_spot(wl_sp(rs[i].sp).habitat, rs[i].x, rs[i].z, 0.0, rs[i].r) { continue }
if wl_near(wl_px, wl_pz) < 200.0 { continue }
let a = wildlife_new(rs[i].sp, wl_px, wl_pz, false)
if a != null {
a.rg = i
added += 1
}
break
}
}
return added
}

View file

@ -0,0 +1,57 @@
# species.ludic - what a kind of animal IS, as far as its day goes: the game hands the table in,
# one entry per species, and the entry's place in it is the species' number
export const WILD_MEADOW: int = 0
export const WILD_FOREST: int = 1
export const WILD_SHORE: int = 2
export const WILD_CLIFF: int = 3
# what it eats, and so what food and which lures it comes to
export const WILD_PLANTS: int = 0
export const WILD_FISH: int = 1
export const WILD_HAY: int = 2
export const WILD_SEED: int = 3
export property WildSpecies {
key: string = ""
walk: float = 1.0 # m/s
run: float = 1.0 # m/s
flee: float = 30.0 # its nerve: how far off a player starts to reach it, m
home: float = 60.0 # how far it roams about its home, m
habitat: int = 0 # WILD_MEADOW .. WILD_CLIFF
tracks: bool = false # a walker that leaves prints (and droppings, and beds)
size: int = 0 # 0 small (a snare holds it), 1 medium, 2 large
diet: int = 0 # WILD_PLANTS .. WILD_SEED
flies: bool = false # a bird: it circles, lands and lifts off
tame: bool = false # minds nobody (a horse)
charges: bool = false # comes at a player who gets too close (a bear)
forget: float = 2.5 # alert points a second it lets go of
land_shy: float = 80.0 # a bird: how close it comes down with somebody about, m
land_chance: float = 0.1 # and how readily it comes down at all
keep_away: float = 60.0 # how far its ranges keep from where the valley starts, m
range: float = 100.0 # how wide one of its ranges is, m
scale: float = 1.0 # its size, times
}
var wl_species: []WildSpecies = null
function wl_sp_all() -> []WildSpecies {
if wl_species == null { wl_species = new []WildSpecies }
return wl_species
}
export function wildlife_species_clear() -> void { wl_species = new []WildSpecies }
# the next species; returns its number
export function wildlife_species_add(s: WildSpecies) -> int {
push(wl_sp_all(), s)
return len(wl_sp_all()) - 1
}
export function wildlife_species_count() -> int { return len(wl_sp_all()) }
export function wildlife_species(sp: int) -> WildSpecies {
if sp < 0 or sp >= len(wl_sp_all()) { return null }
return wl_sp_all()[sp]
}
function wl_sp(sp: int) -> WildSpecies { return wl_sp_all()[sp] }

View file

@ -0,0 +1,64 @@
# steer.ludic - turning toward a point at a rate, a step along the facing that turns away from water
# and a climb, and a target somewhere about home; a walker leaves a print every 2.8 m
function wl_wrap(a: float) -> float {
var r = a
while r > 3.1415927 { r = r - WL_TAU }
while r < -3.1415927 { r = r + WL_TAU }
return r
}
function wl_face(a: WildAnimal, tx: float, tz: float, rate: float, dt: float) -> void {
let want = Math.atan2(-(tx - a.x), -(tz - a.z))
let r = rate * dt
a.yaw = a.yaw + Math.clamp(wl_wrap(want - a.yaw), -r, r)
}
# true when it moved
function wl_step(a: WildAnimal, dt: float) -> bool {
let d = a.speed * dt
if d < 0.0001 { return false }
let nx = a.x - Math.sin(a.yaw) * d
let nz = a.z - Math.cos(a.yaw) * d
let nh = WildlifeWorld.ground(nx, nz)
if nh < WildlifeWorld.water(nx, nz) + 0.6 or nh - a.y > d * 1.2 {
a.yaw = a.yaw + Math.deg_to_rad(75.0)
a.timer = 0.0
return false
}
a.x = nx
a.z = nz
a.y = nh
a.track_d = a.track_d + d
if wl_sp(a.sp).tracks and a.track_d > 2.8 {
a.track_d = 0.0
wl_fact(WILD_PRINT, a)
}
return true
}
function wl_pick_target(a: WildAnimal, radius: float) -> void {
let ang = wl_rnd() * WL_TAU
let d = wl_rnd() * radius
a.tx = a.home_x + Math.cos(ang) * d
a.tz = a.home_z + Math.sin(ang) * d
}
function wl_at_target(a: WildAnimal, r: float) -> bool {
let dx = a.tx - a.x
let dz = a.tz - a.z
return dx * dx + dz * dz < r * r
}
# back to wandering about home after `t` seconds of whatever it was doing
function wl_wander(a: WildAnimal, t: float) -> void {
a.state = WILD_WANDER
a.timer = t
wl_pick_target(a, wl_sp(a.sp).home)
}
# home where it stands now, kept inside its range
function wl_rehome(a: WildAnimal) -> void {
a.home_x = a.x
a.home_z = a.z
wl_clamp(a)
}

View file

@ -0,0 +1,95 @@
# store.ludic - the animals, their own dice, and the verbs that make and take one away
var wl_list: []WildAnimal = null
var wl_keys: int = 0
var wl_dice: Rng = null
var wl_hold: bool = false # staged: nobody thinks, everybody keeps their speed
const WL_TAU: float = 6.2831853
export function wildlife_all() -> []WildAnimal {
if wl_list == null { wl_list = new []WildAnimal }
return wl_list
}
export function wildlife_count_all() -> int { return len(wildlife_all()) }
# a new valley: every animal forgotten, quietly (the game has already let its drawings go)
export function wildlife_clear() -> void { wl_list = new []WildAnimal }
function wl_rng() -> Rng {
if wl_dice == null { wl_dice = rng_new(97) }
return wl_dice
}
function wl_rnd() -> float { return rng_float(wl_rng()) }
# the package's dice from a known place (a spawn, a test)
export function wildlife_seed(seed: int) -> void { rng_seed(wl_rng(), seed) }
# an individual exactly as given: the host's own, or the host's on a guest
export function wildlife_make(sp: int, x: float, z: float, legend: bool, yaw: float, variant: int, scale: float, nid: int) -> WildAnimal {
let s = wildlife_species(sp)
if s == null { return null }
let a = new WildAnimal
wl_keys += 1
a.key = wl_keys
a.nid = nid
a.sp = sp
a.x = x
a.z = z
a.home_x = x
a.home_z = z
a.tx = x
a.tz = z
a.y = WildlifeWorld.ground(x, z)
a.yaw = yaw
a.variant = variant
a.scale = scale
a.legend = legend
a.state = WILD_IDLE
push(wildlife_all(), a)
WildlifeWorld.born(a)
return a
}
# a new individual of a species at (x, z), off the package's dice
export function wildlife_new(sp: int, x: float, z: float, legend: bool) -> WildAnimal {
let s = wildlife_species(sp)
if s == null { return null }
let yaw = wl_rnd() * WL_TAU
let variant = rng_between(wl_rng(), 0, 11)
let scale = s.scale * (0.88 + wl_rnd() * 0.24)
let a = wildlife_make(sp, x, z, legend, yaw, variant, scale, WildlifeWorld.next_id())
a.thirst = wl_rnd() * 50.0
a.hunger = wl_rnd() * 50.0
a.scat_t = 40.0 + wl_rnd() * 180.0
if s.flies {
a.state = WILD_FLY
a.fly_h = 10.0 + wl_rnd() * 20.0
}
a.timer = wl_rnd() * 6.0
return a
}
# gone for good: not drawn, not ticked
export function wildlife_remove(a: WildAnimal) -> void {
if a == null { return }
a.alive = false
a.shown = false
}
# staged for a picture: nobody thinks, and everybody keeps the speed they were given
export function wildlife_hold(on: bool) -> void { wl_hold = on }
export function wildlife_by_nid(nid: int) -> WildAnimal {
let all = wildlife_all()
for i in 0 .. len(all) { if all[i].nid == nid { return all[i] } }
return null
}
# how many of a species are alive, legends apart
export function wildlife_count(sp: int) -> int {
var n = 0
let all = wildlife_all()
for i in 0 .. len(all) { if all[i].alive and all[i].sp == sp and not all[i].legend { n += 1 } }
return n
}

View file

@ -0,0 +1,44 @@
# system.ludic - the wildlife as a system: its save section is this player's record (the species
# seen and fed) and which ranges they have found; the animals themselves are respawned with the
# valley, so a reset forgets the record and nothing else
export function wildlife_reset() -> void {
wl_record_reset()
wl_found = null
let rs = wildlife_ranges()
for i in 0 .. len(rs) { rs[i].found = false }
q_clear(wildlife_facts())
}
export function wildlife_save() -> Val {
let v = Value.object()
sv_put_int(v, "seen", wl_seen)
sv_put_int(v, "fed", wl_fed)
sv_put_bool(v, "fed_any", wl_fed_any)
let found = new []int
let rs = wildlife_ranges()
for i in 0 .. len(rs) {
var f = 0
if rs[i].found { f = 1 }
push(found, f)
}
sv_put_ints(v, "found", found)
return v
}
export function wildlife_load(v: Val, version: int) -> void {
wildlife_reset()
wl_seen = sv_int(v, "seen", 0)
wl_fed = sv_int(v, "fed", 0)
wl_fed_any = sv_bool(v, "fed_any", false)
wl_found = sv_ints(v, "found")
wl_found_apply()
}
# PH_SIMULATE with no tick of its own: the game steps it (wildlife_tick) where the world is owned
export function wildlife_system() -> System {
let s = system_new("wildlife", PH_SIMULATE)
s.reset = fn wildlife_reset
s.save = fn wildlife_save
s.load = fn wildlife_load
return s
}

View file

@ -0,0 +1,108 @@
# fake.ludic - a toy valley for the tests: flat ground at 10 m with a lake west of x = -300 (its
# surface at 10.5), forest north of z = 400, up to two players, and a table of things set out
var fk_px: []float = null
var fk_pz: []float = null
var fk_on: []bool = null
var fk_noise: float = 0.0
var fk_scent: float = 0.0
var fk_still: int = 0
var fk_hidden: bool = false
var fk_blocked: bool = false
var fk_wind: float = 0.0
var fk_wind_dir: float = 0.0
var fk_winter: float = 0.0
var fk_about: int = 0
var fk_born: int = 0
var fk_lures: []WildLure = null
var fk_gone: int = 0 # a uid the port says is no longer there
function fk_setup() -> void {
fk_px = floats(2)
fk_pz = floats(2)
fk_on = new []bool
push(fk_on, true)
push(fk_on, false)
fk_px[0] = 5000.0
fk_pz[0] = 5000.0
fk_lures = new []WildLure
wildlife_clear()
wildlife_species_clear()
fk_species()
}
# 0 deer (meadow, tracks, medium), 1 bear (forest, charges), 2 hare (small), 3 jay (flies)
function fk_species() -> void {
let d = new WildSpecies
d.key = "deer"
d.walk = 1.1
d.run = 7.0
d.flee = 34.0
d.tracks = true
d.size = 1
d.range = 150.0
d.keep_away = 120.0
wildlife_species_add(d)
let b = new WildSpecies
b.key = "bear"
b.habitat = WILD_FOREST
b.charges = true
b.size = 2
b.diet = WILD_FISH
b.forget = 1.6
b.run = 6.0
wildlife_species_add(b)
let h = new WildSpecies
h.key = "hare"
wildlife_species_add(h)
let j = new WildSpecies
j.key = "jay"
j.flies = true
j.walk = 4.0
j.flee = 8.0
j.land_shy = 0.0
j.land_chance = 1.0
wildlife_species_add(j)
}
function fk_ground(x: float, z: float) -> float { return 10.0 }
function fk_water(x: float, z: float) -> float {
if x < -300.0 { return 10.5 }
return 0.0
}
function fk_forest(x: float, z: float) -> bool { return z > 400.0 }
function fk_sight(ax: float, ay: float, az: float, bx: float, by: float, bz: float) -> bool { return not fk_blocked }
function fk_players() -> int { return 2 }
function fk_player_on(p: int) -> bool { return fk_on[p] }
function fk_x(p: int) -> float { return fk_px[p] }
function fk_y(p: int) -> float { return 10.0 }
function fk_z(p: int) -> float { return fk_pz[p] }
function fk_is_hidden(p: int) -> bool { return fk_hidden }
function fk_noise_of(p: int) -> float { return fk_noise }
function fk_scent_of(p: int) -> float { return fk_scent }
function fk_still_of(p: int) -> int { return fk_still }
function fk_wind_of() -> float { return fk_wind }
function fk_wind_to() -> float { return fk_wind_dir }
function fk_winter_of() -> float { return fk_winter }
function fk_about_of(a: WildAnimal) -> int { return fk_about }
function fk_one(sp: int) -> int { return 1 }
function fk_count() -> int { return len(fk_lures) }
function fk_lure(i: int, l: WildLure) -> bool {
let f = fk_lures[i]
l.uid = f.uid
l.kind = f.kind
l.diet = f.diet
l.size = f.size
l.x = f.x
l.z = f.z
l.owner = f.owner
return f.uid != fk_gone
}
function fk_present(uid: int) -> bool { return uid != fk_gone }
function fk_on_born(a: WildAnimal) -> void { fk_born += 1 }
function fk_spot(a: WildAnimal, water: bool, s: WildSpot) -> bool {
s.uid = 900
s.x = a.x + 30.0
s.z = a.z
s.water = water
return true
}

View file

@ -0,0 +1,131 @@
# lures_test.ludic - food of its diet draws an animal and feeding it is a fact with the one who left
# it; the wrong food does not; a snare holds a small animal; thirst finds water; a bird comes to seed
import "ludic.wildlife"
import "ludic.base"
import "fake"
program LuresTest {
numbers float
bind WildlifeWorld {
ground: fn fk_ground
water: fn fk_water
players: fn fk_players
player_on: fn fk_player_on
player_x: fn fk_x
player_y: fn fk_y
player_z: fn fk_z
lures: fn fk_count
lure: fn fk_lure
present: fn fk_present
spot: fn fk_spot
}
function facts() -> []WildFact { return q_drain(wildlife_facts()) }
function set_out(uid: int, kind: int, diet: int, x: float, z: float, owner: int) -> WildLure {
let l = new WildLure
l.uid = uid
l.kind = kind
l.diet = diet
l.x = x
l.z = z
l.owner = owner
push(fk_lures, l)
return l
}
function idle(sp: int, x: float, z: float) -> WildAnimal {
let a = wildlife_make(sp, x, z, false, 0.0, 0, 1.0, 7)
a.timer = 30.0
return a
}
function go(a: WildAnimal, secs: float) -> void {
for k in 0 .. int(secs * 20.0) { wildlife_tick_ground(a, 0.05, 0.05 / 60.0) }
}
test "food of its diet draws it in, it eats, and the fact names what and whose" {
fk_setup()
set_out(41, WILD_FEED, WILD_PLANTS, 20.0, 0.0, 3)
let a = idle(0, 0.0, 0.0)
go(a, 40.0)
let fs = facts()
var fed = 0
for i in 0 .. len(fs) {
if fs[i].what == WILD_FED {
fed += 1
expect_eq(fs[i].lure, 41)
expect_eq(fs[i].owner, 3)
}
}
expect_eq(fed, 1)
expect(a.calm > 0.0)
}
test "food of another diet is left alone" {
fk_setup()
set_out(41, WILD_FEED, WILD_FISH, 20.0, 0.0, -1)
let a = idle(0, 0.0, 0.0)
go(a, 5.0)
expect(a.state != WILD_APPROACH)
expect_eq(a.lure, 0)
}
test "food taken away while it walks is given up" {
fk_setup()
set_out(41, WILD_FEED, WILD_PLANTS, 50.0, 0.0, -1)
let a = idle(0, 0.0, 0.0)
go(a, 1.0)
expect_eq(a.state, WILD_APPROACH)
fk_gone = 41
go(a, 0.2)
expect(a.state != WILD_APPROACH)
}
test "a snare holds a small animal, the port can find it, and a release lets it go" {
fk_setup()
let l = set_out(55, WILD_SNARE, 0, 10.0, 0.0, -1)
l.size = 0
let h = idle(2, 0.0, 0.0)
for k in 0 .. 40 {
if h.state == WILD_TRAPPED { break }
go(h, 2.0)
}
expect_eq(h.state, WILD_TRAPPED)
expect(wildlife_in_snare(55) == h)
let fs = facts()
var held = false
for i in 0 .. len(fs) { if fs[i].what == WILD_CAUGHT and fs[i].lure == 55 { held = true } }
expect(held)
wildlife_release(h)
expect_eq(h.state, WILD_FLEE)
expect(wildlife_in_snare(55) == null)
}
test "a thirsty animal walks to the water the port offers, and drinks it down" {
fk_setup()
let a = idle(0, 0.0, 0.0)
a.thirst = 90.0
a.state = WILD_WANDER
go(a, 0.1)
expect_eq(a.state, WILD_GO_DRINK)
expect_eq(a.spot, 900)
go(a, 40.0)
expect(a.thirst < 60.0)
}
test "seed down brings a bird out of the sky and it takes it" {
fk_setup()
set_out(77, WILD_FEED, WILD_SEED, 30.0, 0.0, -1)
let j = wildlife_new(3, 0.0, 0.0, false)
fk_px[0] = 200.0 # within 900 m: an animal nobody is near does not think
fk_pz[0] = 0.0
var landed = false
var ate = false
for k in 0 .. 4000 {
wildlife_tick(0.05, 0.0)
if j.state == WILD_PERCH { landed = true }
let fs = facts()
for i in 0 .. len(fs) { if fs[i].what == WILD_FED and fs[i].diet == WILD_SEED { ate = true } }
if ate { break }
}
expect(landed)
expect(ate)
}
}

View file

@ -0,0 +1,211 @@
# wildlife_test.ludic - species as data, a valley spawned against its ground, the alert (where it
# SETTLES, not only how it rises), cover, stillness and wind, the charge, the hours, prints and the save
import "ludic.wildlife"
import "ludic.base"
import "fake"
program WildlifeTest {
numbers float
bind WildlifeWorld {
ground: fn fk_ground
water: fn fk_water
forest: fn fk_forest
sight: fn fk_sight
players: fn fk_players
player_on: fn fk_player_on
player_x: fn fk_x
player_y: fn fk_y
player_z: fn fk_z
hidden: fn fk_is_hidden
noise: fn fk_noise_of
scent: fn fk_scent_of
stillness: fn fk_still_of
wind: fn fk_wind_of
wind_dir: fn fk_wind_to
winterness: fn fk_winter_of
about: fn fk_about_of
per_range: fn fk_one
lures: fn fk_count
lure: fn fk_lure
present: fn fk_present
born: fn fk_on_born
spot: fn fk_spot
}
function facts() -> []WildFact { return q_drain(wildlife_facts()) }
function count(fs: []WildFact, what: int) -> int {
var n = 0
for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } }
return n
}
function deer_at(x: float, z: float) -> WildAnimal {
let a = wildlife_make(0, x, z, false, 0.0, 0, 1.0, 1)
a.state = WILD_IDLE
a.timer = 30.0
return a
}
function player_at(x: float, z: float) -> void {
fk_px[0] = x
fk_pz[0] = z
}
function go(a: WildAnimal, secs: float) -> void {
for k in 0 .. int(secs * 20.0) { wildlife_tick_ground(a, 0.05, 0.05 / 60.0) }
}
test "the table handed in numbers the species, and a new animal is born through the port" {
fk_setup()
expect_eq(wildlife_species_count(), 4)
expect(wildlife_species(3).flies)
let a = wildlife_new(0, 10.0, 20.0, false)
expect_eq(fk_born, 1)
expect_near(a.y, 10.0, 0.001)
expect(a.variant >= 0 and a.variant <= 11)
expect(a.scale > 0.87 and a.scale < 1.13)
let j = wildlife_new(3, 0.0, 0.0, false)
expect_eq(j.state, WILD_FLY)
expect(wildlife_new(9, 0.0, 0.0, false) == null)
}
test "a spawn sets ranges out on their own habitat, clear of the start, and fills them" {
fk_setup()
wildlife_spawn(0.0, 0.0)
let rs = wildlife_ranges()
expect(len(rs) >= 6)
for i in 0 .. len(rs) {
expect(wildlife_range_dist(rs[i], 0.0, 0.0) >= wildlife_species(rs[i].sp).keep_away)
if rs[i].sp == 1 { expect(rs[i].z > 400.0) }
expect(rs[i].sx > -300.0)
}
expect(wildlife_count(0) >= 1)
expect_eq(wildlife_count_all(), wildlife_count(0) + wildlife_count(1) + wildlife_count(2) + wildlife_count(3))
}
test "a player far off makes no difference that lasts: the alert settles under the unaware line" {
fk_setup()
let a = deer_at(0.0, 0.0)
player_at(55.0, 0.0)
fk_noise = 0.3
go(a, 60.0)
expect(a.alert < 25.0)
expect(a.state != WILD_FLEE)
}
test "close and in the open it is flushed, runs, and says so" {
fk_setup()
let a = deer_at(0.0, 0.0)
player_at(8.0, 0.0)
fk_noise = 0.8
go(a, 10.0)
expect(a.spook > 0.0)
let fs = facts()
expect(count(fs, WILD_FLED) >= 1)
expect(a.x < 0.0)
}
test "cover hides a player from sight, a hide from everything but the nose" {
fk_setup()
let a = deer_at(0.0, 0.0)
let open = wildlife_alert_rate(a, 0, 30.0)
player_at(30.0, 0.0)
let open2 = wildlife_alert_rate(a, 0, 30.0)
fk_blocked = true
let covered = wildlife_alert_rate(a, 0, 30.0)
fk_blocked = false
fk_hidden = true
let hidden = wildlife_alert_rate(a, 0, 30.0)
expect(open2 > 0.0)
expect(open >= open2)
expect_near(covered, 0.0, 0.0001)
expect_near(hidden, 0.0, 0.0001)
}
test "sitting still makes it forget faster, and the wind carries scent only downwind" {
fk_setup()
let a = deer_at(0.0, 0.0)
let moving = wildlife_alert_decay(a, 0)
fk_still = WILD_SITTING
expect(wildlife_alert_decay(a, 0) > moving * 2.0)
player_at(0.0, -60.0)
fk_blocked = true
fk_scent = 1.0
fk_wind = 0.5
fk_wind_dir = 3.1415927 # blowing toward +z: from the player to the deer
expect(wildlife_downwind(a, 0) > 0.9)
expect(wildlife_alert_rate(a, 0, 60.0) > 0.0)
fk_wind_dir = 0.0
expect_near(wildlife_alert_rate(a, 0, 60.0), 0.0, 0.0001)
}
test "a charger comes at a player inside its reach, and a shove is a fact for the game" {
fk_setup()
let b = wildlife_make(1, 0.0, 0.0, false, 0.0, 0, 1.0, 2)
player_at(6.0, 0.0)
wildlife_tick_ground(b, 0.05, 0.001)
expect_eq(b.state, WILD_CHARGE)
go(b, 2.0)
let fs = facts()
expect_eq(count(fs, WILD_CHARGED), 1)
expect_eq(count(fs, WILD_SHOVED), 1)
expect(b.calm > 0.0)
}
test "away is hidden and skipped, gone is removed and said" {
fk_setup()
let a = deer_at(0.0, 0.0)
player_at(10.0, 0.0)
fk_about = WILD_AWAY
wildlife_tick(0.05, 0.001)
expect(not a.shown and a.alive)
fk_about = WILD_GONE
wildlife_tick(0.05, 0.001)
expect(not a.alive)
expect_eq(count(facts(), WILD_WENT), 1)
}
test "a walker leaves a print every 2.8 m, and player 0 spots a species once" {
fk_setup()
let a = deer_at(0.0, 0.0)
player_at(40.0, 0.0)
fk_blocked = true
a.state = WILD_WANDER
a.tx = 0.0
a.tz = -100.0
a.timer = 100.0
for k in 0 .. 200 { wildlife_tick(0.05, 0.0) }
let fs = facts()
expect(count(fs, WILD_PRINT) >= 3)
expect_eq(count(fs, WILD_SPOTTED), 1)
expect(wildlife_seen(0))
}
test "the record and the found ranges come back from the save section" {
fk_setup()
wildlife_spawn(0.0, 0.0)
wildlife_mark_fed(2)
wildlife_saw(0)
let r = wildlife_ranges()[0]
wildlife_ranges_visit(r.x, r.z)
let v = wildlife_save()
wildlife_reset()
expect(not wildlife_fed_any() and not wildlife_ranges()[0].found)
wildlife_load(v, 1)
expect(wildlife_fed_any())
expect_eq(wildlife_fed_mask(), 4)
expect(wildlife_seen(0))
expect(wildlife_ranges()[0].found)
}
test "a morning moves the ranges toward winter and brings one back to a range that lost its own" {
fk_setup()
wildlife_spawn(0.0, 0.0)
let r = wildlife_ranges()[0]
let x0 = r.x
let all = wildlife_all()
for i in 0 .. len(all) { if all[i].rg == 0 { wildlife_remove(all[i]) } }
fk_winter = 1.0
let added = wildlife_morning()
expect(added >= 1)
expect_eq(wildlife_range_alive(0), 1)
expect_near(r.x, r.wx, 0.001)
expect(r.x != x0 or r.wx == r.sx)
}
}

View file

@ -0,0 +1,56 @@
# want.ludic - thirst and hunger over sixty send it to the water or the forage the game offers for
# it (a hungry one on its way turns to food put down nearer); it drinks or grazes there
var wl_spot_ask: WildSpot = null
function wl_seek(a: WildAnimal, dt: float) -> bool {
if a.state == WILD_GO_FEED and wl_attraction(a) and wl_best.kind == WILD_FEED { wl_take_lure(a) }
if a.state != WILD_GO_DRINK and a.state != WILD_GO_FEED { return false }
if a.spot == 0 or not WildlifeWorld.present(a.spot) {
a.spot = 0
a.state = WILD_IDLE
a.timer = 2.0
return true
}
wl_face(a, a.spot_x, a.spot_z, 2.0, dt)
a.speed = wl_sp(a.sp).walk
a.tx = a.spot_x
a.tz = a.spot_z
wl_step(a, dt)
if wl_at_target(a, 2.0) {
a.state = WILD_DRINK
a.timer = 6.0 + wl_rnd() * 10.0
}
a.timer = a.timer - dt
if a.timer < -600.0 {
a.state = WILD_IDLE
a.timer = 3.0
}
return true
}
# true while it drinks or grazes, or when it has just set off
function wl_drink(a: WildAnimal, dt: float, gh: float) -> bool {
if a.state == WILD_DRINK {
a.speed = 0.0
a.timer = a.timer - dt
if a.spot != 0 and a.spot_water { a.thirst = Math.max(a.thirst - 600.0 * gh, 0.0) } else { a.hunger = Math.max(a.hunger - 600.0 * gh, 0.0) }
if a.timer < 0.0 or Math.min(a.thirst, a.hunger) < 1.0 {
wl_rehome(a)
wl_wander(a, 6.0)
}
return true
}
if a.state != WILD_IDLE and a.state != WILD_WANDER { return false }
if not (a.thirst > 60.0 or a.hunger > 60.0) { return false }
if wl_spot_ask == null { wl_spot_ask = new WildSpot }
let water = a.thirst > a.hunger
if not WildlifeWorld.spot(a, water, wl_spot_ask) { return false }
a.spot = wl_spot_ask.uid
a.spot_x = wl_spot_ask.x
a.spot_z = wl_spot_ask.z
a.spot_water = wl_spot_ask.water
a.state = WILD_GO_DRINK
if not water { a.state = WILD_GO_FEED }
a.timer = 120.0
return true
}