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:
parent
80f3468ff8
commit
c9dc592dcc
28 changed files with 1956 additions and 0 deletions
77
packages/ludic.wildlife/README.md
Normal file
77
packages/ludic.wildlife/README.md
Normal 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
|
||||
```
|
||||
64
packages/ludic.wildlife/animal.ludic
Normal file
64
packages/ludic.wildlife/animal.ludic
Normal 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
|
||||
}
|
||||
105
packages/ludic.wildlife/birds.ludic
Normal file
105
packages/ludic.wildlife/birds.ludic
Normal 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)
|
||||
}
|
||||
50
packages/ludic.wildlife/facts.ludic
Normal file
50
packages/ludic.wildlife/facts.ludic
Normal 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
|
||||
}
|
||||
89
packages/ludic.wildlife/ground.ludic
Normal file
89
packages/ludic.wildlife/ground.ludic
Normal 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)
|
||||
}
|
||||
17
packages/ludic.wildlife/guest.ludic
Normal file
17
packages/ludic.wildlife/guest.ludic
Normal 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 }
|
||||
38
packages/ludic.wildlife/habitat.ludic
Normal file
38
packages/ludic.wildlife/habitat.ludic
Normal 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 }
|
||||
27
packages/ludic.wildlife/index.ludic
Normal file
27
packages/ludic.wildlife/index.ludic
Normal 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"
|
||||
102
packages/ludic.wildlife/lures.ludic
Normal file
102
packages/ludic.wildlife/lures.ludic
Normal 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
|
||||
}
|
||||
66
packages/ludic.wildlife/migrate.ludic
Normal file
66
packages/ludic.wildlife/migrate.ludic
Normal 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
|
||||
}
|
||||
22
packages/ludic.wildlife/needs.ludic
Normal file
22
packages/ludic.wildlife/needs.ludic
Normal 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)
|
||||
}
|
||||
5
packages/ludic.wildlife/package.ludic
Normal file
5
packages/ludic.wildlife/package.ludic
Normal 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
|
||||
38
packages/ludic.wildlife/places.ludic
Normal file
38
packages/ludic.wildlife/places.ludic
Normal 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
|
||||
}
|
||||
73
packages/ludic.wildlife/port.ludic
Normal file
73
packages/ludic.wildlife/port.ludic
Normal 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
|
||||
}
|
||||
97
packages/ludic.wildlife/ranges.ludic
Normal file
97
packages/ludic.wildlife/ranges.ludic
Normal 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 } }
|
||||
}
|
||||
79
packages/ludic.wildlife/react.ludic
Normal file
79
packages/ludic.wildlife/react.ludic
Normal 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
|
||||
}
|
||||
34
packages/ludic.wildlife/record.ludic
Normal file
34
packages/ludic.wildlife/record.ludic
Normal 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
|
||||
}
|
||||
58
packages/ludic.wildlife/run.ludic
Normal file
58
packages/ludic.wildlife/run.ludic
Normal 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
|
||||
}
|
||||
97
packages/ludic.wildlife/senses.ludic
Normal file
97
packages/ludic.wildlife/senses.ludic
Normal 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)
|
||||
}
|
||||
52
packages/ludic.wildlife/spawn.ludic
Normal file
52
packages/ludic.wildlife/spawn.ludic
Normal 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
|
||||
}
|
||||
57
packages/ludic.wildlife/species.ludic
Normal file
57
packages/ludic.wildlife/species.ludic
Normal 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] }
|
||||
64
packages/ludic.wildlife/steer.ludic
Normal file
64
packages/ludic.wildlife/steer.ludic
Normal 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)
|
||||
}
|
||||
95
packages/ludic.wildlife/store.ludic
Normal file
95
packages/ludic.wildlife/store.ludic
Normal 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
|
||||
}
|
||||
44
packages/ludic.wildlife/system.ludic
Normal file
44
packages/ludic.wildlife/system.ludic
Normal 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
|
||||
}
|
||||
108
packages/ludic.wildlife/tests/fake/index.ludic
Normal file
108
packages/ludic.wildlife/tests/fake/index.ludic
Normal 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
|
||||
}
|
||||
131
packages/ludic.wildlife/tests/lures_test.ludic
Normal file
131
packages/ludic.wildlife/tests/lures_test.ludic
Normal 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)
|
||||
}
|
||||
}
|
||||
211
packages/ludic.wildlife/tests/wildlife_test.ludic
Normal file
211
packages/ludic.wildlife/tests/wildlife_test.ludic
Normal 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)
|
||||
}
|
||||
}
|
||||
56
packages/ludic.wildlife/want.ludic
Normal file
56
packages/ludic.wildlife/want.ludic
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue