ludic.zones: a disc per player from their fog, merged into clusters; the simulating packages ask whether a place is simulated
zones_set / zones_clear / zones_merge, zones_contains / zones_nearest and the clusters' bounding circles, over fixed arrays of ZONES_MAX (16). WildlifeWorld.active freezes an animal outside (no state change, no dice), NpcWorld.active a walker (and no birth outside), VehicleWorld.active a moored boat, and ThingsWorld.restocks keeps a morning's restock inside; every default is "everywhere", so an unbound game is unchanged. Tests: the merge (near players one cluster, far two, a player leaving splits a chain), contains, nearest, the radius; wildlife, npc, vehicles and things pass as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2958539514
commit
8b85282502
18 changed files with 257 additions and 4 deletions
10
changes/zones.md
Normal file
10
changes/zones.md
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**`ludic.zones`: where a world is simulated.** A disc per player, `(x, z, r)` with `r = zones_radius(fog)`
|
||||||
|
(the fog distance plus 40 m, never under 150 m, 900 m with no fog), overlapping discs merged into
|
||||||
|
clusters with a bounding circle, and `zones_contains` / `zones_nearest` for everything that asks; fixed
|
||||||
|
arrays, nothing made per frame. The packages that simulate now ask their world port whether a place is
|
||||||
|
simulated, with a default of everywhere: `WildlifeWorld.active` (an animal outside is frozen - its
|
||||||
|
state kept, no dice drawn), `NpcWorld.active` (a walker outside is frozen, and nobody is born outside),
|
||||||
|
`VehicleWorld.active` (a moored boat outside is left where it lies) and `ThingsWorld.restocks` (a
|
||||||
|
morning restocks only inside).
|
||||||
|
|
@ -22,6 +22,7 @@ section. The rules are in [ludic.base](ludic.base/README.md).
|
||||||
| [ludic.crafting](ludic.crafting/README.md) | recipes: what goes in and out, where and how long, making, refunding, hold-to-make |
|
| [ludic.crafting](ludic.crafting/README.md) | recipes: what goes in and out, where and how long, making, refunding, hold-to-make |
|
||||||
| [ludic.effects](ludic.effects/README.md) | timed modifiers that run down in game time (a meal's warmth, a drink's legs) |
|
| [ludic.effects](ludic.effects/README.md) | timed modifiers that run down in game time (a meal's warmth, a drink's legs) |
|
||||||
| [ludic.fire](ludic.fire/README.md) | a camp fire: fuel, the rain on it, warmth at a distance, whether it will cook |
|
| [ludic.fire](ludic.fire/README.md) | a camp fire: fuel, the rain on it, warmth at a distance, whether it will cook |
|
||||||
|
| [ludic.zones](ludic.zones/README.md) | where the world is simulated: a disc per player from their fog, merged into clusters |
|
||||||
| [ludic.fishing](ludic.fishing/README.md) | a rod at the water: cast, bite, the reel's fight, the landing; species as an open registry |
|
| [ludic.fishing](ludic.fishing/README.md) | a rod at the water: cast, bite, the reel's fight, the landing; species as an open registry |
|
||||||
| [ludic.hints](ludic.hints/README.md) | a rail of what is true now, most urgent first, each a card that teaches it and can be muted by key |
|
| [ludic.hints](ludic.hints/README.md) | a rail of what is true now, most urgent first, each a card that teaches it and can be muted by key |
|
||||||
| [ludic.i18n](ludic.i18n/README.md) | a game in any language: gettext `.po` files, patterns with holes, plurals, a mod folder, a font per language |
|
| [ludic.i18n](ludic.i18n/README.md) | a game in any language: gettext `.po` files, patterns with holes, plurals, a mod folder, a font per language |
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ export function npc_spawn(npc_st: mut NpcState, kind: int) -> NpcPerson {
|
||||||
let start = k.start
|
let start = k.start
|
||||||
start(p)
|
start(p)
|
||||||
} else { npc_start_near(npc_st, p, NpcWorld.origin_x(), NpcWorld.origin_z(), 6.0, 30.0) }
|
} else { npc_start_near(npc_st, p, NpcWorld.origin_x(), NpcWorld.origin_z(), 6.0, 30.0) }
|
||||||
|
if not NpcWorld.active(p.x, p.z) { return null } # nobody is born outside every zone; the slot waits
|
||||||
p.active = true
|
p.active = true
|
||||||
npc_st.np_n += 1
|
npc_st.np_n += 1
|
||||||
NpcWorld.born(p)
|
NpcWorld.born(p)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ export port NpcWorld {
|
||||||
water: fn(float, float) -> float = fn nw_dry # the water's surface there
|
water: fn(float, float) -> float = fn nw_dry # the water's surface there
|
||||||
slope: fn(float, float) -> float = fn nw_zero2
|
slope: fn(float, float) -> float = fn nw_zero2
|
||||||
allowed: fn(float, float) -> bool = fn nw_yes2 # a person may stop here (not the player's camp)
|
allowed: fn(float, float) -> bool = fn nw_yes2 # a person may stop here (not the player's camp)
|
||||||
|
active: fn(float, float) -> bool = fn nw_yes2 # simulated here: outside, a walker is frozen and none is born
|
||||||
# (x, z, r, feet, head): true when a still thing takes a body of radius r there; pushed_x / _z say where
|
# (x, z, r, feet, head): true when a still thing takes a body of radius r there; pushed_x / _z say where
|
||||||
push: fn(float, float, float, float, float) -> bool = fn nw_no_push
|
push: fn(float, float, float, float, float) -> bool = fn nw_no_push
|
||||||
pushed_x: fn() -> float = fn nw_asked_x
|
pushed_x: fn() -> float = fn nw_asked_x
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ export function npc_tick(npc_st: mut NpcState, dt: float) -> void {
|
||||||
np_census(npc_st)
|
np_census(npc_st)
|
||||||
for i in 0 .. len(npc_st.np_walkers) {
|
for i in 0 .. len(npc_st.np_walkers) {
|
||||||
let p = npc_st.np_walkers[i]
|
let p = npc_st.np_walkers[i]
|
||||||
if not p.active or np_leaves(npc_st, p) { continue }
|
if not p.active or not NpcWorld.active(p.x, p.z) or np_leaves(npc_st, p) { continue }
|
||||||
p.goal_speed = 0.0
|
p.goal_speed = 0.0
|
||||||
if p.act == NPCA_WALK { npc_walk(npc_st, p, dt) } else { npc_act(npc_st, p, dt) }
|
if p.act == NPCA_WALK { npc_walk(npc_st, p, dt) } else { npc_act(npc_st, p, dt) }
|
||||||
npc_notice(npc_st, p)
|
npc_notice(npc_st, p)
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ export function things_restock(things_st: ThingsState) -> void {
|
||||||
let all = things_all(things_st)
|
let all = things_all(things_st)
|
||||||
for i in 0 .. len(all) {
|
for i in 0 .. len(all) {
|
||||||
let t = all[i]
|
let t = all[i]
|
||||||
if thing_is_put_down(t) { continue }
|
if thing_is_put_down(t) or not ThingsWorld.restocks(t.x, t.z) { continue }
|
||||||
if not ThingKinds[t.kind].keeps { t.used = 0 }
|
if not ThingKinds[t.kind].keeps { t.used = 0 }
|
||||||
if not t.active { thing_show(t) }
|
if not t.active { thing_show(t) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ export port ThingsWorld {
|
||||||
placed: fn(Thing) -> void = fn th_nothing # a Thing exists now: draw it
|
placed: fn(Thing) -> void = fn th_nothing # a Thing exists now: draw it
|
||||||
removed: fn(Thing) -> void = fn th_nothing # it is gone for good: stop drawing it
|
removed: fn(Thing) -> void = fn th_nothing # it is gone for good: stop drawing it
|
||||||
shown: fn(Thing) -> void = fn th_nothing # its `active` changed: show or hide it
|
shown: fn(Thing) -> void = fn th_nothing # its `active` changed: show or hide it
|
||||||
|
restocks: fn(float, float) -> bool = fn th_yes2 # a morning restocks here (inside a zone)
|
||||||
moved: fn(Thing) -> void = fn th_nothing # it stands somewhere else now
|
moved: fn(Thing) -> void = fn th_nothing # it stands somewhere else now
|
||||||
keeps: fn(Thing) -> bool = fn th_kind_saves # the save's section carries it
|
keeps: fn(Thing) -> bool = fn th_kind_saves # the save's section carries it
|
||||||
put_down: fn(Thing) -> bool = fn th_kind_put_down # set down by someone: a restock leaves it be
|
put_down: fn(Thing) -> bool = fn th_kind_put_down # set down by someone: a restock leaves it be
|
||||||
|
|
@ -12,5 +13,6 @@ export port ThingsWorld {
|
||||||
}
|
}
|
||||||
function th_flat(x: float, z: float) -> float { return 0.0 }
|
function th_flat(x: float, z: float) -> float { return 0.0 }
|
||||||
function th_nothing(t: Thing) -> void { }
|
function th_nothing(t: Thing) -> void { }
|
||||||
|
function th_yes2(x: float, z: float) -> bool { return true }
|
||||||
function th_kind_saves(t: Thing) -> bool { return thing_kind_ok(t.kind) and ThingKinds[t.kind].saved }
|
function th_kind_saves(t: Thing) -> bool { return thing_kind_ok(t.kind) and ThingKinds[t.kind].saved }
|
||||||
function th_kind_put_down(t: Thing) -> bool { return thing_kind_ok(t.kind) and ThingKinds[t.kind].put_down }
|
function th_kind_put_down(t: Thing) -> bool { return thing_kind_ok(t.kind) and ThingKinds[t.kind].put_down }
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ export port VehicleWorld {
|
||||||
made: fn(Vehicle) -> void = fn ve__nothing # a new vehicle: draw it
|
made: fn(Vehicle) -> void = fn ve__nothing # a new vehicle: draw it
|
||||||
placed: fn(Vehicle) -> void = fn ve__nothing # it moved: put its model there
|
placed: fn(Vehicle) -> void = fn ve__nothing # it moved: put its model there
|
||||||
gone: fn(Vehicle) -> void = fn ve__nothing # its owner left: take it away
|
gone: fn(Vehicle) -> void = fn ve__nothing # its owner left: take it away
|
||||||
|
active: fn(float, float) -> bool = fn ve__yes2 # simulated here: outside, a moored boat is left as it is
|
||||||
}
|
}
|
||||||
|
|
||||||
# a boat's hull in the world's physics (Maroon Lake: a buoyant Jolt body), named by the vehicle's
|
# a boat's hull in the world's physics (Maroon Lake: a buoyant Jolt body), named by the vehicle's
|
||||||
|
|
@ -70,6 +71,7 @@ export state VehiclesState {
|
||||||
function ve__dry(x: float, z: float) -> float { return -10000.0 }
|
function ve__dry(x: float, z: float) -> float { return -10000.0 }
|
||||||
function ve__calm(x: float, z: float, fx: float, fz: float) -> float { return 0.0 }
|
function ve__calm(x: float, z: float, fx: float, fz: float) -> float { return 0.0 }
|
||||||
function ve__zero() -> float { return 0.0 }
|
function ve__zero() -> float { return 0.0 }
|
||||||
|
function ve__yes2(x: float, z: float) -> bool { return true }
|
||||||
function ve__next(vehicles_st: mut VehiclesState) -> int {
|
function ve__next(vehicles_st: mut VehiclesState) -> int {
|
||||||
vehicles_st.ve_ids += 1
|
vehicles_st.ve_ids += 1
|
||||||
return vehicles_st.ve_ids
|
return vehicles_st.ve_ids
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ export function vehicles_tick(vehicles_st: VehiclesState, dt: float) -> void {
|
||||||
if w > 0.0 { VehicleHull.drift(v.nid, -Math.sin(wy) * w, -Math.cos(wy) * w) }
|
if w > 0.0 { VehicleHull.drift(v.nid, -Math.sin(wy) * w, -Math.cos(wy) * w) }
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if not VehicleWorld.active(v.x, v.z) { continue } # outside every zone: left where it lies
|
||||||
ve_hull_read(v)
|
ve_hull_read(v)
|
||||||
ve_hold(v)
|
ve_hold(v)
|
||||||
VehicleWorld.placed(v)
|
VehicleWorld.placed(v)
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ export port WildlifeWorld {
|
||||||
taught: fn() -> bool = fn wl_no0 # food carries further, feeders mind less
|
taught: fn() -> bool = fn wl_no0 # food carries further, feeders mind less
|
||||||
about: fn(WildAnimal) -> int = fn wl_about # WILD_ABOUT, _AWAY or _GONE
|
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
|
ranges: fn(int) -> int = fn wl_two # ranges a species gets on this map
|
||||||
|
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
|
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
|
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
|
lures: fn() -> int = fn wl_none0 # how many things to ask `lure` about
|
||||||
|
|
@ -62,6 +63,7 @@ function wl_no0() -> bool { return false }
|
||||||
function wl_no1(p: int) -> bool { return false }
|
function wl_no1(p: int) -> bool { return false }
|
||||||
function wl_no2(x: float, z: float) -> bool { return false }
|
function wl_no2(x: float, z: float) -> bool { return false }
|
||||||
function wl_yes1(u: int) -> bool { return true }
|
function wl_yes1(u: int) -> bool { return true }
|
||||||
|
function wl_yes2(x: float, z: float) -> bool { return true }
|
||||||
function wl_clear(ax: float, ay: float, az: float, bx: float, by: float, bz: float) -> bool { return true }
|
function wl_clear(ax: float, ay: float, az: float, bx: float, by: float, bz: float) -> bool { return true }
|
||||||
function wl_no_push(x: float, z: float, r: float, y0: float, y1: float) -> bool { return false }
|
function wl_no_push(x: float, z: float, r: float, y0: float, y1: float) -> bool { return false }
|
||||||
function wl_asked_x(wildlife_st: WildlifeState) -> float { return wildlife_st.wl_ask_x }
|
function wl_asked_x(wildlife_st: WildlifeState) -> float { return wildlife_st.wl_ask_x }
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# run.ludic - one step of every animal: the port says whether it is about at this hour; anything
|
# run.ludic - one step of every animal inside a zone (WildlifeWorld.active): the port says whether it
|
||||||
# within 900 m of a player thinks; the gait follows the ground covered; player 0 sees what is near
|
# is about at this hour; within 900 m of a player it thinks; player 0 sees what is near
|
||||||
export function wildlife_tick(wildlife_st: mut WildlifeState, dt: float, gh: float) -> void {
|
export function wildlife_tick(wildlife_st: mut WildlifeState, dt: float, gh: float) -> void {
|
||||||
wl_seed_scan(wildlife_st, dt)
|
wl_seed_scan(wildlife_st, dt)
|
||||||
let all = wildlife_all(wildlife_st)
|
let all = wildlife_all(wildlife_st)
|
||||||
|
|
@ -9,6 +9,7 @@ export function wildlife_tick(wildlife_st: mut WildlifeState, dt: float, gh: flo
|
||||||
|
|
||||||
function wl_tick_one(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float, gh: float) -> void {
|
function wl_tick_one(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float, gh: float) -> void {
|
||||||
if not a.alive { return }
|
if not a.alive { return }
|
||||||
|
if not WildlifeWorld.active(a.x, a.z) { return } # outside every zone: its state kept, no dice drawn
|
||||||
let about = WildlifeWorld.about(a)
|
let about = WildlifeWorld.about(a)
|
||||||
if about == WILD_AWAY {
|
if about == WILD_AWAY {
|
||||||
a.shown = false
|
a.shown = false
|
||||||
|
|
|
||||||
16
packages/ludic.zones/README.md
Normal file
16
packages/ludic.zones/README.md
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# ludic.zones
|
||||||
|
|
||||||
|
Where a world is simulated: a disc per player - `(x, z, r)`, `r = zones_radius(fog)`, that is the
|
||||||
|
fog distance plus 40 m and never under 150 m, 900 m with no fog - and the discs that overlap merged
|
||||||
|
into clusters with a bounding circle. Uses `ludic.base` and nothing else.
|
||||||
|
|
||||||
|
- `zones_set(pid, x, z, r)`, `zones_clear(pid)`, `zones_reset()`; `zones_merge()` once a frame
|
||||||
|
after the discs are set, for the clusters.
|
||||||
|
- `zones_contains(x, z)`: inside any disc. `zones_nearest(x, z)`: metres to the nearest disc's
|
||||||
|
edge, 0 inside. With no disc set, everywhere is inside (0) - a world before its first frame
|
||||||
|
freezes nothing.
|
||||||
|
- `zones_clusters()`, `zones_cluster_x / _z / _r / _n(c)`, `zones_cluster_of(pid)`.
|
||||||
|
|
||||||
|
The rules it keeps: fixed arrays of `ZONES_MAX` (16) slots, nothing made after the state; the
|
||||||
|
questions allocate nothing. A world gates its ticks on `zones_contains` so what is outside every
|
||||||
|
zone keeps its state and draws no dice.
|
||||||
41
packages/ludic.zones/ask.ludic
Normal file
41
packages/ludic.zones/ask.ludic
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# ask.ludic - the questions: inside any disc, how far outside the nearest, and the clusters. With no
|
||||||
|
# disc set yet (a world before its first frame) everywhere is inside, so nothing freezes by accident
|
||||||
|
export function zones_any(zones_st: ZonesState) -> bool {
|
||||||
|
for i in 0 .. ZONES_MAX { if zones_st.zn_on[i] != 0 { return true } }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function zones_contains(zones_st: ZonesState, x: float, z: float) -> bool {
|
||||||
|
var any = false
|
||||||
|
for i in 0 .. ZONES_MAX {
|
||||||
|
if zones_st.zn_on[i] == 0 { continue }
|
||||||
|
any = true
|
||||||
|
let dx = x - zones_st.zn_x[i]
|
||||||
|
let dz = z - zones_st.zn_z[i]
|
||||||
|
if dx * dx + dz * dz <= zones_st.zn_r[i] * zones_st.zn_r[i] { return true }
|
||||||
|
}
|
||||||
|
return not any
|
||||||
|
}
|
||||||
|
|
||||||
|
# metres from (x, z) to the edge of the nearest disc: 0 inside one (and 0 with no disc set)
|
||||||
|
export function zones_nearest(zones_st: ZonesState, x: float, z: float) -> float {
|
||||||
|
var best = -1.0
|
||||||
|
for i in 0 .. ZONES_MAX {
|
||||||
|
if zones_st.zn_on[i] == 0 { continue }
|
||||||
|
let dx = x - zones_st.zn_x[i]
|
||||||
|
let dz = z - zones_st.zn_z[i]
|
||||||
|
let d = Math.max(Math.sqrt(dx * dx + dz * dz) - zones_st.zn_r[i], 0.0)
|
||||||
|
if best < 0.0 or d < best { best = d }
|
||||||
|
}
|
||||||
|
if best < 0.0 { return 0.0 }
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
|
export function zones_on(zones_st: ZonesState, pid: int) -> bool { return pid >= 0 and pid < ZONES_MAX and zones_st.zn_on[pid] != 0 }
|
||||||
|
export function zones_r(zones_st: ZonesState, pid: int) -> float { return zones_st.zn_r[pid] }
|
||||||
|
export function zones_cluster_of(zones_st: ZonesState, pid: int) -> int { return zones_st.zn_cl[pid] }
|
||||||
|
export function zones_clusters(zones_st: ZonesState) -> int { return zones_st.zn_ncl }
|
||||||
|
export function zones_cluster_x(zones_st: ZonesState, c: int) -> float { return zones_st.zn_cx[c] }
|
||||||
|
export function zones_cluster_z(zones_st: ZonesState, c: int) -> float { return zones_st.zn_cz[c] }
|
||||||
|
export function zones_cluster_r(zones_st: ZonesState, c: int) -> float { return zones_st.zn_cr[c] }
|
||||||
|
export function zones_cluster_n(zones_st: ZonesState, c: int) -> int { return zones_st.zn_cn[c] }
|
||||||
8
packages/ludic.zones/index.ludic
Normal file
8
packages/ludic.zones/index.ludic
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# ludic.zones - a disc per player (x, z, r), the discs that overlap merged into clusters with a
|
||||||
|
# bounding circle, and the questions a world asks of them; fixed arrays, nothing made per frame
|
||||||
|
module ludic_zones uses ludic_base
|
||||||
|
numbers float
|
||||||
|
import "ludic.base"
|
||||||
|
import "state.ludic"
|
||||||
|
import "merge.ludic"
|
||||||
|
import "ask.ludic"
|
||||||
70
packages/ludic.zones/merge.ludic
Normal file
70
packages/ludic.zones/merge.ludic
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
# merge.ludic - discs that overlap (or touch) are one cluster, transitively; a cluster's bounding
|
||||||
|
# circle is centred on its discs' box and reaches the farthest edge
|
||||||
|
export function zones_merge(zones_st: mut ZonesState) -> void {
|
||||||
|
for i in 0 .. ZONES_MAX { zones_st.zn_up[i] = i }
|
||||||
|
for i in 0 .. ZONES_MAX {
|
||||||
|
if zones_st.zn_on[i] == 0 { continue }
|
||||||
|
for j in i + 1 .. ZONES_MAX { if zones_st.zn_on[j] != 0 and zn_touch(zones_st, i, j) { zn_join(zones_st, i, j) } }
|
||||||
|
}
|
||||||
|
zones_st.zn_ncl = 0
|
||||||
|
for i in 0 .. ZONES_MAX { zones_st.zn_cl[i] = -1 }
|
||||||
|
for i in 0 .. ZONES_MAX {
|
||||||
|
if zones_st.zn_on[i] == 0 { continue }
|
||||||
|
let root = zn_root(zones_st, i)
|
||||||
|
if zones_st.zn_cl[root] < 0 {
|
||||||
|
zones_st.zn_cl[root] = zones_st.zn_ncl
|
||||||
|
zones_st.zn_ncl += 1
|
||||||
|
}
|
||||||
|
zones_st.zn_cl[i] = zones_st.zn_cl[root]
|
||||||
|
}
|
||||||
|
for c in 0 .. zones_st.zn_ncl { zn_bound(zones_st, c) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function zn_touch(zones_st: ZonesState, i: int, j: int) -> bool {
|
||||||
|
let dx = zones_st.zn_x[i] - zones_st.zn_x[j]
|
||||||
|
let dz = zones_st.zn_z[i] - zones_st.zn_z[j]
|
||||||
|
let rr = zones_st.zn_r[i] + zones_st.zn_r[j]
|
||||||
|
return dx * dx + dz * dz <= rr * rr
|
||||||
|
}
|
||||||
|
|
||||||
|
function zn_root(zones_st: ZonesState, i: int) -> int {
|
||||||
|
var k = i
|
||||||
|
while zones_st.zn_up[k] != k { k = zones_st.zn_up[k] }
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
function zn_join(zones_st: mut ZonesState, i: int, j: int) -> void {
|
||||||
|
let a = zn_root(zones_st, i)
|
||||||
|
let b = zn_root(zones_st, j)
|
||||||
|
if a < b { zones_st.zn_up[b] = a }
|
||||||
|
if b < a { zones_st.zn_up[a] = b }
|
||||||
|
}
|
||||||
|
|
||||||
|
function zn_bound(zones_st: mut ZonesState, c: int) -> void {
|
||||||
|
var x0 = 1000000000.0
|
||||||
|
var x1 = -1000000000.0
|
||||||
|
var z0 = 1000000000.0
|
||||||
|
var z1 = -1000000000.0
|
||||||
|
var n = 0
|
||||||
|
for i in 0 .. ZONES_MAX {
|
||||||
|
if zones_st.zn_on[i] == 0 or zones_st.zn_cl[i] != c { continue }
|
||||||
|
x0 = Math.min(x0, zones_st.zn_x[i] - zones_st.zn_r[i])
|
||||||
|
x1 = Math.max(x1, zones_st.zn_x[i] + zones_st.zn_r[i])
|
||||||
|
z0 = Math.min(z0, zones_st.zn_z[i] - zones_st.zn_r[i])
|
||||||
|
z1 = Math.max(z1, zones_st.zn_z[i] + zones_st.zn_r[i])
|
||||||
|
n += 1
|
||||||
|
}
|
||||||
|
let cx = (x0 + x1) * 0.5
|
||||||
|
let cz = (z0 + z1) * 0.5
|
||||||
|
var r = 0.0
|
||||||
|
for i in 0 .. ZONES_MAX {
|
||||||
|
if zones_st.zn_on[i] == 0 or zones_st.zn_cl[i] != c { continue }
|
||||||
|
let dx = zones_st.zn_x[i] - cx
|
||||||
|
let dz = zones_st.zn_z[i] - cz
|
||||||
|
r = Math.max(r, Math.sqrt(dx * dx + dz * dz) + zones_st.zn_r[i])
|
||||||
|
}
|
||||||
|
zones_st.zn_cx[c] = cx
|
||||||
|
zones_st.zn_cz[c] = cz
|
||||||
|
zones_st.zn_cr[c] = r
|
||||||
|
zones_st.zn_cn[c] = n
|
||||||
|
}
|
||||||
5
packages/ludic.zones/package.ludic
Normal file
5
packages/ludic.zones/package.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# ludic.zones - where the world is simulated: a disc per player, overlapping discs merged into
|
||||||
|
# clusters, asked whether a point is inside any. Uses ludic.base and nothing else.
|
||||||
|
package "ludic.zones"
|
||||||
|
version "0.1.0"
|
||||||
|
kind source
|
||||||
40
packages/ludic.zones/state.ludic
Normal file
40
packages/ludic.zones/state.ludic
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# state.ludic - the discs by player slot and the clusters the last merge made
|
||||||
|
export const ZONES_MAX: int = 16 # player slots a world can hold (at least PLY_MAX)
|
||||||
|
export const ZONES_NO_FOG: float = 900.0 # the radius of a player who sees to the horizon
|
||||||
|
export const ZONES_PAST_FOG: float = 40.0 # simulated this far past what a player can see
|
||||||
|
export const ZONES_LEAST: float = 150.0 # and never less than this
|
||||||
|
|
||||||
|
export state ZonesState {
|
||||||
|
zn_on: words = words(ZONES_MAX)
|
||||||
|
zn_x: []float = floats(ZONES_MAX)
|
||||||
|
zn_z: []float = floats(ZONES_MAX)
|
||||||
|
zn_r: []float = floats(ZONES_MAX)
|
||||||
|
zn_cl: words = words(ZONES_MAX) # each disc's cluster, after zones_merge
|
||||||
|
zn_up: words = words(ZONES_MAX) # the merge's union-find
|
||||||
|
zn_ncl: int = 0
|
||||||
|
zn_cx: []float = floats(ZONES_MAX) # each cluster's bounding circle
|
||||||
|
zn_cz: []float = floats(ZONES_MAX)
|
||||||
|
zn_cr: []float = floats(ZONES_MAX)
|
||||||
|
zn_cn: words = words(ZONES_MAX) # how many discs it holds
|
||||||
|
}
|
||||||
|
|
||||||
|
# a player's radius from how far they see: fog_dist <= 0 is no fog
|
||||||
|
export function zones_radius(fog_dist: float) -> float {
|
||||||
|
if fog_dist <= 0.0 { return ZONES_NO_FOG }
|
||||||
|
return Math.max(fog_dist + ZONES_PAST_FOG, ZONES_LEAST)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function zones_set(zones_st: mut ZonesState, pid: int, x: float, z: float, r: float) -> void {
|
||||||
|
if pid < 0 or pid >= ZONES_MAX { return }
|
||||||
|
zones_st.zn_on[pid] = 1
|
||||||
|
zones_st.zn_x[pid] = x
|
||||||
|
zones_st.zn_z[pid] = z
|
||||||
|
zones_st.zn_r[pid] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
export function zones_clear(zones_st: mut ZonesState, pid: int) -> void { if pid >= 0 and pid < ZONES_MAX { zones_st.zn_on[pid] = 0 } }
|
||||||
|
|
||||||
|
export function zones_reset(zones_st: mut ZonesState) -> void {
|
||||||
|
for i in 0 .. ZONES_MAX { zones_st.zn_on[i] = 0 }
|
||||||
|
zones_st.zn_ncl = 0
|
||||||
|
}
|
||||||
52
packages/ludic.zones/tests/zones_test.ludic
Normal file
52
packages/ludic.zones/tests/zones_test.ludic
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# zones_test.ludic - discs merged into clusters (near players one, far ones two, a player leaving
|
||||||
|
# splits the chain), inside or not, how far to the nearest edge, and the radius a fog gives
|
||||||
|
import "ludic.zones"
|
||||||
|
import "ludic.base"
|
||||||
|
program ZonesTest {
|
||||||
|
numbers float
|
||||||
|
|
||||||
|
test "two near players are one cluster, two far apart are two" (zones_st: mut ZonesState) {
|
||||||
|
zones_reset(zones_st)
|
||||||
|
zones_set(zones_st, 0, 0.0, 0.0, 200.0)
|
||||||
|
zones_set(zones_st, 1, 300.0, 0.0, 150.0)
|
||||||
|
zones_merge(zones_st)
|
||||||
|
expect_eq(zones_clusters(zones_st), 1)
|
||||||
|
expect_eq(zones_cluster_of(zones_st, 0), zones_cluster_of(zones_st, 1))
|
||||||
|
expect_eq(zones_cluster_n(zones_st, 0), 2)
|
||||||
|
expect(zones_cluster_r(zones_st, 0) >= 325.0)
|
||||||
|
zones_set(zones_st, 1, 1000.0, 0.0, 150.0)
|
||||||
|
zones_merge(zones_st)
|
||||||
|
expect_eq(zones_clusters(zones_st), 2)
|
||||||
|
expect(zones_cluster_of(zones_st, 0) != zones_cluster_of(zones_st, 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a player leaving the middle of a chain splits it" (zones_st: mut ZonesState) {
|
||||||
|
zones_reset(zones_st)
|
||||||
|
zones_set(zones_st, 0, 0.0, 0.0, 200.0)
|
||||||
|
zones_set(zones_st, 3, 350.0, 0.0, 200.0)
|
||||||
|
zones_set(zones_st, 5, 700.0, 0.0, 200.0)
|
||||||
|
zones_merge(zones_st)
|
||||||
|
expect_eq(zones_clusters(zones_st), 1)
|
||||||
|
zones_clear(zones_st, 3)
|
||||||
|
zones_merge(zones_st)
|
||||||
|
expect_eq(zones_clusters(zones_st), 2)
|
||||||
|
expect(zones_cluster_of(zones_st, 0) != zones_cluster_of(zones_st, 5))
|
||||||
|
}
|
||||||
|
|
||||||
|
test "inside, the distance to the nearest edge, and nothing set is everywhere" (zones_st: mut ZonesState) {
|
||||||
|
zones_reset(zones_st)
|
||||||
|
expect(zones_contains(zones_st, 5000.0, 5000.0))
|
||||||
|
expect(zones_nearest(zones_st, 5000.0, 5000.0) == 0.0)
|
||||||
|
zones_set(zones_st, 2, 100.0, 100.0, 150.0)
|
||||||
|
expect(zones_contains(zones_st, 200.0, 100.0))
|
||||||
|
expect(not zones_contains(zones_st, 300.0, 100.0))
|
||||||
|
expect(zones_nearest(zones_st, 200.0, 100.0) == 0.0)
|
||||||
|
expect(Math.abs(zones_nearest(zones_st, 350.0, 100.0) - 100.0) < 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a player's radius: past the fog, never under the least, the horizon with none" (zones_st: mut ZonesState) {
|
||||||
|
expect(zones_radius(0.0) == ZONES_NO_FOG)
|
||||||
|
expect(zones_radius(30.0) == ZONES_LEAST)
|
||||||
|
expect(zones_radius(500.0) == 540.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue