Merge commit 'cc24409' into lang/foundations
This commit is contained in:
commit
aba5d9e679
10 changed files with 170 additions and 32 deletions
7
changes/wildlife-painted.md
Normal file
7
changes/wildlife-painted.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**`ludic.wildlife`: a map may paint where a species lives.** Two new `WildlifeWorld` members, both with
|
||||
defaults that change nothing: `habitat_in(sp, x, z)` answers 1 inside a species' painted habitat, 0
|
||||
outside and -1 where the map paints none (the kind's rule stands), and `range_at(sp, k, r)` fills the
|
||||
map's k-th painted range, which is set out as painted and draws no dice. Spots, ranges, fills and
|
||||
newcomers keep to the paint (`wildlife_habitat_of`, `wildlife_find_spot_of`), never under water.
|
||||
|
|
@ -39,6 +39,8 @@ export port WildlifeWorld { # every member has a default: a flat dry mead
|
|||
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
|
||||
habitat_in: fn(sp, x, z) -> int # the map's painted habitat: 1 in, 0 out, -1 none painted
|
||||
range_at: fn(sp, k, WildRange) -> bool # the map's k-th painted range (x, z, r; r 0 = the species')
|
||||
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
|
||||
|
|
@ -58,6 +60,12 @@ target took - so binding them changes where an animal walks and never the order
|
|||
no answer (the defaults) it walks straight and turns 75 degrees off water and a climb, as before.
|
||||
Maroon Lake binds both to its navmesh, walked by the species' habitat's taste.
|
||||
|
||||
**A map may paint where a species lives.** Where `habitat_in` answers 0 or 1, a species' spots,
|
||||
ranges and newcomers keep inside its painted areas (never under water) instead of its kind's rule;
|
||||
where it answers -1 the rule stands. A range `range_at` answers is set out as painted, drawing no
|
||||
dice; the ones it does not answer are placed by the dice as before. The package never reads the
|
||||
map's tables: the game answers both from its Areas.
|
||||
|
||||
## API
|
||||
|
||||
| | |
|
||||
|
|
@ -67,7 +75,7 @@ Maroon Lake binds both to its navmesh, walked by the species' habitat's taste.
|
|||
| `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_habitat_ok(hab, x, z)`, `wildlife_habitat_of(sp, hab, x, z)`, `wildlife_find_spot(hab, x0, z0, d0, d1)`, `wildlife_find_spot_of(sp, hab, ...)`, `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 |
|
||||
|
|
|
|||
|
|
@ -16,14 +16,26 @@ export function wildlife_habitat_ok(hab: int, x: float, z: float) -> bool {
|
|||
return s > 0.25 or h > 80.0
|
||||
}
|
||||
|
||||
# a species' ground: its painted habitat where the map has one (never under water), else its kind's rule
|
||||
export function wildlife_habitat_of(sp: int, hab: int, x: float, z: float) -> bool {
|
||||
let painted = WildlifeWorld.habitat_in(sp, x, z)
|
||||
if painted < 0 { return wildlife_habitat_ok(hab, x, z) }
|
||||
return painted == 1 and WildlifeWorld.ground(x, z) >= WildlifeWorld.water(x, z) + 0.8
|
||||
}
|
||||
|
||||
# a spot of `hab` d0..d1 metres from (x0, z0); where it is, is wildlife_spot_x / _z
|
||||
export function wildlife_find_spot(wildlife_st: mut WildlifeState, hab: int, x0: float, z0: float, d0: float, d1: float) -> bool {
|
||||
return wildlife_find_spot_of(wildlife_st, -1, hab, x0, z0, d0, d1)
|
||||
}
|
||||
|
||||
# the same for a species (sp >= 0): inside its painted habitat when the map has one
|
||||
export function wildlife_find_spot_of(wildlife_st: mut WildlifeState, sp: int, hab: int, x0: float, z0: float, d0: float, d1: float) -> bool {
|
||||
for t in 0 .. 60 {
|
||||
let ang = wl_rnd(wildlife_st) * WL_TAU
|
||||
let d = d0 + wl_rnd(wildlife_st) * (d1 - d0)
|
||||
let x = x0 + Math.cos(ang) * d
|
||||
let z = z0 + Math.sin(ang) * d
|
||||
if wildlife_habitat_ok(hab, x, z) {
|
||||
if (sp < 0 and wildlife_habitat_ok(hab, x, z)) or (sp >= 0 and wildlife_habitat_of(sp, hab, x, z)) {
|
||||
wildlife_st.wl_px = x
|
||||
wildlife_st.wl_pz = z
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import "guest.ludic"
|
|||
import "record.ludic"
|
||||
import "habitat.ludic"
|
||||
import "ranges.ludic"
|
||||
import "winter.ludic"
|
||||
import "migrate.ludic"
|
||||
import "senses.ludic"
|
||||
import "steer.ludic"
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ export port WildlifeWorld {
|
|||
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
|
||||
# (sp, x, z): the map's painted habitat for a species: 1 inside, 0 outside, -1 none painted (the rule)
|
||||
habitat_in: fn(int, float, float) -> int = fn wl_unpainted
|
||||
range_at: fn(int, int, WildRange) -> bool = fn wl_no_range # (sp, k): the k-th painted range, x, z, r
|
||||
active: fn(float, float) -> bool = fn wl_yes2 # simulated here: outside, an animal is frozen
|
||||
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
|
||||
|
|
@ -78,6 +81,8 @@ 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_unpainted(sp: int, x: float, z: float) -> int { return -1 }
|
||||
function wl_no_range(sp: int, k: int, r: WildRange) -> bool { return false }
|
||||
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 { }
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ function wl_range_clashes(wildlife_st: WildlifeState, sp: int, x: float, z: floa
|
|||
function wl_range_try(wildlife_st: mut WildlifeState, sp: int, x0: float, z0: float) -> bool {
|
||||
let s = wl_sp(wildlife_st, sp)
|
||||
for tries in 0 .. 80 {
|
||||
if not wildlife_find_spot(wildlife_st, s.habitat, x0, z0, 120.0, 700.0) { continue }
|
||||
if not wildlife_find_spot_of(wildlife_st, sp, s.habitat, x0, z0, 120.0, 700.0) { continue }
|
||||
let dx = wildlife_st.wl_px - x0
|
||||
let dz = wildlife_st.wl_pz - z0
|
||||
if Math.sqrt(dx * dx + dz * dz) < s.keep_away or wl_range_clashes(wildlife_st, sp, wildlife_st.wl_px, wildlife_st.wl_pz) { continue }
|
||||
|
|
@ -48,39 +48,16 @@ export function wildlife_ranges_setup(wildlife_st: mut WildlifeState, x0: float,
|
|||
wildlife_seed(wildlife_st, 613)
|
||||
wildlife_st.wl_ranges = new []WildRange
|
||||
for sp in 0 .. wildlife_species_count(wildlife_st) {
|
||||
for k in 0 .. WildlifeWorld.ranges(sp) { if not wl_range_try(wildlife_st, sp, x0, z0) { break } }
|
||||
for k in 0 .. WildlifeWorld.ranges(sp) {
|
||||
if wl_range_painted(wildlife_st, sp, k) { continue }
|
||||
if not wl_range_try(wildlife_st, sp, x0, z0) { break }
|
||||
}
|
||||
}
|
||||
wl_found_apply(wildlife_st)
|
||||
let rs = wildlife_ranges(wildlife_st)
|
||||
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(wildlife_st: WildlifeState, x: float, z: float) -> void {
|
||||
let rs = wildlife_ranges(wildlife_st)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ function wl_fill(wildlife_st: mut WildlifeState, i: int, want: int) -> int {
|
|||
let r = wildlife_range(wildlife_st, i)
|
||||
var made = 0
|
||||
for k in 0 .. want {
|
||||
if not wildlife_find_spot(wildlife_st, wl_sp(wildlife_st, r.sp).habitat, r.x, r.z, 0.0, r.r) { continue }
|
||||
if not wildlife_find_spot_of(wildlife_st, r.sp, wl_sp(wildlife_st, r.sp).habitat, r.x, r.z, 0.0, r.r) { continue }
|
||||
let a = wildlife_new(wildlife_st, r.sp, wildlife_st.wl_px, wildlife_st.wl_pz, false)
|
||||
if a != null {
|
||||
a.rg = i
|
||||
|
|
@ -38,7 +38,7 @@ export function wildlife_repopulate(wildlife_st: mut WildlifeState) -> int {
|
|||
let want = WildlifeWorld.per_range(rs[i].sp)
|
||||
if want == 0 or wildlife_range_alive(wildlife_st, i) >= want { continue }
|
||||
for t in 0 .. 12 {
|
||||
if not wildlife_find_spot(wildlife_st, wl_sp(wildlife_st, rs[i].sp).habitat, rs[i].x, rs[i].z, 0.0, rs[i].r) { continue }
|
||||
if not wildlife_find_spot_of(wildlife_st, rs[i].sp, wl_sp(wildlife_st, rs[i].sp).habitat, rs[i].x, rs[i].z, 0.0, rs[i].r) { continue }
|
||||
if wl_near(wildlife_st, wildlife_st.wl_px, wildlife_st.wl_pz) < 200.0 { continue }
|
||||
let a = wildlife_new(wildlife_st, rs[i].sp, wildlife_st.wl_px, wildlife_st.wl_pz, false)
|
||||
if a != null {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export state WildlifeState {
|
|||
wl_pz: float = 0.0
|
||||
wl_ask: WildLure = new WildLure
|
||||
wl_best: WildLure = new WildLure
|
||||
wl_ask_rg: WildRange = new WildRange # what WildlifeWorld.range_at fills, copied only when it answers
|
||||
wl_seed_any: bool = false
|
||||
wl_seed_t: float = 0.0
|
||||
wl_ids: int = 0
|
||||
|
|
|
|||
82
packages/ludic.wildlife/tests/painted_test.ludic
Normal file
82
packages/ludic.wildlife/tests/painted_test.ludic
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# painted_test.ludic - a map that paints a species' habitat and a range: its ranges and animals keep
|
||||
# inside it, the painted range stands where it was painted, and a species with none keeps its rule
|
||||
import "ludic.wildlife"
|
||||
import "ludic.base"
|
||||
import "fake"
|
||||
program WildPaintedTest {
|
||||
numbers float
|
||||
bind WildlifeWorld {
|
||||
ground: fn fk_ground
|
||||
water: fn fk_water
|
||||
forest: fn fk_forest
|
||||
sight: fn fk_sight
|
||||
push: fn fk_push
|
||||
pushed_x: fn fk_pushed_x
|
||||
pushed_z: fn fk_pushed_z
|
||||
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
|
||||
habitat_in: fn pt_habitat
|
||||
range_at: fn pt_range
|
||||
}
|
||||
|
||||
# the deer's habitat is the square x 500..700, z -100..100; its first range is painted at (600, 0)
|
||||
function pt_habitat(sp: int, x: float, z: float) -> int {
|
||||
if sp != 0 { return -1 }
|
||||
if x >= 500.0 and x <= 700.0 and z >= -100.0 and z <= 100.0 { return 1 }
|
||||
return 0
|
||||
}
|
||||
function pt_range(sp: int, k: int, r: WildRange) -> bool {
|
||||
if sp != 0 or k != 0 { return false }
|
||||
r.x = 600.0
|
||||
r.z = 0.0
|
||||
r.r = 80.0
|
||||
return true
|
||||
}
|
||||
function pt_inside(x: float, z: float) -> bool { return x >= 500.0 and x <= 700.0 and z >= -100.0 and z <= 100.0 }
|
||||
|
||||
test "a painted range stands where it was painted, and every deer range and deer keeps to the paint" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) {
|
||||
fk_setup(wildlife_st, wildlife_tests_fake_st)
|
||||
wildlife_spawn(wildlife_st, 0.0, 0.0)
|
||||
let rs = wildlife_ranges(wildlife_st)
|
||||
var first = -1
|
||||
for i in 0 .. len(rs) { if first < 0 and rs[i].sp == 0 { first = i } }
|
||||
expect(first >= 0)
|
||||
expect_eq(rs[first].sx, 600.0)
|
||||
expect_eq(rs[first].r, 80.0)
|
||||
for i in 0 .. len(rs) { if rs[i].sp == 0 { expect(pt_inside(rs[i].sx, rs[i].sz)) } }
|
||||
let all = wildlife_all(wildlife_st)
|
||||
var deer = 0
|
||||
for i in 0 .. len(all) {
|
||||
if all[i].sp == 0 {
|
||||
deer += 1
|
||||
expect(pt_inside(all[i].x, all[i].z))
|
||||
}
|
||||
}
|
||||
expect(deer >= 1)
|
||||
}
|
||||
|
||||
test "a species with nothing painted keeps its kind's rule" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) {
|
||||
fk_setup(wildlife_st, wildlife_tests_fake_st)
|
||||
expect(wildlife_habitat_of(1, WILD_FOREST, 0.0, 500.0))
|
||||
expect(not wildlife_habitat_of(1, WILD_FOREST, 0.0, 0.0))
|
||||
expect(not wildlife_habitat_of(0, WILD_MEADOW, 0.0, 0.0))
|
||||
expect(wildlife_habitat_of(0, WILD_MEADOW, 550.0, 0.0))
|
||||
}
|
||||
}
|
||||
45
packages/ludic.wildlife/winter.ludic
Normal file
45
packages/ludic.wildlife/winter.ludic
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# winter.ludic - where a range winters, and a range the map paints rather than the dice
|
||||
|
||||
# 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# the map's own k-th range for a species, when it paints one: no dice drawn
|
||||
function wl_range_painted(wildlife_st: mut WildlifeState, sp: int, k: int) -> bool {
|
||||
let q = wildlife_st.wl_ask_rg
|
||||
q.r = 0.0
|
||||
if not WildlifeWorld.range_at(sp, k, q) { return false }
|
||||
let r = new WildRange
|
||||
r.sp = sp
|
||||
r.x = q.x
|
||||
r.z = q.z
|
||||
r.r = q.r
|
||||
if r.r <= 0.0 { r.r = wl_sp(wildlife_st, sp).range }
|
||||
r.sx = r.x
|
||||
r.sz = r.z
|
||||
wl_pick_winter(r)
|
||||
push(wildlife_ranges(wildlife_st), r)
|
||||
return true
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue