wip(0.S3): the packages migrated by ludic migrate state packages - every package test green

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 14:02:21 +03:00
parent 1e8b5b0523
commit 07505e7ef2
294 changed files with 14996 additions and 14675 deletions

View file

@ -6,50 +6,55 @@ export const AIM_REACH_MIN: float = 3.0 # about as far as a hand goes with a
export const AIM_USE_MAX: float = 10.0 # past this the crosshair is for looking, not doing export const AIM_USE_MAX: float = 10.0 # past this the crosshair is for looking, not doing
export const AIM_NEVER: float = -1.0 # a reach that is never in reach (an animal without a camera) export const AIM_NEVER: float = -1.0 # a reach that is never in reach (an animal without a camera)
var am_kind: int = 0 export state AimState {
var am_id: int = -1 am_kind: int = 0
var am_t: float = 10000.0 am_id: int = -1
var am_dist: float = 0.0 am_t: float = 10000.0
var am_reach: bool = false am_dist: float = 0.0
var am_off: float = 1.0 am_reach: bool = false
am_off: float = 1.0
am_seen: int = 0
am_blocked: int = 0
am_disagree: int = 0
}
# The crosshair decides WHICH; the distance decides only WHETHER. Every reach is lifted to a floor of # The crosshair decides WHICH; the distance decides only WHETHER. Every reach is lifted to a floor of
# AIM_REACH_MIN, because a per-kind reach chosen against its model was never chosen against standing # AIM_REACH_MIN, because a per-kind reach chosen against its model was never chosen against standing
# between two props and looking at one. # between two props and looking at one.
export function aim_reach_of(reach: float) -> float { return Math.max(reach, AIM_REACH_MIN) } export function aim_reach_of(reach: float) -> float { return Math.max(reach, AIM_REACH_MIN) }
export function aim_begin() -> void { export function aim_begin(aim_st: mut AimState) -> void {
am_kind = AIM_NONE aim_st.am_kind = AIM_NONE
am_id = -1 aim_st.am_id = -1
am_t = 10000.0 aim_st.am_t = 10000.0
am_dist = 0.0 aim_st.am_dist = 0.0
am_reach = false aim_st.am_reach = false
am_off = 1.0 aim_st.am_off = 1.0
} }
# A candidate of `kind` (the game's own, not AIM_NONE) and `id`: an upright cylinder at (x, z) from y0 # A candidate of `kind` (the game's own, not AIM_NONE) and `id`: an upright cylinder at (x, z) from y0
# to y1 of radius r, within `far` of the body, in reach within `reach` (floored; AIM_NEVER never). # to y1 of radius r, within `far` of the body, in reach within `reach` (floored; AIM_NEVER never).
# True when it is now the pick. # True when it is now the pick.
export function aim_offer(kind: int, id: int, x: float, y0: float, y1: float, z: float, r: float, reach: float, far: float) -> bool { export function aim_offer(aim_st: mut AimState, kind: int, id: int, x: float, y0: float, y1: float, z: float, r: float, reach: float, far: float) -> bool {
let d = aim_range(x, z) let d = aim_range(x, z)
if d > far { return false } if d > far { return false }
let rr = r + aim_slack_m(d) let rr = r + aim_slack_m(d)
let hit = aim_ray_cyl(x, y0, y1, z, rr) let hit = aim_ray_cyl(x, y0, y1, z, rr)
if hit < 0.0 or not (hit < am_t) { return false } if hit < 0.0 or not (hit < aim_st.am_t) { return false }
if not aim_clear_to(x, (y0 + y1) * 0.5, z) { return false } if not aim_clear_to(x, (y0 + y1) * 0.5, z) { return false }
am_t = hit aim_st.am_t = hit
am_off = aim_miss(x, z, rr) aim_st.am_off = aim_miss(x, z, rr)
am_kind = kind aim_st.am_kind = kind
am_id = id aim_st.am_id = id
am_dist = d aim_st.am_dist = d
am_reach = reach >= 0.0 and d < aim_reach_of(reach) aim_st.am_reach = reach >= 0.0 and d < aim_reach_of(reach)
return true return true
} }
export function aim_kind() -> int { return am_kind } export function aim_kind(aim_st: AimState) -> int { return aim_st.am_kind }
export function aim_id() -> int { return am_id } export function aim_id(aim_st: AimState) -> int { return aim_st.am_id }
export function aim_dist() -> float { return am_dist } export function aim_dist(aim_st: AimState) -> float { return aim_st.am_dist }
export function aim_in_reach() -> bool { return am_reach } export function aim_in_reach(aim_st: AimState) -> bool { return aim_st.am_reach }
export function aim_t() -> float { return am_t } export function aim_t(aim_st: AimState) -> float { return aim_st.am_t }
# how well centred the pick is: 0 dead centre, 1 at its (forgiven) edge # how well centred the pick is: 0 dead centre, 1 at its (forgiven) edge
export function aim_off_centre() -> float { return am_off } export function aim_off_centre(aim_st: AimState) -> float { return aim_st.am_off }

View file

@ -3,19 +3,16 @@
# the ground under each line against aim_clear_to. Lines the ground merely grazes are left out: that # the ground under each line against aim_clear_to. Lines the ground merely grazes are left out: that
# is the probe's sampling against the aim's, not a disagreement. Moves the camera (AimView's) through # is the probe's sampling against the aim's, not a disagreement. Moves the camera (AimView's) through
# `look`, which the game binds to put its camera at a point. # `look`, which the game binds to put its camera at a point.
var am_seen: int = 0
var am_blocked: int = 0
var am_disagree: int = 0
export function aim_probe_seen() -> int { return am_seen } export function aim_probe_seen(aim_st: AimState) -> int { return aim_st.am_seen }
export function aim_probe_blocked() -> int { return am_blocked } export function aim_probe_blocked(aim_st: AimState) -> int { return aim_st.am_blocked }
export function aim_probe_disagree() -> int { return am_disagree } export function aim_probe_disagree(aim_st: AimState) -> int { return aim_st.am_disagree }
# true when at least one line is hidden and the aim disagrees with the ground on at most one in fifty # true when at least one line is hidden and the aim disagrees with the ground on at most one in fifty
export function aim_probe_ground(px: float, pz: float, look: fn(float, float, float) -> void) -> bool { export function aim_probe_ground(aim_st: mut AimState, px: float, pz: float, look: fn(float, float, float) -> void) -> bool {
am_seen = 0 aim_st.am_seen = 0
am_blocked = 0 aim_st.am_blocked = 0
am_disagree = 0 aim_st.am_disagree = 0
for sp in 0 .. 9 { for sp in 0 .. 9 {
var sx = px var sx = px
var sz = pz var sz = pz
@ -24,12 +21,12 @@ export function aim_probe_ground(px: float, pz: float, look: fn(float, float, fl
sx = px + Math.sin(sa) * 250.0 sx = px + Math.sin(sa) * 250.0
sz = pz + Math.cos(sa) * 250.0 sz = pz + Math.cos(sa) * 250.0
} }
if AimView.dry(sx, sz) { am_probe_from(sx, AimView.ground(sx, sz) + 1.6, sz, look) } if AimView.dry(sx, sz) { am_probe_from(aim_st, sx, AimView.ground(sx, sz) + 1.6, sz, look) }
} }
return am_blocked >= 1 and not (am_disagree > am_seen / 50) return aim_st.am_blocked >= 1 and not (aim_st.am_disagree > aim_st.am_seen / 50)
} }
function am_probe_from(sx: float, sy: float, sz: float, look: fn(float, float, float) -> void) -> void { function am_probe_from(aim_st: mut AimState, sx: float, sy: float, sz: float, look: fn(float, float, float) -> void) -> void {
for di in 0 .. 3 { for di in 0 .. 3 {
var dist = 60.0 var dist = 60.0
if di == 1 { dist = 120.0 } if di == 1 { dist = 120.0 }
@ -42,10 +39,10 @@ function am_probe_from(sx: float, sy: float, sz: float, look: fn(float, float, f
let truth = am_probe_gap(sx, sy, sz, ex, ey, ez, 0.5, 10000.0) let truth = am_probe_gap(sx, sy, sz, ex, ey, ez, 0.5, 10000.0)
if am_probe_gap(sx, sy, sz, ex, ey, ez, -0.5, 0.5) and not truth { continue } if am_probe_gap(sx, sy, sz, ex, ey, ez, -0.5, 0.5) and not truth { continue }
look(sx, sy, sz) look(sx, sy, sz)
am_seen += 1 aim_st.am_seen += 1
let clear = aim_clear_to(ex, ey, ez) let clear = aim_clear_to(ex, ey, ez)
if not clear { am_blocked += 1 } if not clear { aim_st.am_blocked += 1 }
if clear == truth { am_disagree += 1 } if clear == truth { aim_st.am_disagree += 1 }
} }
} }
} }

View file

@ -6,34 +6,36 @@ import "ludic.base"
program AimTest { program AimTest {
numbers float numbers float
var cx: float = 0.0 state AimtestState {
var cy: float = 1.5 cx: float = 0.0
var cz: float = 0.0 cy: float = 1.5
var fy: float = 0.0 cz: float = 0.0
function fk_x() -> float { return cx } fy: float = 0.0
function fk_y() -> float { return cy } }
function fk_z() -> float { return cz } function fk_x(aimtest_st: AimtestState) -> float { return aimtest_st.cx }
function fk_fy() -> float { return fy } function fk_y(aimtest_st: AimtestState) -> float { return aimtest_st.cy }
function fk_fz() -> float { return -Math.sqrt(1.0 - fy * fy) } function fk_z(aimtest_st: AimtestState) -> float { return aimtest_st.cz }
function fk_fy(aimtest_st: AimtestState) -> float { return aimtest_st.fy }
function fk_fz(aimtest_st: AimtestState) -> float { return -Math.sqrt(1.0 - aimtest_st.fy * aimtest_st.fy) }
# a 6 m ridge along z = -42, thirty metres across, the whole width of the view # a 6 m ridge along z = -42, thirty metres across, the whole width of the view
function fk_ground(x: float, z: float) -> float { return 6.0 * Math.max(0.0, 1.0 - Math.abs(z + 42.0) / 15.0) } function fk_ground(x: float, z: float) -> float { return 6.0 * Math.max(0.0, 1.0 - Math.abs(z + 42.0) / 15.0) }
function fk_look(x: float, y: float, z: float) -> void { function fk_look(aimtest_st: mut AimtestState, x: float, y: float, z: float) -> void {
cx = x aimtest_st.cx = x
cy = y aimtest_st.cy = y
cz = z aimtest_st.cz = z
} }
bind AimView { x: fn fk_x, y: fn fk_y, z: fn fk_z, fy: fn fk_fy, fz: fn fk_fz, ground: fn fk_ground } bind AimView { x: fn fk_x, y: fn fk_y, z: fn fk_z, fy: fn fk_fy, fz: fn fk_fz, ground: fn fk_ground }
function fresh() -> void { function fresh(aim_st: mut AimState, aimtest_st: mut AimtestState) -> void {
cx = 0.0 aimtest_st.cx = 0.0
cy = 1.5 aimtest_st.cy = 1.5
cz = 0.0 aimtest_st.cz = 0.0
fy = 0.0 aimtest_st.fy = 0.0
aim_begin() aim_begin(aim_st)
} }
test "the ray enters a cylinder in front and misses one to the side or behind" { test "the ray enters a cylinder in front and misses one to the side or behind" (aim_st: mut AimState, aimtest_st: mut AimtestState) {
fresh() fresh(aim_st, aimtest_st)
expect_near(aim_ray_cyl(0.0, 0.0, 2.0, -10.0, 0.5), 9.5, 0.01) expect_near(aim_ray_cyl(0.0, 0.0, 2.0, -10.0, 0.5), 9.5, 0.01)
expect(aim_ray_cyl(3.0, 0.0, 2.0, -10.0, 0.5) < 0.0) expect(aim_ray_cyl(3.0, 0.0, 2.0, -10.0, 0.5) < 0.0)
expect(aim_ray_cyl(0.0, 0.0, 2.0, 10.0, 0.5) < 0.0) expect(aim_ray_cyl(0.0, 0.0, 2.0, 10.0, 0.5) < 0.0)
@ -41,61 +43,61 @@ program AimTest {
expect_near(aim_ray_cyl(0.0, 0.0, 2.0, 0.0, 0.5), 0.0, 0.001) expect_near(aim_ray_cyl(0.0, 0.0, 2.0, 0.0, 0.5), 0.0, 0.001)
} }
test "a thin thing far off is still aimable, by an angle and not a metre" { test "a thin thing far off is still aimable, by an angle and not a metre" (aim_st: mut AimState, aimtest_st: mut AimtestState) {
fresh() fresh(aim_st, aimtest_st)
expect(aim_slack_m(20.0) > aim_slack_m(5.0) * 3.9) expect(aim_slack_m(20.0) > aim_slack_m(5.0) * 3.9)
expect(aim_offer(1, 7, 0.25, 0.0, 2.0, -20.0, 0.05, 1.5, 30.0)) expect(aim_offer(aim_st, 1, 7, 0.25, 0.0, 2.0, -20.0, 0.05, 1.5, 30.0))
expect(aim_off_centre() > 0.5) expect(aim_off_centre(aim_st) > 0.5)
fresh() fresh(aim_st, aimtest_st)
expect(not aim_offer(1, 7, 3.0, 0.0, 2.0, -20.0, 0.05, 1.5, 30.0)) expect(not aim_offer(aim_st, 1, 7, 3.0, 0.0, 2.0, -20.0, 0.05, 1.5, 30.0))
} }
test "the nearest thing the ray enters is the pick, whatever order they are offered in" { test "the nearest thing the ray enters is the pick, whatever order they are offered in" (aim_st: mut AimState, aimtest_st: mut AimtestState) {
fresh() fresh(aim_st, aimtest_st)
expect(aim_offer(1, 1, 0.0, 0.0, 2.0, -20.0, 0.5, 1.5, 30.0)) expect(aim_offer(aim_st, 1, 1, 0.0, 0.0, 2.0, -20.0, 0.5, 1.5, 30.0))
expect(aim_offer(1, 2, 0.0, 0.0, 2.0, -8.0, 0.5, 1.5, 30.0)) expect(aim_offer(aim_st, 1, 2, 0.0, 0.0, 2.0, -8.0, 0.5, 1.5, 30.0))
expect(not aim_offer(1, 3, 0.0, 0.0, 2.0, -15.0, 0.5, 1.5, 30.0)) expect(not aim_offer(aim_st, 1, 3, 0.0, 0.0, 2.0, -15.0, 0.5, 1.5, 30.0))
expect_eq(aim_id(), 2) expect_eq(aim_id(aim_st), 2)
expect_near(aim_dist(), 8.0, 0.01) expect_near(aim_dist(aim_st), 8.0, 0.01)
expect(not aim_offer(2, 4, 0.0, 0.0, 2.0, -25.0, 0.5, 1.5, 30.0)) expect(not aim_offer(aim_st, 2, 4, 0.0, 0.0, 2.0, -25.0, 0.5, 1.5, 30.0))
expect(not aim_offer(2, 5, 0.0, 0.0, 2.0, -3.0, 0.5, 1.5, 2.0)) expect(not aim_offer(aim_st, 2, 5, 0.0, 0.0, 2.0, -3.0, 0.5, 1.5, 2.0))
} }
test "the ground in front hides a thing, and nothing within arm's length is hidden" { test "the ground in front hides a thing, and nothing within arm's length is hidden" (aim_st: mut AimState, aimtest_st: mut AimtestState) {
fresh() fresh(aim_st, aimtest_st)
expect(not aim_clear_to(0.0, 1.0, -60.0)) expect(not aim_clear_to(0.0, 1.0, -60.0))
expect(aim_clear_to(0.0, 1.0, -30.0)) expect(aim_clear_to(0.0, 1.0, -30.0))
expect(not aim_offer(1, 1, 0.0, 0.0, 2.0, -60.0, 2.0, 1.5, 80.0)) expect(not aim_offer(aim_st, 1, 1, 0.0, 0.0, 2.0, -60.0, 2.0, 1.5, 80.0))
cy = -5.0 aimtest_st.cy = -5.0
expect(aim_clear_to(0.0, -5.0, -2.0)) expect(aim_clear_to(0.0, -5.0, -2.0))
} }
test "the crosshair decides which, the distance whether: every reach is floored at three metres" { test "the crosshair decides which, the distance whether: every reach is floored at three metres" (aim_st: mut AimState, aimtest_st: mut AimtestState) {
fresh() fresh(aim_st, aimtest_st)
expect_near(aim_reach_of(1.5), AIM_REACH_MIN, 0.001) expect_near(aim_reach_of(1.5), AIM_REACH_MIN, 0.001)
expect_near(aim_reach_of(3.4), 3.4, 0.001) expect_near(aim_reach_of(3.4), 3.4, 0.001)
aim_offer(1, 1, 0.0, 0.0, 2.0, -2.6, 0.3, 1.5, 30.0) aim_offer(aim_st, 1, 1, 0.0, 0.0, 2.0, -2.6, 0.3, 1.5, 30.0)
expect(aim_in_reach()) expect(aim_in_reach(aim_st))
expect_eq(aim_use(1), AIM_USE_THIS) expect_eq(aim_use(aim_st, 1), AIM_USE_THIS)
fresh() fresh(aim_st, aimtest_st)
aim_offer(1, 1, 0.0, 0.0, 2.0, -6.0, 0.3, 1.5, 30.0) aim_offer(aim_st, 1, 1, 0.0, 0.0, 2.0, -6.0, 0.3, 1.5, 30.0)
expect(not aim_in_reach()) expect(not aim_in_reach(aim_st))
expect_eq(aim_use(1), AIM_USE_NOTHING) expect_eq(aim_use(aim_st, 1), AIM_USE_NOTHING)
expect_eq(aim_use(3), AIM_USE_NEAREST) expect_eq(aim_use(aim_st, 3), AIM_USE_NEAREST)
fresh() fresh(aim_st, aimtest_st)
aim_offer(1, 1, 0.0, 0.0, 3.0, -20.0, 0.3, 1.5, 30.0) aim_offer(aim_st, 1, 1, 0.0, 0.0, 3.0, -20.0, 0.3, 1.5, 30.0)
expect_eq(aim_use(1), AIM_USE_NEAREST) expect_eq(aim_use(aim_st, 1), AIM_USE_NEAREST)
fresh() fresh(aim_st, aimtest_st)
aim_offer(2, 9, 0.0, 0.0, 2.0, -2.0, 0.3, AIM_NEVER, 30.0) aim_offer(aim_st, 2, 9, 0.0, 0.0, 2.0, -2.0, 0.3, AIM_NEVER, 30.0)
expect(not aim_in_reach()) expect(not aim_in_reach(aim_st))
expect_eq(aim_kind(), 2) expect_eq(aim_kind(aim_st), 2)
} }
test "the probe finds the bank and agrees with the ground about every line it hides" { test "the probe finds the bank and agrees with the ground about every line it hides" (aim_st: mut AimState, aimtest_st: mut AimtestState) {
fresh() fresh(aim_st, aimtest_st)
expect(aim_probe_ground(0.0, 0.0, fn fk_look)) expect(aim_probe_ground(aim_st, 0.0, 0.0, fn fk_look))
expect(aim_probe_seen() > 100) expect(aim_probe_seen(aim_st) > 100)
expect(aim_probe_blocked() > 0) expect(aim_probe_blocked(aim_st) > 0)
expect(not (aim_probe_disagree() > aim_probe_seen() / 50)) expect(not (aim_probe_disagree(aim_st) > aim_probe_seen(aim_st) / 50))
} }
} }

View file

@ -8,8 +8,8 @@ export const AIM_USE_THIS: int = 0
export const AIM_USE_NOTHING: int = 1 export const AIM_USE_NOTHING: int = 1
export const AIM_USE_NEAREST: int = 2 export const AIM_USE_NEAREST: int = 2
export function aim_use(kind: int) -> int { export function aim_use(aim_st: AimState, kind: int) -> int {
if am_kind == kind and am_reach { return AIM_USE_THIS } if aim_st.am_kind == kind and aim_st.am_reach { return AIM_USE_THIS }
if am_kind == kind and am_dist < AIM_USE_MAX { return AIM_USE_NOTHING } if aim_st.am_kind == kind and aim_st.am_dist < AIM_USE_MAX { return AIM_USE_NOTHING }
return AIM_USE_NEAREST return AIM_USE_NEAREST
} }

View file

@ -16,37 +16,47 @@ export property Clip {
vals: floats vals: floats
} }
var clips: []Clip = null export state AnimState {
clips: []Clip = null
positions: bool = false
q_a: floats = null
q_b: floats = null
q_cur: floats = null
parts: []floats = null
rot_s: floats = null
pos_s: floats = null
hit_s: words = null
cap: int = 0
}
# Keyed translations are off unless asked for: baking keys location on every bone, and a skeleton # Keyed translations are off unless asked for: baking keys location on every bone, and a skeleton
# does not translate its joints, so applying them turns rounding into a pose. # does not translate its joints, so applying them turns rounding into a pose.
var positions: bool = false
function clips_init() -> void { function clips_init(anim_st: mut AnimState) -> void {
if clips == null { clips = new []Clip } if anim_st.clips == null { anim_st.clips = new []Clip }
} }
# the clip of that name, or null; names are per file (per body, per species), never shared # the clip of that name, or null; names are per file (per body, per species), never shared
export function anim_find(name: string) -> Clip { export function anim_find(anim_st: mut AnimState, name: string) -> Clip {
clips_init() clips_init(anim_st)
for i in 0 .. len(clips) { if clips[i].name == name { return clips[i] } } for i in 0 .. len(anim_st.clips) { if anim_st.clips[i].name == name { return anim_st.clips[i] } }
return null return null
} }
export function anim_count() -> int { export function anim_count(anim_st: mut AnimState) -> int {
clips_init() clips_init(anim_st)
return len(clips) return len(anim_st.clips)
} }
# a clip made outside a glTF (a test, a generated clip); a name already held is refused # a clip made outside a glTF (a test, a generated clip); a name already held is refused
export function anim_add(c: Clip) -> bool { export function anim_add(anim_st: mut AnimState, c: Clip) -> bool {
if c == null or anim_find(c.name) != null { return false } if c == null or anim_find(anim_st, c.name) != null { return false }
push(clips, c) push(anim_st.clips, c)
return true return true
} }
export function anim_clear() -> void { clips = new []Clip } export function anim_clear(anim_st: mut AnimState) -> void { anim_st.clips = new []Clip }
export function anim_set_positions(on: bool) -> void { positions = on } export function anim_set_positions(anim_st: mut AnimState, on: bool) -> void { anim_st.positions = on }
export function anim_positions() -> bool { return positions } export function anim_positions(anim_st: AnimState) -> bool { return anim_st.positions }
# Which key pair straddles time t, by halving rather than walking: a grazing clip is three # Which key pair straddles time t, by halving rather than walking: a grazing clip is three
# hundred keys long and a valley can have dozens of animals posed in a frame. # hundred keys long and a valley can have dozens of animals posed in a frame.

View file

@ -1,16 +1,13 @@
# mix.ludic - sample one clip at a time into per-node buffers, mixed in at a weight. `rot` is 4 per # mix.ludic - sample one clip at a time into per-node buffers, mixed in at a weight. `rot` is 4 per
# node, `pos` 3 per node, and `hit` says which nodes have received a value (bit 0 a rotation, bit 1 # node, `pos` 3 per node, and `hit` says which nodes have received a value (bit 0 a rotation, bit 1
# a translation), so a node the clips never mention stays at rest # a translation), so a node the clips never mention stays at rest
var q_a: floats = null
var q_b: floats = null
var q_cur: floats = null
export function anim_mix(c: Clip, sk: Skin, t: float, w: float, rot: floats, pos: floats, hit: words) -> void { export function anim_mix(anim_st: mut AnimState, c: Clip, sk: Skin, t: float, w: float, rot: floats, pos: floats, hit: words) -> void {
if c == null or sk == null { return } if c == null or sk == null { return }
if q_a == null { if anim_st.q_a == null {
q_a = floats(4) anim_st.q_a = floats(4)
q_b = floats(4) anim_st.q_b = floats(4)
q_cur = floats(4) anim_st.q_cur = floats(4)
} }
var tt = t var tt = t
if c.dur > 0.0 { tt = t % c.dur } if c.dur > 0.0 { tt = t % c.dur }
@ -27,27 +24,27 @@ export function anim_mix(c: Clip, sk: Skin, t: float, w: float, rot: floats, pos
let span = c.times[o + i1] - c.times[o + i0] let span = c.times[o + i1] - c.times[o + i0]
if span > 0.000001 { f = Math.clamp((tt - c.times[o + i0]) / span, 0.0, 1.0) } if span > 0.000001 { f = Math.clamp((tt - c.times[o + i0]) / span, 0.0, 1.0) }
} }
if c.cpath[ch] == AC_ROT { mix_rot(c, ch, node, i0, i1, f, w, rot, hit) } else { mix_pos(c, ch, node, i0, i1, f, w, pos, hit) } if c.cpath[ch] == AC_ROT { mix_rot(anim_st, c, ch, node, i0, i1, f, w, rot, hit) } else { mix_pos(c, ch, node, i0, i1, f, w, pos, hit) }
} }
} }
# Across clips the first contributor lands and the rest are nlerped onto it by their share, which # Across clips the first contributor lands and the rest are nlerped onto it by their share, which
# is what makes a cross-fade rather than a jump. Test the ROTATION bit, not the whole word: glTF # is what makes a cross-fade rather than a jump. Test the ROTATION bit, not the whole word: glTF
# writes a node's translation before its rotation, and `hit == 0` then skipped every rotation. # writes a node's translation before its rotation, and `hit == 0` then skipped every rotation.
function mix_rot(c: Clip, ch: int, node: int, i0: int, i1: int, f: float, w: float, rot: floats, hit: words) -> void { function mix_rot(anim_st: mut AnimState, c: Clip, ch: int, node: int, i0: int, i1: int, f: float, w: float, rot: floats, hit: words) -> void {
let vo = c.cvoff[ch] let vo = c.cvoff[ch]
for e in 0 .. 4 { for e in 0 .. 4 {
q_a[e] = c.vals[vo + i0 * 4 + e] anim_st.q_a[e] = c.vals[vo + i0 * 4 + e]
q_b[e] = c.vals[vo + i1 * 4 + e] anim_st.q_b[e] = c.vals[vo + i1 * 4 + e]
} }
q_nlerp(q_a, q_a, q_b, f) q_nlerp(anim_st.q_a, anim_st.q_a, anim_st.q_b, f)
if (hit[node] & 1) == 0 { if (hit[node] & 1) == 0 {
q_store(rot, node, q_a) q_store(rot, node, anim_st.q_a)
hit[node] = hit[node] | 1 hit[node] = hit[node] | 1
} else { } else {
q_load(q_cur, rot, node) q_load(anim_st.q_cur, rot, node)
q_nlerp(q_cur, q_cur, q_a, w) q_nlerp(anim_st.q_cur, anim_st.q_cur, anim_st.q_a, w)
q_store(rot, node, q_cur) q_store(rot, node, anim_st.q_cur)
} }
} }

View file

@ -1,28 +1,23 @@
# pose.ludic - the sampled LOCAL transforms turned into the model-frame deltas `skin_pose` wants. # pose.ludic - the sampled LOCAL transforms turned into the model-frame deltas `skin_pose` wants.
# skin_pose folds a delta D in as local = (G_p^-1 . D . G_p) . R_rest, and a channel gives the # skin_pose folds a delta D in as local = (G_p^-1 . D . G_p) . R_rest, and a channel gives the
# absolute local L, so D = G_p . (L . R_rest^-1) . G_p^-1 (a root: D = L . R_rest^-1) # absolute local L, so D = G_p . (L . R_rest^-1) . G_p^-1 (a root: D = L . R_rest^-1)
var parts: []floats = null
var rot_s: floats = null
var pos_s: floats = null
var hit_s: words = null
var cap: int = 0
function part(i: int) -> floats { function part(anim_st: mut AnimState, i: int) -> floats {
if parts == null { if anim_st.parts == null {
parts = new []floats anim_st.parts = new []floats
for k in 0 .. 8 { push(parts, floats(4)) } for k in 0 .. 8 { push(anim_st.parts, floats(4)) }
} }
return parts[i] return anim_st.parts[i]
} }
export function anim_to_pose(sk: Skin, rot: floats, pos: floats, hit: words) -> void { export function anim_to_pose(anim_st: mut AnimState, sk: Skin, rot: floats, pos: floats, hit: words) -> void {
let l = part(0) let l = part(anim_st, 0)
let rinv = part(1) let rinv = part(anim_st, 1)
let gp = part(2) let gp = part(anim_st, 2)
let gpi = part(3) let gpi = part(anim_st, 3)
let tmp = part(4) let tmp = part(anim_st, 4)
let d = part(5) let d = part(anim_st, 5)
let v = part(6) let v = part(anim_st, 6)
for i in 0 .. sk.n_nodes { for i in 0 .. sk.n_nodes {
let p = sk.par[i] let p = sk.par[i]
if (hit[i] & 1) != 0 { if (hit[i] & 1) != 0 {
@ -39,7 +34,7 @@ export function anim_to_pose(sk: Skin, rot: floats, pos: floats, hit: words) ->
q_normalize(d) q_normalize(d)
skin_set_quat(sk, i, d) skin_set_quat(sk, i, d)
} }
if (hit[i] & 2) != 0 and positions { if (hit[i] & 2) != 0 and anim_st.positions {
let dx = pos[i * 3] - sk.rest_t[i * 3] let dx = pos[i * 3] - sk.rest_t[i * 3]
let dy = pos[i * 3 + 1] - sk.rest_t[i * 3 + 1] let dy = pos[i * 3 + 1] - sk.rest_t[i * 3 + 1]
let dz = pos[i * 3 + 2] - sk.rest_t[i * 3 + 2] let dz = pos[i * 3 + 2] - sk.rest_t[i * 3 + 2]
@ -54,25 +49,25 @@ export function anim_to_pose(sk: Skin, rot: floats, pos: floats, hit: words) ->
} }
# The scratch is made ONCE and grown, never allocated per call: a call is per actor per frame. # The scratch is made ONCE and grown, never allocated per call: a call is per actor per frame.
function scratch(n: int) -> void { function scratch(anim_st: mut AnimState, n: int) -> void {
if cap >= n { return } if anim_st.cap >= n { return }
rot_s = floats(n * 4) anim_st.rot_s = floats(n * 4)
pos_s = floats(n * 3) anim_st.pos_s = floats(n * 3)
hit_s = words(n) anim_st.hit_s = words(n)
cap = n anim_st.cap = n
} }
# Play `a` at time `ta`, cross-faded toward `b` at `tb` by `blend` (0 is all a, 1 is all b), and # Play `a` at time `ta`, cross-faded toward `b` at `tb` by `blend` (0 is all a, 1 is all b), and
# fold it into the joint matrices. `b` may be null. False when there is no skin or no clip. # fold it into the joint matrices. `b` may be null. False when there is no skin or no clip.
export function anim_play(sk: Skin, a: Clip, ta: float, b: Clip, tb: float, blend: float) -> bool { export function anim_play(anim_st: mut AnimState, sk: Skin, a: Clip, ta: float, b: Clip, tb: float, blend: float) -> bool {
if sk == null or a == null { return false } if sk == null or a == null { return false }
let n = sk.n_nodes let n = sk.n_nodes
scratch(n) scratch(anim_st, n)
for i in 0 .. n { hit_s[i] = 0 } for i in 0 .. n { anim_st.hit_s[i] = 0 }
skin_reset(sk) skin_reset(sk)
anim_mix(a, sk, ta, 1.0, rot_s, pos_s, hit_s) anim_mix(anim_st, a, sk, ta, 1.0, anim_st.rot_s, anim_st.pos_s, anim_st.hit_s)
if b != null and blend > 0.001 { anim_mix(b, sk, tb, blend, rot_s, pos_s, hit_s) } if b != null and blend > 0.001 { anim_mix(anim_st, b, sk, tb, blend, anim_st.rot_s, anim_st.pos_s, anim_st.hit_s) }
anim_to_pose(sk, rot_s, pos_s, hit_s) anim_to_pose(anim_st, sk, anim_st.rot_s, anim_st.pos_s, anim_st.hit_s)
skin_pose(sk) skin_pose(sk)
return true return true
} }

View file

@ -3,31 +3,31 @@
# Call it straight after a load and before the next one: `gltf_doc` and `gltf_bin` point at # Call it straight after a load and before the next one: `gltf_doc` and `gltf_bin` point at
# whichever file was read last, so a clip that is not taken now cannot be taken later. # whichever file was read last, so a clip that is not taken now cannot be taken later.
export function anim_read_doc(dir: string) -> int { export function anim_read_doc(anim_st: mut AnimState, render3d_st: mut Render3dState, dir: string) -> int {
clips_init() clips_init(anim_st)
if gltf_doc == null { return 0 } if render3d_st.gltf_doc == null { return 0 }
if value_has(gltf_doc, "animations") == 0 { return 0 } if value_has(render3d_st.gltf_doc, "animations") == 0 { return 0 }
# REOPEN THE .bin. `gltf_load` closes it before it returns, and an accessor read through a closed # REOPEN THE .bin. `gltf_load` closes it before it returns, and an accessor read through a closed
# handle returns a buffer of the right size full of zeros: right names, right counts, no motion. # handle returns a buffer of the right size full of zeros: right names, right counts, no motion.
let buffers = value_get(gltf_doc, "buffers") let buffers = value_get(render3d_st.gltf_doc, "buffers")
if value_count(buffers) == 0 { return 0 } if value_count(buffers) == 0 { return 0 }
let bin_uri = value_as_str(value_get(value_at(buffers, 0), "uri")) let bin_uri = value_as_str(value_get(value_at(buffers, 0), "uri"))
gltf_bin = file_open(dir + "/" + bin_uri, "rb") render3d_st.gltf_bin = file_open(dir + "/" + bin_uri, "rb")
if gltf_bin == null { if render3d_st.gltf_bin == null {
print(`anim: cannot reopen {dir}/{bin_uri}`) print(`anim: cannot reopen {dir}/{bin_uri}`)
return 0 return 0
} }
let arr = value_get(gltf_doc, "animations") let arr = value_get(render3d_st.gltf_doc, "animations")
var got = 0 var got = 0
for ai in 0 .. value_count(arr) { for ai in 0 .. value_count(arr) {
let c = read_one(value_at(arr, ai)) let c = read_one(anim_st, render3d_st, value_at(arr, ai))
if c != null { if c != null {
push(clips, c) push(anim_st.clips, c)
got += 1 got += 1
} }
} }
file_close(gltf_bin) file_close(render3d_st.gltf_bin)
gltf_bin = null render3d_st.gltf_bin = null
return got return got
} }
@ -39,10 +39,10 @@ function kept_path(ch: Val) -> string {
return path return path
} }
function read_one(a: Val) -> Clip { function read_one(anim_st: mut AnimState, render3d_st: mut Render3dState, a: Val) -> Clip {
var nm = "" var nm = ""
if value_has(a, "name") != 0 { nm = value_as_str(value_get(a, "name")) } if value_has(a, "name") != 0 { nm = value_as_str(value_get(a, "name")) }
if anim_find(nm) != null { return null } # a second rig's file, same clip if anim_find(anim_st, nm) != null { return null } # a second rig's file, same clip
let chans = value_get(a, "channels") let chans = value_get(a, "channels")
let samps = value_get(a, "samplers") let samps = value_get(a, "samplers")
let nc = value_count(chans) let nc = value_count(chans)
@ -55,7 +55,7 @@ function read_one(a: Val) -> Clip {
let path = kept_path(ch) let path = kept_path(ch)
if path == "" { continue } if path == "" { continue }
let sp = value_at(samps, value_as_int(value_get(ch, "sampler"))) let sp = value_at(samps, value_as_int(value_get(ch, "sampler")))
let cnt = gltf_accessor_count(value_as_int(value_get(sp, "input"))) let cnt = gltf_accessor_count(render3d_st, value_as_int(value_get(sp, "input")))
n_keep += 1 n_keep += 1
n_t += cnt n_t += cnt
if path == "rotation" { n_v += cnt * 4 } else { n_v += cnt * 3 } if path == "rotation" { n_v += cnt * 4 } else { n_v += cnt * 3 }
@ -81,14 +81,14 @@ function read_one(a: Val) -> Clip {
let sp = value_at(samps, value_as_int(value_get(ch, "sampler"))) let sp = value_at(samps, value_as_int(value_get(ch, "sampler")))
c.cnode[k] = value_as_int(value_get(value_get(ch, "target"), "node")) c.cnode[k] = value_as_int(value_get(value_get(ch, "target"), "node"))
if path == "rotation" { c.cpath[k] = AC_ROT } else { c.cpath[k] = AC_POS } if path == "rotation" { c.cpath[k] = AC_ROT } else { c.cpath[k] = AC_POS }
let tb = gltf_accessor_floats(value_as_int(value_get(sp, "input"))) let tb = gltf_accessor_floats(render3d_st, value_as_int(value_get(sp, "input")))
let nk = gltf_count let nk = render3d_st.gltf_count
c.ckeys[k] = nk c.ckeys[k] = nk
c.coff[k] = at c.coff[k] = at
for i in 0 .. nk { c.times[at + i] = tb[i] } for i in 0 .. nk { c.times[at + i] = tb[i] }
if nk > 0 and c.times[at + nk - 1] > c.dur { c.dur = c.times[at + nk - 1] } if nk > 0 and c.times[at + nk - 1] > c.dur { c.dur = c.times[at + nk - 1] }
at += nk at += nk
let vb = gltf_accessor_floats(value_as_int(value_get(sp, "output"))) let vb = gltf_accessor_floats(render3d_st, value_as_int(value_get(sp, "output")))
var comps = 3 var comps = 3
if c.cpath[k] == AC_ROT { comps = 4 } if c.cpath[k] == AC_ROT { comps = 4 }
c.cvoff[k] = av c.cvoff[k] = av

View file

@ -9,9 +9,9 @@ program AnimTest {
function links_runtime() -> bool { return Input.key_pressed(0) } function links_runtime() -> bool { return Input.key_pressed(0) }
# a root and one child 1 m up, no inverse bind matrices, one skin of both # a root and one child 1 m up, no inverse bind matrices, one skin of both
function skin() -> Skin { function skin(render3d_st: mut Render3dState) -> Skin {
gltf_doc = Json.parse("{\"nodes\": [{\"name\": \"root\", \"children\": [1]}, {\"name\": \"bone\", \"translation\": [0, 1, 0]}], \"skins\": [{\"joints\": [0, 1]}]}") render3d_st.gltf_doc = Json.parse("{\"nodes\": [{\"name\": \"root\", \"children\": [1]}, {\"name\": \"bone\", \"translation\": [0, 1, 0]}], \"skins\": [{\"joints\": [0, 1]}]}")
return skin_load(0) return skin_load(render3d_st, 0)
} }
# one channel on node 1: identity at 0, a quarter turn about Z at 1 s # one channel on node 1: identity at 0, a quarter turn about Z at 1 s
@ -44,14 +44,14 @@ program AnimTest {
return q[2] return q[2]
} }
test "a clip is found by name, once" { test "a clip is found by name, once" (anim_st: mut AnimState) {
anim_clear() anim_clear(anim_st)
expect(anim_add(turn("hiker_m_walk"))) expect(anim_add(anim_st, turn("hiker_m_walk")))
expect(not anim_add(turn("hiker_m_walk"))) expect(not anim_add(anim_st, turn("hiker_m_walk")))
expect(anim_add(turn("hiker_f_walk"))) expect(anim_add(anim_st, turn("hiker_f_walk")))
expect_eq(anim_count(), 2) expect_eq(anim_count(anim_st), 2)
expect(anim_find("hiker_f_walk") != null) expect(anim_find(anim_st, "hiker_f_walk") != null)
expect(anim_find("bear_walk") == null) expect(anim_find(anim_st, "bear_walk") == null)
} }
test "a key pair is found by halving" { test "a key pair is found by halving" {
@ -61,56 +61,56 @@ program AnimTest {
expect_eq(anim_key_at(c, 0, -1.0), 0) expect_eq(anim_key_at(c, 0, -1.0), 0)
} }
test "playing samples between keys, and wraps on the duration" { test "playing samples between keys, and wraps on the duration" (anim_st: mut AnimState, render3d_st: mut Render3dState) {
let sk = skin() let sk = skin(render3d_st)
let c = turn("t") let c = turn("t")
expect(anim_play(sk, c, 0.0, null, 0.0, 0.0)) expect(anim_play(anim_st, sk, c, 0.0, null, 0.0, 0.0))
expect_near(pose_z(sk), 0.0, 0.001) expect_near(pose_z(sk), 0.0, 0.001)
anim_play(sk, c, 1.0 - 0.0001, null, 0.0, 0.0) anim_play(anim_st, sk, c, 1.0 - 0.0001, null, 0.0, 0.0)
expect_near(pose_z(sk), HALF, 0.01) expect_near(pose_z(sk), HALF, 0.01)
anim_play(sk, c, 0.5, null, 0.0, 0.0) anim_play(anim_st, sk, c, 0.5, null, 0.0, 0.0)
let mid = pose_z(sk) let mid = pose_z(sk)
anim_play(sk, c, 1.5, null, 0.0, 0.0) anim_play(anim_st, sk, c, 1.5, null, 0.0, 0.0)
expect_near(pose_z(sk), mid, 0.001) expect_near(pose_z(sk), mid, 0.001)
expect(mid > 0.3 and mid < 0.45) expect(mid > 0.3 and mid < 0.45)
} }
test "a cross-fade lands between the two clips" { test "a cross-fade lands between the two clips" (anim_st: mut AnimState, render3d_st: mut Render3dState) {
let sk = skin() let sk = skin(render3d_st)
let a = turn("a") let a = turn("a")
let b = turn("b") let b = turn("b")
anim_play(sk, a, 0.0, b, 0.999, 0.5) anim_play(anim_st, sk, a, 0.0, b, 0.999, 0.5)
let z = pose_z(sk) let z = pose_z(sk)
expect(z > 0.3 and z < 0.45) expect(z > 0.3 and z < 0.45)
anim_play(sk, a, 0.0, b, 0.999, 0.0) anim_play(anim_st, sk, a, 0.0, b, 0.999, 0.0)
expect_near(pose_z(sk), 0.0, 0.001) expect_near(pose_z(sk), 0.0, 0.001)
} }
test "a bone is found in the skin's own joints, not the file's first of that name" { test "a bone is found in the skin's own joints, not the file's first of that name" (render3d_st: mut Render3dState) {
gltf_doc = Json.parse("{\"nodes\": [{\"name\": \"bone\"}, {\"name\": \"root\", \"children\": [2]}, {\"name\": \"bone\"}], \"skins\": [{\"joints\": [1, 2]}]}") render3d_st.gltf_doc = Json.parse("{\"nodes\": [{\"name\": \"bone\"}, {\"name\": \"root\", \"children\": [2]}, {\"name\": \"bone\"}], \"skins\": [{\"joints\": [1, 2]}]}")
let sk = skin_load(0) let sk = skin_load(render3d_st, 0)
expect_eq(skin_joint(sk, "bone"), 2) expect_eq(skin_joint(sk, "bone"), 2)
expect_eq(skin_joint_quiet(sk, "wing_l"), -1) expect_eq(skin_joint_quiet(sk, "wing_l"), -1)
expect_eq(skin_joint_quiet(null, "bone"), -1) expect_eq(skin_joint_quiet(null, "bone"), -1)
} }
test "no skin or no clip plays nothing" { test "no skin or no clip plays nothing" (anim_st: mut AnimState, render3d_st: mut Render3dState) {
expect(not anim_play(null, turn("t"), 0.0, null, 0.0, 0.0)) expect(not anim_play(anim_st, null, turn("t"), 0.0, null, 0.0, 0.0))
expect(not anim_play(skin(), null, 0.0, null, 0.0, 0.0)) expect(not anim_play(anim_st, skin(render3d_st), null, 0.0, null, 0.0, 0.0))
} }
test "keyed translations are ignored until asked for" { test "keyed translations are ignored until asked for" (anim_st: mut AnimState, render3d_st: mut Render3dState) {
let sk = skin() let sk = skin(render3d_st)
let c = turn("t") let c = turn("t")
c.cpath[0] = AC_POS c.cpath[0] = AC_POS
c.vals[0] = 0.0 c.vals[0] = 0.0
c.vals[1] = 3.0 c.vals[1] = 3.0
c.vals[3] = 0.0 c.vals[3] = 0.0
c.vals[4] = 3.0 c.vals[4] = 3.0
anim_play(sk, c, 0.0, null, 0.0, 0.0) anim_play(anim_st, sk, c, 0.0, null, 0.0, 0.0)
expect_near(sk.pose_t[4], 0.0, 0.0001) expect_near(sk.pose_t[4], 0.0, 0.0001)
anim_set_positions(true) anim_set_positions(anim_st, true)
anim_play(sk, c, 0.0, null, 0.0, 0.0) anim_play(anim_st, sk, c, 0.0, null, 0.0, 0.0)
expect_near(sk.pose_t[4], 2.0, 0.0001) expect_near(sk.pose_t[4], 2.0, 0.0001)
} }
@ -122,8 +122,8 @@ program AnimTest {
b[at + 3] = (v >> 24) & 255 b[at + 3] = (v >> 24) & 255
} }
test "clips are read out of a glTF's .bin, and a late first key is measured" { test "clips are read out of a glTF's .bin, and a late first key is measured" (anim_st: mut AnimState, render3d_st: mut Render3dState) {
anim_clear() anim_clear(anim_st)
let dir = Os.temp_dir() + "/ludic-anim-test" let dir = Os.temp_dir() + "/ludic-anim-test"
Fs.mkdir(dir) Fs.mkdir(dir)
let b = buffer(40) let b = buffer(40)
@ -133,10 +133,10 @@ program AnimTest {
put_f(b, 8 + 24, HALF) put_f(b, 8 + 24, HALF)
put_f(b, 8 + 28, HALF) put_f(b, 8 + 28, HALF)
expect(Fs.write_bytes(dir + "/t.bin", b, 40)) expect(Fs.write_bytes(dir + "/t.bin", b, 40))
gltf_doc = Json.parse("{\"buffers\": [{\"uri\": \"t.bin\"}], \"bufferViews\": [{\"byteOffset\": 0}, {\"byteOffset\": 8}], \"accessors\": [{\"bufferView\": 0, \"count\": 2, \"type\": \"SCALAR\"}, {\"bufferView\": 1, \"count\": 2, \"type\": \"VEC4\"}], \"animations\": [{\"name\": \"bear_walk\", \"samplers\": [{\"input\": 0, \"output\": 1}], \"channels\": [{\"sampler\": 0, \"target\": {\"node\": 1, \"path\": \"rotation\"}}, {\"sampler\": 0, \"target\": {\"node\": 1, \"path\": \"scale\"}}]}]}") render3d_st.gltf_doc = Json.parse("{\"buffers\": [{\"uri\": \"t.bin\"}], \"bufferViews\": [{\"byteOffset\": 0}, {\"byteOffset\": 8}], \"accessors\": [{\"bufferView\": 0, \"count\": 2, \"type\": \"SCALAR\"}, {\"bufferView\": 1, \"count\": 2, \"type\": \"VEC4\"}], \"animations\": [{\"name\": \"bear_walk\", \"samplers\": [{\"input\": 0, \"output\": 1}], \"channels\": [{\"sampler\": 0, \"target\": {\"node\": 1, \"path\": \"rotation\"}}, {\"sampler\": 0, \"target\": {\"node\": 1, \"path\": \"scale\"}}]}]}")
expect_eq(anim_read_doc(dir), 1) expect_eq(anim_read_doc(anim_st, render3d_st, dir), 1)
expect_eq(anim_read_doc(dir), 0) expect_eq(anim_read_doc(anim_st, render3d_st, dir), 0)
let c = anim_find("bear_walk") let c = anim_find(anim_st, "bear_walk")
expect(c != null) expect(c != null)
expect_eq(c.n, 1) expect_eq(c.n, 1)
expect_near(c.dur, 1.0, 0.0001) expect_near(c.dur, 1.0, 0.0001)

View file

@ -1,33 +1,37 @@
# emit.ludic - the only calls into the runtime's playback. aud_emit is the ONE place a float # emit.ludic - the only calls into the runtime's playback. aud_emit is the ONE place a float
# crosses into Audio.play_at's Q16.16: handed over unconverted, 0.94 as a pitch clamped to 4x and # crosses into Audio.play_at's Q16.16: handed over unconverted, 0.94 as a pitch clamped to 4x and
# -0.85 as a pan to hard left, and every sound in a valley played fast out of one speaker # -0.85 as a pan to hard left, and every sound in a valley played fast out of one speaker
var last_gain: float = 0.0 export state AudioState {
var last_pitch: float = 0.0 last_gain: float = 0.0
var last_pan: float = 0.0 last_pitch: float = 0.0
var emitted: int = 0 last_pan: float = 0.0
emitted: int = 0
tried: int = 0
live: int = 0
}
export function aud_emit(h: int, gain: float, pitch: float, pan: float) -> void { export function aud_emit(audio_st: mut AudioState, h: int, gain: float, pitch: float, pan: float) -> void {
if h == 0 { return } if h == 0 { return }
last_gain = gain audio_st.last_gain = gain
last_pitch = pitch audio_st.last_pitch = pitch
last_pan = pan audio_st.last_pan = pan
emitted += 1 audio_st.emitted += 1
Audio.play_at(h, fixed(gain), fixed(pitch), fixed(pan)) Audio.play_at(h, fixed(gain), fixed(pitch), fixed(pan))
} }
# a sound at a place in the world, heard within `range` metres: false (and nothing played) when the # a sound at a place in the world, heard within `range` metres: false (and nothing played) when the
# listener is out of earshot # listener is out of earshot
export function aud_emit_at(h: int, x: float, z: float, range: float) -> bool { export function aud_emit_at(audio_st: mut AudioState, h: int, x: float, z: float, range: float) -> bool {
let g = aud_gain_at(x, z, range) let g = aud_gain_at(x, z, range)
if g == 0.0 { return false } if g == 0.0 { return false }
aud_emit(h, g, aud_pitch_at(g), aud_pan_at(x, z)) aud_emit(audio_st, h, g, aud_pitch_at(g), aud_pan_at(x, z))
return true return true
} }
# a click, a chime, a shutter, a warning: in the player's head, flat # a click, a chime, a shutter, a warning: in the player's head, flat
export function aud_ui(h: int) -> void { export function aud_ui(audio_st: mut AudioState, h: int) -> void {
if h == 0 { return } if h == 0 { return }
emitted += 1 audio_st.emitted += 1
Audio.play(h) Audio.play(h)
} }
@ -37,7 +41,7 @@ export function aud_master(v: float) -> void { Audio.volume(fixed(Math.clamp(v,
# what the last emit handed the runtime, and how many shots so far: Audio.* is a no-op headless, so # what the last emit handed the runtime, and how many shots so far: Audio.* is a no-op headless, so
# the numbers are the only thing a test can hear # the numbers are the only thing a test can hear
export function aud_last_gain() -> float { return last_gain } export function aud_last_gain(audio_st: AudioState) -> float { return audio_st.last_gain }
export function aud_last_pitch() -> float { return last_pitch } export function aud_last_pitch(audio_st: AudioState) -> float { return audio_st.last_pitch }
export function aud_last_pan() -> float { return last_pan } export function aud_last_pan(audio_st: AudioState) -> float { return audio_st.last_pan }
export function aud_emitted() -> int { return emitted } export function aud_emitted(audio_st: AudioState) -> int { return audio_st.emitted }

View file

@ -2,25 +2,23 @@
# bottoms out in AVAudioPlayer), not the pack-aware file_open every other asset arrives through: in a # bottoms out in AVAudioPlayer), not the pack-aware file_open every other asset arrives through: in a
# bundle every clip loads as 0 while Fs.exists, which IS pack-aware, says it is there. So try the # bundle every clip loads as 0 while Fs.exists, which IS pack-aware, says it is there. So try the
# runtime's path, and on 0 lift the bytes out of the pack into the cache directory and load that. # runtime's path, and on 0 lift the bytes out of the pack into the cache directory and load that.
var tried: int = 0
var live: int = 0
export function aud_load(path: string) -> int { export function aud_load(audio_st: mut AudioState, path: string) -> int {
tried += 1 audio_st.tried += 1
if not is_windowed() { return 0 } if not is_windowed() { return 0 }
var h = Audio.load(path) var h = Audio.load(path)
if h == 0 { if h == 0 {
let out = aud_unpack(path, base_name(path)) let out = aud_unpack(path, base_name(path))
if out != null { h = Audio.load(out) } if out != null { h = Audio.load(out) }
} }
if h != 0 { live += 1 } if h != 0 { audio_st.live += 1 }
return h return h
} }
# how many clips were asked for and how many loaded: a silent game looks exactly like a working one # how many clips were asked for and how many loaded: a silent game looks exactly like a working one
# unless something says so # unless something says so
export function aud_tried() -> int { return tried } export function aud_tried(audio_st: AudioState) -> int { return audio_st.tried }
export function aud_live() -> int { return live } export function aud_live(audio_st: AudioState) -> int { return audio_st.live }
# One packed file out to the cache, or null. A cached copy is trusted only if it is as long as the # One packed file out to the cache, or null. A cached copy is trusted only if it is as long as the
# packed one: a write interrupted halfway would otherwise play truncated for ever. # packed one: a write interrupted halfway would otherwise play truncated for ever.

View file

@ -4,35 +4,37 @@
import "ludic.audio" import "ludic.audio"
program AudioTest { program AudioTest {
numbers float numbers float
var lx: float = 0.0 state AudiotestState {
var lz: float = 0.0 lx: float = 0.0
var lyaw: float = 0.0 lz: float = 0.0
function t_x() -> float { return lx } lyaw: float = 0.0
function t_z() -> float { return lz } }
function t_yaw() -> float { return lyaw } function t_x(audiotest_st: AudiotestState) -> float { return audiotest_st.lx }
function t_z(audiotest_st: AudiotestState) -> float { return audiotest_st.lz }
function t_yaw(audiotest_st: AudiotestState) -> float { return audiotest_st.lyaw }
function dir() -> string { return Os.temp_dir() + "/ludic-audio-test" } function dir() -> string { return Os.temp_dir() + "/ludic-audio-test" }
function cache() -> string { return dir() + "/cache" } function cache() -> string { return dir() + "/cache" }
bind AudioListener { x: fn t_x, z: fn t_z, yaw: fn t_yaw } bind AudioListener { x: fn t_x, z: fn t_z, yaw: fn t_yaw }
bind AudioFiles { cache_dir: fn cache } bind AudioFiles { cache_dir: fn cache }
test "the gain falls off with distance and is gone past the range" { test "the gain falls off with distance and is gone past the range" (audiotest_st: mut AudiotestState) {
let near = aud_gain_at(0.0, -10.0, 200.0) let near = aud_gain_at(0.0, -10.0, 200.0)
let mid = aud_gain_at(0.0, -100.0, 200.0) let mid = aud_gain_at(0.0, -100.0, 200.0)
let far = aud_gain_at(0.0, -150.0, 200.0) let far = aud_gain_at(0.0, -150.0, 200.0)
expect(near > mid and mid > far and far > 0.0) expect(near > mid and mid > far and far > 0.0)
expect_eq(aud_gain_at(0.0, -400.0, 200.0), 0.0) expect_eq(aud_gain_at(0.0, -400.0, 200.0), 0.0)
expect_near(aud_gain_at(0.0, 0.0, 50.0), 1.0, 0.0001) expect_near(aud_gain_at(0.0, 0.0, 50.0), 1.0, 0.0001)
lx = 1000.0 audiotest_st.lx = 1000.0
expect_near(aud_gain_at(1000.0, 0.0, 50.0), 1.0, 0.0001) expect_near(aud_gain_at(1000.0, 0.0, 50.0), 1.0, 0.0001)
} }
test "the pan says which side, ahead and behind are centred, and it turns with the listener" { test "the pan says which side, ahead and behind are centred, and it turns with the listener" (audiotest_st: mut AudiotestState) {
expect(aud_pan_at(-60.0, 0.0) < -0.5) expect(aud_pan_at(-60.0, 0.0) < -0.5)
expect(aud_pan_at(60.0, 0.0) > 0.5) expect(aud_pan_at(60.0, 0.0) > 0.5)
expect_near(aud_pan_at(0.0, -60.0), 0.0, 0.01) expect_near(aud_pan_at(0.0, -60.0), 0.0, 0.01)
expect_near(aud_pan_at(0.0, 60.0), 0.0, 0.01) expect_near(aud_pan_at(0.0, 60.0), 0.0, 0.01)
expect_near(aud_pan_at(0.5, 0.0), 0.0, 0.0001) expect_near(aud_pan_at(0.5, 0.0), 0.0, 0.0001)
lyaw = 3.14159265 audiotest_st.lyaw = 3.14159265
expect(aud_pan_at(-60.0, 0.0) > 0.5) expect(aud_pan_at(-60.0, 0.0) > 0.5)
} }
@ -41,18 +43,18 @@ program AudioTest {
expect(aud_pitch_at(0.0) > 0.9 and aud_pitch_at(1.0) < 1.1) expect(aud_pitch_at(0.0) > 0.9 and aud_pitch_at(1.0) < 1.1)
} }
test "emit hands the runtime what it was told, and a sound out of earshot is not played" { test "emit hands the runtime what it was told, and a sound out of earshot is not played" (audio_st: mut AudioState) {
aud_emit(7, 0.5, 0.97, -0.25) aud_emit(audio_st, 7, 0.5, 0.97, -0.25)
expect_near(aud_last_gain(), 0.5, 0.0001) expect_near(aud_last_gain(audio_st), 0.5, 0.0001)
expect_near(aud_last_pan(), -0.25, 0.0001) expect_near(aud_last_pan(audio_st), -0.25, 0.0001)
expect_eq(aud_emitted(), 1) expect_eq(aud_emitted(audio_st), 1)
aud_emit(0, 1.0, 1.0, 0.0) aud_emit(audio_st, 0, 1.0, 1.0, 0.0)
expect_eq(aud_emitted(), 1) expect_eq(aud_emitted(audio_st), 1)
expect(not aud_emit_at(7, 0.0, -500.0, 100.0)) expect(not aud_emit_at(audio_st, 7, 0.0, -500.0, 100.0))
expect(aud_emit_at(7, 30.0, 0.0, 100.0)) expect(aud_emit_at(audio_st, 7, 30.0, 0.0, 100.0))
expect(aud_last_pan() > 0.5) expect(aud_last_pan(audio_st) > 0.5)
expect(aud_last_gain() > 0.4 and aud_last_gain() < 0.5) expect(aud_last_gain(audio_st) > 0.4 and aud_last_gain(audio_st) < 0.5)
expect(aud_last_pitch() < 1.0) expect(aud_last_pitch(audio_st) < 1.0)
} }
function wav(path: string, rate: int, frames: int) -> void { function wav(path: string, rate: int, frames: int) -> void {
@ -92,9 +94,9 @@ program AudioTest {
expect(aud_unpack(dir() + "/nothing.wav", "nothing.wav") == null) expect(aud_unpack(dir() + "/nothing.wav", "nothing.wav") == null)
} }
test "nothing loads headless, and the count says so" { test "nothing loads headless, and the count says so" (audio_st: mut AudioState) {
expect_eq(aud_load(dir() + "/click.wav"), 0) expect_eq(aud_load(audio_st, dir() + "/click.wav"), 0)
expect_eq(aud_tried(), 1) expect_eq(aud_tried(audio_st), 1)
expect_eq(aud_live(), 0) expect_eq(aud_live(audio_st), 0)
} }
} }

View file

@ -6,53 +6,56 @@ export property Queue<T> {
} }
# every named queue's pending count, so a frame's end can say which facts nobody read # every named queue's pending count, so a frame's end can say which facts nobody read
var qb_names: []string = null export state BaseState {
var qb_pending: []int = null qb_names: []string = null
qb_pending: []int = null
cs_list: []System = null
}
function qb_init() -> void { function qb_init(base_st: mut BaseState) -> void {
if qb_names != null { return } if base_st.qb_names != null { return }
qb_names = new []string base_st.qb_names = new []string
qb_pending = new []int base_st.qb_pending = new []int
} }
# a queue; the name is only for core_undrained # a queue; the name is only for core_undrained
export function queue_new<T>(name: string) -> Queue<T> { export function queue_new<T>(base_st: mut BaseState, name: string) -> Queue<T> {
qb_init() qb_init(base_st)
let q = new Queue<T> let q = new Queue<T>
q.items = new []T q.items = new []T
q.id = len(qb_names) q.id = len(base_st.qb_names)
push(qb_names, name) push(base_st.qb_names, name)
push(qb_pending, 0) push(base_st.qb_pending, 0)
return q return q
} }
export function q_push<T>(q: Queue<T>, v: T) -> void { export function q_push<T>(base_st: mut BaseState, q: Queue<T>, v: T) -> void {
push(q.items, v) push(q.items, v)
qb_pending[q.id] = len(q.items) base_st.qb_pending[q.id] = len(q.items)
} }
# the facts in the order they were pushed; the queue is empty afterwards # the facts in the order they were pushed; the queue is empty afterwards
export function q_drain<T>(q: Queue<T>) -> []T { export function q_drain<T>(base_st: mut BaseState, q: Queue<T>) -> []T {
let out = q.items let out = q.items
q.items = new []T q.items = new []T
qb_pending[q.id] = 0 base_st.qb_pending[q.id] = 0
return out return out
} }
export function q_len<T>(q: Queue<T>) -> int { return len(q.items) } export function q_len<T>(q: Queue<T>) -> int { return len(q.items) }
export function q_clear<T>(q: Queue<T>) -> void { export function q_clear<T>(base_st: mut BaseState, q: Queue<T>) -> void {
q.items = new []T q.items = new []T
qb_pending[q.id] = 0 base_st.qb_pending[q.id] = 0
} }
# the names of the queues still holding facts; call it at the end of a frame, where a fact # the names of the queues still holding facts; call it at the end of a frame, where a fact
# left behind is a route nobody wrote # left behind is a route nobody wrote
export function core_undrained() -> []string { export function core_undrained(base_st: mut BaseState) -> []string {
qb_init() qb_init(base_st)
let out = new []string let out = new []string
for i in 0 .. len(qb_names) { for i in 0 .. len(base_st.qb_names) {
if qb_pending[i] > 0 { push(out, qb_names[i]) } if base_st.qb_pending[i] > 0 { push(out, base_st.qb_names[i]) }
} }
return out return out
} }

View file

@ -16,14 +16,13 @@ export property System {
# fishing_tick }` from its own module; the runner starts from these, then core_add appends # fishing_tick }` from its own module; the runner starts from these, then core_add appends
export open registry Systems of System as SYS export open registry Systems of System as SYS
var cs_list: []System = null
function cs_all() -> []System { function cs_all(base_st: mut BaseState) -> []System {
if cs_list == null { if base_st.cs_list == null {
cs_list = new []System base_st.cs_list = new []System
for i in 0 .. SYS_COUNT { push(cs_list, Systems[i]) } for i in 0 .. SYS_COUNT { push(base_st.cs_list, Systems[i]) }
} }
return cs_list return base_st.cs_list
} }
export function system_new(key: string, phase: int) -> System { export function system_new(key: string, phase: int) -> System {
@ -34,8 +33,8 @@ export function system_new(key: string, phase: int) -> System {
} }
# a key is a save section's name, so two systems may not share one # a key is a save section's name, so two systems may not share one
export function core_add(s: System) -> void { export function core_add(base_st: mut BaseState, s: System) -> void {
let all = cs_all() let all = cs_all(base_st)
for i in 0 .. len(all) { for i in 0 .. len(all) {
if all[i].key == s.key { if all[i].key == s.key {
print(`ludic.base: two systems are keyed "{s.key}"`) print(`ludic.base: two systems are keyed "{s.key}"`)
@ -45,21 +44,21 @@ export function core_add(s: System) -> void {
push(all, s) push(all, s)
} }
export function core_clear() -> void { cs_list = new []System } export function core_clear(base_st: mut BaseState) -> void { base_st.cs_list = new []System }
export function core_count() -> int { return len(cs_all()) } export function core_count(base_st: mut BaseState) -> int { return len(cs_all(base_st)) }
export function core_init_all() -> void { export function core_init_all(base_st: mut BaseState) -> void {
let all = cs_all() let all = cs_all(base_st)
for i in 0 .. len(all) { if all[i].init != null { all[i].init() } } for i in 0 .. len(all) { if all[i].init != null { all[i].init() } }
} }
export function core_reset_all() -> void { export function core_reset_all(base_st: mut BaseState) -> void {
let all = cs_all() let all = cs_all(base_st)
for i in 0 .. len(all) { if all[i].reset != null { all[i].reset() } } for i in 0 .. len(all) { if all[i].reset != null { all[i].reset() } }
} }
export function core_tick_all(t: Tick) -> void { export function core_tick_all(base_st: mut BaseState, t: Tick) -> void {
let all = cs_all() let all = cs_all(base_st)
for ph in 0 .. PH_COUNT { for ph in 0 .. PH_COUNT {
for i in 0 .. len(all) { for i in 0 .. len(all) {
if all[i].phase == ph and all[i].tick != null { all[i].tick(t) } if all[i].phase == ph and all[i].tick != null { all[i].tick(t) }
@ -68,9 +67,9 @@ export function core_tick_all(t: Tick) -> void {
} }
# every system that saves, under its key with its version # every system that saves, under its key with its version
export function core_save_all() -> Val { export function core_save_all(base_st: mut BaseState) -> Val {
let root = save_tree() let root = save_tree()
let all = cs_all() let all = cs_all(base_st)
for i in 0 .. len(all) { for i in 0 .. len(all) {
if all[i].save != null { save_section(root, all[i].key, all[i].version, all[i].save()) } if all[i].save != null { save_section(root, all[i].key, all[i].version, all[i].save()) }
} }
@ -78,8 +77,8 @@ export function core_save_all() -> Val {
} }
# each system its own section; one with no section (or no load) is reset instead # each system its own section; one with no section (or no load) is reset instead
export function core_load_all(root: Val) -> void { export function core_load_all(base_st: mut BaseState, root: Val) -> void {
let all = cs_all() let all = cs_all(base_st)
for i in 0 .. len(all) { for i in 0 .. len(all) {
let n = load_section(root, all[i].key) let n = load_section(root, all[i].key)
if n.found and all[i].load != null { all[i].load(n.data, n.version) } else if all[i].reset != null { all[i].reset() } if n.found and all[i].load != null { all[i].load(n.data, n.version) } else if all[i].reset != null { all[i].reset() }

View file

@ -12,52 +12,52 @@ program QueueTest {
return c return c
} }
function fifo_case() -> void { function fifo_case(base_st: mut BaseState) -> void {
let q: Queue<int> = queue_new("ints") let q: Queue<int> = queue_new(base_st, "ints")
q_push(q, 3) q_push(base_st, q, 3)
q_push(q, 1) q_push(base_st, q, 1)
q_push(q, 2) q_push(base_st, q, 2)
expect_eq(q_len(q), 3) expect_eq(q_len(q), 3)
let xs = q_drain(q) let xs = q_drain(base_st, q)
expect_eq(len(xs), 3) expect_eq(len(xs), 3)
expect_eq(xs[0], 3) expect_eq(xs[0], 3)
expect_eq(xs[1], 1) expect_eq(xs[1], 1)
expect_eq(xs[2], 2) expect_eq(xs[2], 2)
expect_eq(q_len(q), 0) expect_eq(q_len(q), 0)
expect_eq(len(q_drain(q)), 0) expect_eq(len(q_drain(base_st, q)), 0)
} }
function record_case() -> void { function record_case(base_st: mut BaseState) -> void {
let q: Queue<Caught> = queue_new("caught") let q: Queue<Caught> = queue_new(base_st, "caught")
q_push(q, caught(2, 1.5)) q_push(base_st, q, caught(2, 1.5))
q_push(q, caught(4, 0.25)) q_push(base_st, q, caught(4, 0.25))
let cs = q_drain(q) let cs = q_drain(base_st, q)
expect_eq(cs[1].species, 4) expect_eq(cs[1].species, 4)
expect(cs[0].weight == 1.5) expect(cs[0].weight == 1.5)
} }
function clear_case() -> void { function clear_case(base_st: mut BaseState) -> void {
let q: Queue<string> = queue_new("words") let q: Queue<string> = queue_new(base_st, "words")
q_push(q, "a") q_push(base_st, q, "a")
q_clear(q) q_clear(base_st, q)
expect_eq(q_len(q), 0) expect_eq(q_len(q), 0)
} }
function undrained_case() -> void { function undrained_case(base_st: mut BaseState) -> void {
let a: Queue<int> = queue_new("left_behind") let a: Queue<int> = queue_new(base_st, "left_behind")
let b: Queue<int> = queue_new("read") let b: Queue<int> = queue_new(base_st, "read")
q_push(a, 1) q_push(base_st, a, 1)
q_push(b, 2) q_push(base_st, b, 2)
q_drain(b) q_drain(base_st, b)
let names = core_undrained() let names = core_undrained(base_st)
expect_eq(len(names), 1) expect_eq(len(names), 1)
expect(names[0] == "left_behind") expect(names[0] == "left_behind")
q_drain(a) q_drain(base_st, a)
expect_eq(len(core_undrained()), 0) expect_eq(len(core_undrained(base_st)), 0)
} }
test "a queue drains in the order it was pushed" { fifo_case() } test "a queue drains in the order it was pushed" (base_st: mut BaseState) { fifo_case(base_st) }
test "a queue holds records" { record_case() } test "a queue holds records" (base_st: mut BaseState) { record_case(base_st) }
test "clear drops what is waiting" { clear_case() } test "clear drops what is waiting" (base_st: mut BaseState) { clear_case(base_st) }
test "the undrained queues are named, and a drain clears the report" { undrained_case() } test "the undrained queues are named, and a drain clears the report" (base_st: mut BaseState) { undrained_case(base_st) }
} }

View file

@ -3,19 +3,21 @@
import "ludic.base" import "ludic.base"
program RegistryTest { program RegistryTest {
numbers float numbers float
var trail: string = "" state RegistrytestState {
function late_tick(t: Tick) -> void { trail = trail + "l" } trail: string = ""
function early_tick(t: Tick) -> void { trail = trail + "e" } }
function added_tick(t: Tick) -> void { trail = trail + "a" } function late_tick(registrytest_st: mut RegistrytestState, t: Tick) -> void { registrytest_st.trail = registrytest_st.trail + "l" }
function early_tick(registrytest_st: mut RegistrytestState, t: Tick) -> void { registrytest_st.trail = registrytest_st.trail + "e" }
function added_tick(registrytest_st: mut RegistrytestState, t: Tick) -> void { registrytest_st.trail = registrytest_st.trail + "a" }
def Systems late { phase: PH_RESOLVE, tick: fn late_tick } def Systems late { phase: PH_RESOLVE, tick: fn late_tick }
def Systems early { phase: PH_INPUT, tick: fn early_tick } def Systems early { phase: PH_INPUT, tick: fn early_tick }
test "the declared systems run, phase by phase" { test "the declared systems run, phase by phase" (base_st: mut BaseState, registrytest_st: RegistrytestState) {
expect_eq(core_count(), 2) expect_eq(core_count(base_st), 2)
expect(Systems[SYS_LATE].key == "late") expect(Systems[SYS_LATE].key == "late")
let s = system_new("added", PH_INPUT) let s = system_new("added", PH_INPUT)
s.tick = fn added_tick s.tick = fn added_tick
core_add(s) core_add(base_st, s)
core_tick_all(tick_new(0.016, 1, 0.0)) core_tick_all(base_st, tick_new(0.016, 1, 0.0))
expect(trail == "eal") expect(registrytest_st.trail == "eal")
} }
} }

View file

@ -10,57 +10,57 @@ program RouteTest {
bind FishingWorld { is_water: fn water_everywhere_but_land } bind FishingWorld { is_water: fn water_everywhere_but_land }
# the route: a landed fish goes into the pack # the route: a landed fish goes into the pack
function route_fishing_pack(t: Tick) -> void { function route_fishing_pack(base_st: mut BaseState, toy_fishing_st: ToyFishingState, toy_pack_st: mut ToyPackState, t: Tick) -> void {
let fish = q_drain(caught) let fish = q_drain(base_st, toy_fishing_st.caught)
for i in 0 .. len(fish) { pack_add(fish[i].species, 1) } for i in 0 .. len(fish) { pack_add(toy_pack_st, fish[i].species, 1) }
} }
function game_start() -> void { function game_start(base_st: mut BaseState) -> void {
core_clear() core_clear(base_st)
core_add(fishing_system()) core_add(base_st, fishing_system())
let r = system_new("route.fishing_pack", PH_RESOLVE) let r = system_new("route.fishing_pack", PH_RESOLVE)
r.tick = fn route_fishing_pack r.tick = fn route_fishing_pack
core_add(r) core_add(base_st, r)
core_add(pack_system()) core_add(base_st, pack_system())
core_reset_all() core_reset_all(base_st)
} }
function pack_total() -> int { return pack_count(0) + pack_count(1) + pack_count(2) } function pack_total(toy_pack_st: ToyPackState) -> int { return pack_count(toy_pack_st, 0) + pack_count(toy_pack_st, 1) + pack_count(toy_pack_st, 2) }
test "three casts on water land three fish in the pack, one on land lands nothing" { test "three casts on water land three fish in the pack, one on land lands nothing" (base_st: mut BaseState, toy_fishing_st: mut ToyFishingState, toy_pack_st: ToyPackState) {
game_start() game_start(base_st)
expect(fishing_cast(1.0, 2.0)) expect(fishing_cast(toy_fishing_st, 1.0, 2.0))
expect(fishing_cast(3.0, 2.0)) expect(fishing_cast(toy_fishing_st, 3.0, 2.0))
expect(fishing_cast(5.0, 2.0)) expect(fishing_cast(toy_fishing_st, 5.0, 2.0))
expect(not fishing_cast(500.0, 2.0)) expect(not fishing_cast(toy_fishing_st, 500.0, 2.0))
core_tick_all(tick_new(1.0 / 60.0, 0, 0.0)) core_tick_all(base_st, tick_new(1.0 / 60.0, 0, 0.0))
expect_eq(pack_total(), 3) expect_eq(pack_total(toy_pack_st), 3)
expect_eq(len(core_undrained()), 0) expect_eq(len(core_undrained(base_st)), 0)
} }
test "the pack saves its own section and the fishing, which saves nothing, is reset" { test "the pack saves its own section and the fishing, which saves nothing, is reset" (base_st: mut BaseState, toy_fishing_st: mut ToyFishingState, toy_pack_st: ToyPackState) {
game_start() game_start(base_st)
fishing_cast(1.0, 1.0) fishing_cast(toy_fishing_st, 1.0, 1.0)
core_tick_all(tick_new(1.0 / 60.0, 0, 0.0)) core_tick_all(base_st, tick_new(1.0 / 60.0, 0, 0.0))
let text = save_encode(core_save_all()) let text = save_encode(core_save_all(base_st))
let tree = save_decode(text) let tree = save_decode(text)
expect_eq(Value.count(tree), 1) expect_eq(Value.count(tree), 1)
expect_eq(load_section(tree, "pack").version, 1) expect_eq(load_section(tree, "pack").version, 1)
expect(not load_section(tree, "fishing").found) expect(not load_section(tree, "fishing").found)
core_reset_all() core_reset_all(base_st)
expect_eq(pack_total(), 0) expect_eq(pack_total(toy_pack_st), 0)
core_load_all(save_decode(text)) core_load_all(base_st, save_decode(text))
expect_eq(pack_total(), 1) expect_eq(pack_total(toy_pack_st), 1)
} }
test "the same seed lands the same fish" { test "the same seed lands the same fish" (base_st: mut BaseState, toy_fishing_st: mut ToyFishingState) {
game_start() game_start(base_st)
fishing_cast(1.0, 1.0) fishing_cast(toy_fishing_st, 1.0, 1.0)
core_tick_all(tick_new(1.0 / 60.0, 0, 0.0)) core_tick_all(base_st, tick_new(1.0 / 60.0, 0, 0.0))
let first = save_encode(core_save_all()) let first = save_encode(core_save_all(base_st))
game_start() game_start(base_st)
fishing_cast(1.0, 1.0) fishing_cast(toy_fishing_st, 1.0, 1.0)
core_tick_all(tick_new(1.0 / 60.0, 0, 0.0)) core_tick_all(base_st, tick_new(1.0 / 60.0, 0, 0.0))
expect(save_encode(core_save_all()) == first) expect(save_encode(core_save_all(base_st)) == first)
} }
} }

View file

@ -3,31 +3,33 @@
import "ludic.base" import "ludic.base"
program SystemTest { program SystemTest {
numbers float numbers float
var trail: string = "" state SystemtestState {
var apples: int = 0 trail: string = ""
var hours: float = 0.0 apples: int = 0
hours: float = 0.0
}
function draw_tick(t: Tick) -> void { trail = trail + "d" } function draw_tick(systemtest_st: mut SystemtestState, t: Tick) -> void { systemtest_st.trail = systemtest_st.trail + "d" }
function think_tick(t: Tick) -> void { trail = trail + "t" } function think_tick(systemtest_st: mut SystemtestState, t: Tick) -> void { systemtest_st.trail = systemtest_st.trail + "t" }
function input_tick(t: Tick) -> void { trail = trail + "i" } function input_tick(systemtest_st: mut SystemtestState, t: Tick) -> void { systemtest_st.trail = systemtest_st.trail + "i" }
function commit_tick(t: Tick) -> void { trail = trail + "c" } function commit_tick(systemtest_st: mut SystemtestState, t: Tick) -> void { systemtest_st.trail = systemtest_st.trail + "c" }
function apples_init() -> void { trail = trail + "A" } function apples_init(systemtest_st: mut SystemtestState) -> void { systemtest_st.trail = systemtest_st.trail + "A" }
function apples_reset() -> void { apples = 0 } function apples_reset(systemtest_st: mut SystemtestState) -> void { systemtest_st.apples = 0 }
function apples_tick(t: Tick) -> void { apples += 1 } function apples_tick(systemtest_st: mut SystemtestState, t: Tick) -> void { systemtest_st.apples += 1 }
function apples_save() -> Val { function apples_save(systemtest_st: SystemtestState) -> Val {
let v = Value.object() let v = Value.object()
sv_put_int(v, "n", apples) sv_put_int(v, "n", systemtest_st.apples)
return v return v
} }
function apples_load(v: Val, version: int) -> void { apples = sv_int(v, "n", 0) * version } function apples_load(systemtest_st: mut SystemtestState, v: Val, version: int) -> void { systemtest_st.apples = sv_int(v, "n", 0) * version }
function clock_reset() -> void { hours = 6.0 } function clock_reset(systemtest_st: mut SystemtestState) -> void { systemtest_st.hours = 6.0 }
function clock_tick(t: Tick) -> void { hours = hours + t.hours } function clock_tick(systemtest_st: mut SystemtestState, t: Tick) -> void { systemtest_st.hours = systemtest_st.hours + t.hours }
function clock_save() -> Val { function clock_save(systemtest_st: SystemtestState) -> Val {
let v = Value.object() let v = Value.object()
sv_put_float(v, "h", hours) sv_put_float(v, "h", systemtest_st.hours)
return v return v
} }
function clock_load(v: Val, version: int) -> void { hours = sv_float(v, "h", 6.0) } function clock_load(systemtest_st: mut SystemtestState, v: Val, version: int) -> void { systemtest_st.hours = sv_float(v, "h", 6.0) }
function sys(key: string, phase: int, tick: fn(Tick) -> void) -> System { function sys(key: string, phase: int, tick: fn(Tick) -> void) -> System {
let s = system_new(key, phase) let s = system_new(key, phase)
@ -35,71 +37,71 @@ program SystemTest {
return s return s
} }
function setup() -> void { function setup(base_st: mut BaseState, systemtest_st: mut SystemtestState) -> void {
core_clear() core_clear(base_st)
trail = "" systemtest_st.trail = ""
core_add(sys("draw", PH_PRESENT, fn draw_tick)) core_add(base_st, sys("draw", PH_PRESENT, fn draw_tick))
core_add(sys("think", PH_SIMULATE, fn think_tick)) core_add(base_st, sys("think", PH_SIMULATE, fn think_tick))
let a = sys("apples", PH_SIMULATE, fn apples_tick) let a = sys("apples", PH_SIMULATE, fn apples_tick)
a.init = fn apples_init a.init = fn apples_init
a.reset = fn apples_reset a.reset = fn apples_reset
a.save = fn apples_save a.save = fn apples_save
a.load = fn apples_load a.load = fn apples_load
a.version = 2 a.version = 2
core_add(a) core_add(base_st, a)
core_add(sys("input", PH_INPUT, fn input_tick)) core_add(base_st, sys("input", PH_INPUT, fn input_tick))
core_add(sys("commit", PH_COMMIT, fn commit_tick)) core_add(base_st, sys("commit", PH_COMMIT, fn commit_tick))
let c = sys("clock", PH_SIMULATE, fn clock_tick) let c = sys("clock", PH_SIMULATE, fn clock_tick)
c.reset = fn clock_reset c.reset = fn clock_reset
c.save = fn clock_save c.save = fn clock_save
c.load = fn clock_load c.load = fn clock_load
core_add(c) core_add(base_st, c)
} }
test "tick runs phase by phase, in the order added within a phase" { test "tick runs phase by phase, in the order added within a phase" (base_st: mut BaseState, systemtest_st: mut SystemtestState) {
setup() setup(base_st, systemtest_st)
core_tick_all(tick_new(1.0 / 60.0, 0, 0.0)) core_tick_all(base_st, tick_new(1.0 / 60.0, 0, 0.0))
expect(trail == "itcd") expect(systemtest_st.trail == "itcd")
expect_eq(apples, 1) expect_eq(systemtest_st.apples, 1)
} }
test "init and reset run in the order added and skip a null verb" { test "init and reset run in the order added and skip a null verb" (base_st: mut BaseState, systemtest_st: mut SystemtestState) {
setup() setup(base_st, systemtest_st)
core_init_all() core_init_all(base_st)
expect(trail == "A") expect(systemtest_st.trail == "A")
apples = 9 systemtest_st.apples = 9
core_reset_all() core_reset_all(base_st)
expect_eq(apples, 0) expect_eq(systemtest_st.apples, 0)
expect(hours == 6.0) expect(systemtest_st.hours == 6.0)
expect_eq(core_count(), 6) expect_eq(core_count(base_st), 6)
} }
test "save writes each system under its key with its version, and load reads it back" { test "save writes each system under its key with its version, and load reads it back" (base_st: mut BaseState, systemtest_st: mut SystemtestState) {
setup() setup(base_st, systemtest_st)
core_reset_all() core_reset_all(base_st)
apples = 4 systemtest_st.apples = 4
core_tick_all(tick_new(0.016, 1, 0.5)) core_tick_all(base_st, tick_new(0.016, 1, 0.5))
let root = core_save_all() let root = core_save_all(base_st)
expect_eq(Value.count(root), 2) expect_eq(Value.count(root), 2)
expect_eq(load_section(root, "apples").version, 2) expect_eq(load_section(root, "apples").version, 2)
expect_eq(load_section(root, "clock").version, 1) expect_eq(load_section(root, "clock").version, 1)
let text = save_encode(root) let text = save_encode(root)
core_reset_all() core_reset_all(base_st)
core_load_all(save_decode(text)) core_load_all(base_st, save_decode(text))
expect_eq(apples, 10) expect_eq(systemtest_st.apples, 10)
expect(hours == 6.5) expect(systemtest_st.hours == 6.5)
} }
test "a system with no section in the save is reset" { test "a system with no section in the save is reset" (base_st: mut BaseState, systemtest_st: mut SystemtestState) {
setup() setup(base_st, systemtest_st)
apples = 3 systemtest_st.apples = 3
hours = 11.0 systemtest_st.hours = 11.0
let root = save_tree() let root = save_tree()
let v = Value.object() let v = Value.object()
sv_put_float(v, "h", 20.25) sv_put_float(v, "h", 20.25)
save_section(root, "clock", 1, v) save_section(root, "clock", 1, v)
core_load_all(root) core_load_all(base_st, root)
expect_eq(apples, 0) expect_eq(systemtest_st.apples, 0)
expect(hours == 20.25) expect(systemtest_st.hours == 20.25)
} }
} }

View file

@ -4,26 +4,28 @@ numbers float
import "ludic.base" import "ludic.base"
export property Caught { species: int = 0, weight: float = 0.0 } export property Caught { species: int = 0, weight: float = 0.0 }
export port FishingWorld { is_water: fn(float, float) -> bool } # its port export port FishingWorld { is_water: fn(float, float) -> bool } # its port
var dice: Rng = null export state ToyFishingState {
var casts: int = 0 dice: Rng = null
export var caught: Queue<Caught> = null # its facts casts: int = 0
export function fishing_cast(x: float, z: float) -> bool { # its verb caught: Queue<Caught> = null # its facts
}
export function fishing_cast(toy_fishing_st: mut ToyFishingState, x: float, z: float) -> bool { # its verb
if not FishingWorld.is_water(x, z) { return false } if not FishingWorld.is_water(x, z) { return false }
casts += 1 toy_fishing_st.casts += 1
return true return true
} }
function fishing_reset() -> void { function fishing_reset(base_st: mut BaseState, toy_fishing_st: mut ToyFishingState) -> void {
casts = 0 toy_fishing_st.casts = 0
dice = rng_new(7) toy_fishing_st.dice = rng_new(7)
caught = queue_new("fishing.caught") toy_fishing_st.caught = queue_new(base_st, "fishing.caught")
} }
function fishing_tick(t: Tick) -> void { function fishing_tick(base_st: mut BaseState, toy_fishing_st: mut ToyFishingState, t: Tick) -> void {
while casts > 0 { while toy_fishing_st.casts > 0 {
let c = new Caught let c = new Caught
c.species = rng_between(dice, 0, 2) c.species = rng_between(toy_fishing_st.dice, 0, 2)
c.weight = 0.5 + rng_float(dice) c.weight = 0.5 + rng_float(toy_fishing_st.dice)
q_push(caught, c) q_push(base_st, toy_fishing_st.caught, c)
casts -= 1 toy_fishing_st.casts -= 1
} }
} }
export function fishing_system() -> System { export function fishing_system() -> System {

View file

@ -2,22 +2,24 @@
module toy_pack uses ludic_base module toy_pack uses ludic_base
numbers float numbers float
import "ludic.base" import "ludic.base"
var counts: []int = null export state ToyPackState {
export function pack_add(kind: int, n: int) -> void { counts[kind] = counts[kind] + n } counts: []int = null
export function pack_count(kind: int) -> int { return counts[kind] }
function pack_reset() -> void {
counts = new []int
for i in 0 .. 3 { push(counts, 0) }
} }
function pack_save() -> Val { export function pack_add(toy_pack_st: mut ToyPackState, kind: int, n: int) -> void { toy_pack_st.counts[kind] = toy_pack_st.counts[kind] + n }
export function pack_count(toy_pack_st: ToyPackState, kind: int) -> int { return toy_pack_st.counts[kind] }
function pack_reset(toy_pack_st: mut ToyPackState) -> void {
toy_pack_st.counts = new []int
for i in 0 .. 3 { push(toy_pack_st.counts, 0) }
}
function pack_save(toy_pack_st: ToyPackState) -> Val {
let v = Value.object() let v = Value.object()
sv_put_ints(v, "counts", counts) sv_put_ints(v, "counts", toy_pack_st.counts)
return v return v
} }
function pack_load(v: Val, version: int) -> void { function pack_load(toy_pack_st: mut ToyPackState, v: Val, version: int) -> void {
pack_reset() pack_reset(toy_pack_st)
let xs = sv_ints(v, "counts") let xs = sv_ints(v, "counts")
for i in 0 .. len(xs) { if i < len(counts) { counts[i] = xs[i] } } for i in 0 .. len(xs) { if i < len(toy_pack_st.counts) { toy_pack_st.counts[i] = xs[i] } }
} }
export function pack_system() -> System { export function pack_system() -> System {
let s = system_new("pack", PH_COMMIT) let s = system_new("pack", PH_COMMIT)

View file

@ -1,70 +1,70 @@
# camera.ludic - the orbit: a pivot at the chest easing after the body, the camera on its orbit # camera.ludic - the orbit: a pivot at the chest easing after the body, the camera on its orbit
# behind and a share of the distance to the right (so the body sits left of the crosshair), lifted # behind and a share of the distance to the right (so the body sits left of the crosshair), lifted
# out of the ground and aimed back at the pivot. Or the eyes, with the gait's bob on them. # out of the ground and aimed back at the pivot. Or the eyes, with the gait's bob on them.
function chr_eyes_allowed() -> bool { return not CharacterBody.riding() and not chr_sit and CharacterBody.eyes_ok() } function chr_eyes_allowed(character_st: CharacterState) -> bool { return not CharacterBody.riding() and not character_st.chr_sit and CharacterBody.eyes_ok() }
function chr_shoulder() -> float { return Math.clamp(chr_cam_dist * 0.30, 0.6, 1.9) } function chr_shoulder(character_st: CharacterState) -> float { return Math.clamp(character_st.chr_cam_dist * 0.30, 0.6, 1.9) }
export function char_camera(dt: float) -> void { export function char_camera(character_st: mut CharacterState, dt: float) -> void {
if chr_fps and chr_eyes_allowed() { if character_st.chr_fps and chr_eyes_allowed(character_st) {
chr_camera_eyes(dt) chr_camera_eyes(character_st, dt)
return return
} }
char_camera_third(dt) char_camera_third(character_st, dt)
} }
# over the shoulder, whatever the player plays in (a figure looked at from inside its head is not) # over the shoulder, whatever the player plays in (a figure looked at from inside its head is not)
export function char_camera_third(dt: float) -> void { export function char_camera_third(character_st: mut CharacterState, dt: float) -> void {
chr_eyes_now = false character_st.chr_eyes_now = false
let k = Math.min(1.0, 10.0 * dt) let k = Math.min(1.0, 10.0 * dt)
var ty = chr_y + 1.45 var ty = character_st.chr_y + 1.45
if chr_swim { ty = chr_y + 0.9 } if character_st.chr_swim { ty = character_st.chr_y + 0.9 }
chr_piv_x = Math.lerp(chr_piv_x, chr_x, k) character_st.chr_piv_x = Math.lerp(character_st.chr_piv_x, character_st.chr_x, k)
chr_piv_y = Math.lerp(chr_piv_y, ty, k) character_st.chr_piv_y = Math.lerp(character_st.chr_piv_y, ty, k)
chr_piv_z = Math.lerp(chr_piv_z, chr_z, k) character_st.chr_piv_z = Math.lerp(character_st.chr_piv_z, character_st.chr_z, k)
let cyw = Math.cos(chr_cam_yaw) let cyw = Math.cos(character_st.chr_cam_yaw)
let syw = Math.sin(chr_cam_yaw) let syw = Math.sin(character_st.chr_cam_yaw)
let cpt = Math.cos(chr_cam_pitch) let cpt = Math.cos(character_st.chr_cam_pitch)
let fx = -(syw * cpt) let fx = -(syw * cpt)
let fy = Math.sin(chr_cam_pitch) let fy = Math.sin(character_st.chr_cam_pitch)
let fz = -(cyw * cpt) let fz = -(cyw * cpt)
let ax = chr_piv_x + cyw * chr_shoulder() let ax = character_st.chr_piv_x + cyw * chr_shoulder(character_st)
let az = chr_piv_z - syw * chr_shoulder() let az = character_st.chr_piv_z - syw * chr_shoulder(character_st)
let cx = ax - fx * chr_cam_dist let cx = ax - fx * character_st.chr_cam_dist
var cyy = chr_piv_y - fy * chr_cam_dist var cyy = character_st.chr_piv_y - fy * character_st.chr_cam_dist
let cz = az - fz * chr_cam_dist let cz = az - fz * character_st.chr_cam_dist
cyy = Math.max(cyy, CharacterGround.bed(cx, cz) + 0.55) cyy = Math.max(cyy, CharacterGround.bed(cx, cz) + 0.55)
# at the top of the water the camera is held clear of it; under it, it follows the dive # at the top of the water the camera is held clear of it; under it, it follows the dive
if chr_swim and not chr_dive and cyy < chr_water + 0.35 { cyy = chr_water + 0.35 } if character_st.chr_swim and not character_st.chr_dive and cyy < character_st.chr_water + 0.35 { cyy = character_st.chr_water + 0.35 }
let mid_g = CharacterGround.bed((cx + ax) * 0.5, (cz + az) * 0.5) + 0.4 let mid_g = CharacterGround.bed((cx + ax) * 0.5, (cz + az) * 0.5) + 0.4
let mid_y = (cyy + chr_piv_y) * 0.5 let mid_y = (cyy + character_st.chr_piv_y) * 0.5
if mid_y < mid_g { cyy = cyy + (mid_g - mid_y) * 2.0 } if mid_y < mid_g { cyy = cyy + (mid_g - mid_y) * 2.0 }
chr_eye_x = cx character_st.chr_eye_x = cx
chr_eye_y = cyy character_st.chr_eye_y = cyy
chr_eye_z = cz character_st.chr_eye_z = cz
let ddx = ax - cx let ddx = ax - cx
let ddz = az - cz let ddz = az - cz
chr_eye_yaw = Math.atan2(-ddx, -ddz) character_st.chr_eye_yaw = Math.atan2(-ddx, -ddz)
chr_eye_pitch = Math.atan2(chr_piv_y - cyy, Math.sqrt(ddx * ddx + ddz * ddz)) character_st.chr_eye_pitch = Math.atan2(character_st.chr_piv_y - cyy, Math.sqrt(ddx * ddx + ddz * ddz))
} }
# the eyes: the head's height, the walk's bob, and the look the player is already holding # the eyes: the head's height, the walk's bob, and the look the player is already holding
function chr_camera_eyes(dt: float) -> void { function chr_camera_eyes(character_st: mut CharacterState, dt: float) -> void {
chr_eyes_now = true character_st.chr_eyes_now = true
var eye = 1.62 var eye = 1.62
if chr_swim { eye = 0.42 } if character_st.chr_swim { eye = 0.42 }
if chr_dive { eye = 0.30 } if character_st.chr_dive { eye = 0.30 }
let moving = Math.clamp(chr_speed / 5.0, 0.0, 1.0) let moving = Math.clamp(character_st.chr_speed / 5.0, 0.0, 1.0)
let want = 0.035 * moving * (1.0 - Math.cos(chr_phase * 2.0)) let want = 0.035 * moving * (1.0 - Math.cos(character_st.chr_phase * 2.0))
chr_fps_bob = Math.lerp(chr_fps_bob, want, Math.min(1.0, 12.0 * dt)) character_st.chr_fps_bob = Math.lerp(character_st.chr_fps_bob, want, Math.min(1.0, 12.0 * dt))
# never inside the ground on a steep step # never inside the ground on a steep step
let ey = Math.max(chr_y + eye + chr_fps_bob, CharacterGround.bed(chr_x, chr_z) + 0.30) let ey = Math.max(character_st.chr_y + eye + character_st.chr_fps_bob, CharacterGround.bed(character_st.chr_x, character_st.chr_z) + 0.30)
chr_piv_x = chr_x character_st.chr_piv_x = character_st.chr_x
chr_piv_y = ey character_st.chr_piv_y = ey
chr_piv_z = chr_z character_st.chr_piv_z = character_st.chr_z
chr_eye_x = chr_x character_st.chr_eye_x = character_st.chr_x
chr_eye_y = ey character_st.chr_eye_y = ey
chr_eye_z = chr_z character_st.chr_eye_z = character_st.chr_z
chr_eye_yaw = chr_cam_yaw character_st.chr_eye_yaw = character_st.chr_cam_yaw
chr_eye_pitch = chr_cam_pitch character_st.chr_eye_pitch = character_st.chr_cam_pitch
} }

View file

@ -10,25 +10,23 @@ export function char_stand_at(x: float, z: float, from_y: float) -> float {
# A step up and a slope up are different refusals. Within CHAR_STEP of the feet is a step, taken # A step up and a slope up are different refusals. Within CHAR_STEP of the feet is a step, taken
# whatever the gradient into it (a 45 cm deck arrives in one frame); above that the boots decide, # whatever the gradient into it (a 45 cm deck arrives in one frame); above that the boots decide,
# measured over CHAR_SLOPE_PROBE, so the frame rate never changes the answer. # measured over CHAR_SLOPE_PROBE, so the frame rate never changes the answer.
export function char_slope_blocks(nh: float, fx: float, fz: float) -> bool { export function char_slope_blocks(character_st: CharacterState, nh: float, fx: float, fz: float) -> bool {
if nh - chr_y > CHAR_STEP { return true } if nh - character_st.chr_y > CHAR_STEP { return true }
let sx = chr_x + fx * CHAR_SLOPE_PROBE let sx = character_st.chr_x + fx * CHAR_SLOPE_PROBE
let sz = chr_z + fz * CHAR_SLOPE_PROBE let sz = character_st.chr_z + fz * CHAR_SLOPE_PROBE
return CharacterGround.height(sx, sz) - chr_y > CHAR_SLOPE_PROBE * CharacterGround.slope_limit() return CharacterGround.height(sx, sz) - character_st.chr_y > CHAR_SLOPE_PROBE * CharacterGround.slope_limit()
} }
# a body of radius r at (x, z) from y0 to y1, out of every collider: the resolved point is chr_out_* # a body of radius r at (x, z) from y0 to y1, out of every collider: the resolved point is chr_out_*
var chr_out_x: float = 0.0 function chr_push(character_st: mut CharacterState, x: float, z: float, r: float, y0: float, y1: float) -> bool {
var chr_out_z: float = 0.0 character_st.chr_ask_x = x
function chr_push(x: float, z: float, r: float, y0: float, y1: float) -> bool { character_st.chr_ask_z = z
chr_ask_x = x
chr_ask_z = z
let hit = CharacterGround.push(x, z, r, y0, y1) let hit = CharacterGround.push(x, z, r, y0, y1)
chr_out_x = x character_st.chr_out_x = x
chr_out_z = z character_st.chr_out_z = z
if hit { if hit {
chr_out_x = CharacterGround.pushed_x() character_st.chr_out_x = CharacterGround.pushed_x()
chr_out_z = CharacterGround.pushed_z() character_st.chr_out_z = CharacterGround.pushed_z()
} }
return hit return hit
} }

View file

@ -39,14 +39,70 @@ export port CharacterInput {
look_always: fn() -> bool = fn chr__no # the look is held: never settle behind look_always: fn() -> bool = fn chr__no # the look is held: never settle behind
} }
var chr_ask_x: float = 0.0 export state CharacterState {
var chr_ask_z: float = 0.0 chr_out_x: float = 0.0
chr_out_z: float = 0.0
chr_ask_x: float = 0.0
chr_ask_z: float = 0.0
chr_x: float = 0.0
chr_y: float = 0.0
chr_z: float = 0.0
chr_yaw: float = 0.0 # facing, radians; 0 faces -z
chr_speed: float = 0.0 # m/s along the facing
chr_phase: float = 0.0 # the gait: one cycle is two steps, advanced by ground covered
chr_move: float = 0.0 # 0 standing .. 1 moving
chr_run: float = 0.0 # 0 walking .. 1 running
chr_time: float = 0.0
chr_water: float = -10000.0 # the water line under the body, every update
chr_walk_speed: float = 3.2
chr_run_speed: float = 6.8
chr_vy: float = 0.0
chr_air: bool = false
chr_air_vx: float = 0.0
chr_air_vz: float = 0.0
chr_land: float = 0.0 # seconds of the landing's crouch left
chr_steep: float = 0.0 # seconds of "that was too steep" left to say
chr_can_run: bool = true
chr_step: float = 0.0 # metres covered this frame
chr_sit: bool = false
chr_sit_mode: int = 0
chr_sit_t: float = 0.0 # 0 standing .. 1 sat, eased over a quarter second
chr_sit_x: float = 0.0 # the seat's point, walked to over the blend
chr_sit_z: float = 0.0
chr_sit_yaw: float = 0.0
chr_swim: bool = false
chr_dive: bool = false
chr_depth: float = 0.0 # metres below the surface
chr_breath: float = 0.0
chr_float: float = 0.0 # 0 swimming .. 1 floating
chr_swim_phase: float = 0.0
chr_edge_said: float = 0.0
chr_holds: int = CHAR_HOLD_SCREEN
chr_cam_yaw: float = 0.0
chr_cam_pitch: float = -0.157
chr_cam_dist: float = 4.2
chr_lock: bool = false # the orbit never settles behind on its own
chr_fps: bool = false
chr_fps_bob: float = 0.0
chr_piv_x: float = 0.0
chr_piv_y: float = 0.0
chr_piv_z: float = 0.0
chr_eye_x: float = 0.0 # where the camera is and where it looks, once computed
chr_eye_y: float = 0.0
chr_eye_z: float = 0.0
chr_eye_yaw: float = 0.0
chr_eye_pitch: float = 0.0
chr_eyes_now: bool = false # the last camera was the eyes: the figure is hidden
chr_safe_x: float = 0.0
chr_safe_z: float = 0.0
chr_fact_q: Queue<CharacterFact> = null
}
function chr__top_plain(x: float, z: float, r: float, from_y: float, step: float, base: float) -> float { return base } function chr__top_plain(x: float, z: float, r: float, from_y: float, step: float, base: float) -> float { return base }
function chr__bed_plain(x: float, z: float) -> float { return CharacterGround.height(x, z) } function chr__bed_plain(x: float, z: float) -> float { return CharacterGround.height(x, z) }
function chr__dry(x: float, z: float) -> float { return -10000.0 } function chr__dry(x: float, z: float) -> float { return -10000.0 }
function chr__no_push(x: float, z: float, r: float, y0: float, y1: float) -> bool { return false } function chr__no_push(x: float, z: float, r: float, y0: float, y1: float) -> bool { return false }
function chr__asked_x() -> float { return chr_ask_x } function chr__asked_x(character_st: CharacterState) -> float { return character_st.chr_ask_x }
function chr__asked_z() -> float { return chr_ask_z } function chr__asked_z(character_st: CharacterState) -> float { return character_st.chr_ask_z }
function chr__slope_plain(x: float, z: float) -> float { function chr__slope_plain(x: float, z: float) -> float {
let e = 2.0 let e = 2.0
let dx = CharacterGround.height(x + e, z) - CharacterGround.height(x - e, z) let dx = CharacterGround.height(x + e, z) - CharacterGround.height(x - e, z)

View file

@ -1,48 +1,48 @@
# queries.ludic - what the rest of a game may ASK about the body: the only way out of its state # queries.ludic - what the rest of a game may ASK about the body: the only way out of its state
export function body_x() -> float { return chr_x } export function body_x(character_st: CharacterState) -> float { return character_st.chr_x }
export function body_y() -> float { return chr_y } export function body_y(character_st: CharacterState) -> float { return character_st.chr_y }
export function body_z() -> float { return chr_z } export function body_z(character_st: CharacterState) -> float { return character_st.chr_z }
export function body_yaw() -> float { return chr_yaw } export function body_yaw(character_st: CharacterState) -> float { return character_st.chr_yaw }
export function body_speed() -> float { return chr_speed } export function body_speed(character_st: CharacterState) -> float { return character_st.chr_speed }
export function body_moving() -> float { return chr_move } export function body_moving(character_st: CharacterState) -> float { return character_st.chr_move }
export function body_run() -> float { return chr_run } export function body_run(character_st: CharacterState) -> float { return character_st.chr_run }
export function body_phase() -> float { return chr_phase } export function body_phase(character_st: CharacterState) -> float { return character_st.chr_phase }
export function body_time() -> float { return chr_time } export function body_time(character_st: CharacterState) -> float { return character_st.chr_time }
export function body_water() -> float { return chr_water } export function body_water(character_st: CharacterState) -> float { return character_st.chr_water }
export function body_airborne() -> bool { return chr_air } export function body_airborne(character_st: CharacterState) -> bool { return character_st.chr_air }
export function body_vy() -> float { return chr_vy } export function body_vy(character_st: CharacterState) -> float { return character_st.chr_vy }
export function body_landing() -> float { return chr_land } export function body_landing(character_st: CharacterState) -> float { return character_st.chr_land }
export function body_steep() -> float { return chr_steep } export function body_steep(character_st: CharacterState) -> float { return character_st.chr_steep }
export function body_step() -> float { return chr_step } export function body_step(character_st: CharacterState) -> float { return character_st.chr_step }
export function body_sitting() -> bool { return chr_sit } export function body_sitting(character_st: CharacterState) -> bool { return character_st.chr_sit }
export function body_sit_mode() -> int { return chr_sit_mode } export function body_sit_mode(character_st: CharacterState) -> int { return character_st.chr_sit_mode }
export function body_sit_t() -> float { return chr_sit_t } export function body_sit_t(character_st: CharacterState) -> float { return character_st.chr_sit_t }
export function body_swimming() -> bool { return chr_swim } export function body_swimming(character_st: CharacterState) -> bool { return character_st.chr_swim }
export function body_diving() -> bool { return chr_dive } export function body_diving(character_st: CharacterState) -> bool { return character_st.chr_dive }
export function body_depth() -> float { return chr_depth } export function body_depth(character_st: CharacterState) -> float { return character_st.chr_depth }
export function body_breath() -> float { return chr_breath } export function body_breath(character_st: CharacterState) -> float { return character_st.chr_breath }
export function body_floating() -> float { return chr_float } export function body_floating(character_st: CharacterState) -> float { return character_st.chr_float }
export function body_swim_phase() -> float { return chr_swim_phase } export function body_swim_phase(character_st: CharacterState) -> float { return character_st.chr_swim_phase }
export function body_can_run() -> bool { return chr_can_run } export function body_can_run(character_st: CharacterState) -> bool { return character_st.chr_can_run }
export function body_walk_speed() -> float { return chr_walk_speed } export function body_walk_speed(character_st: CharacterState) -> float { return character_st.chr_walk_speed }
export function body_run_speed() -> float { return chr_run_speed } export function body_run_speed(character_st: CharacterState) -> float { return character_st.chr_run_speed }
export function body_held() -> bool { return chr_holds != 0 } export function body_held(character_st: CharacterState) -> bool { return character_st.chr_holds != 0 }
export function body_fps() -> bool { return chr_fps } export function body_fps(character_st: CharacterState) -> bool { return character_st.chr_fps }
export function body_cam_yaw() -> float { return chr_cam_yaw } export function body_cam_yaw(character_st: CharacterState) -> float { return character_st.chr_cam_yaw }
export function body_cam_pitch() -> float { return chr_cam_pitch } export function body_cam_pitch(character_st: CharacterState) -> float { return character_st.chr_cam_pitch }
export function body_cam_dist() -> float { return chr_cam_dist } export function body_cam_dist(character_st: CharacterState) -> float { return character_st.chr_cam_dist }
# the camera the last char_camera worked out, for the game to hand its renderer # the camera the last char_camera worked out, for the game to hand its renderer
export function body_eye_x() -> float { return chr_eye_x } export function body_eye_x(character_st: CharacterState) -> float { return character_st.chr_eye_x }
export function body_eye_y() -> float { return chr_eye_y } export function body_eye_y(character_st: CharacterState) -> float { return character_st.chr_eye_y }
export function body_eye_z() -> float { return chr_eye_z } export function body_eye_z(character_st: CharacterState) -> float { return character_st.chr_eye_z }
export function body_eye_yaw() -> float { return chr_eye_yaw } export function body_eye_yaw(character_st: CharacterState) -> float { return character_st.chr_eye_yaw }
export function body_eye_pitch() -> float { return chr_eye_pitch } export function body_eye_pitch(character_st: CharacterState) -> float { return character_st.chr_eye_pitch }
export function body_eyes_now() -> bool { return chr_eyes_now } export function body_eyes_now(character_st: CharacterState) -> bool { return character_st.chr_eyes_now }
# where char_safe_spot found room, after it answered true # where char_safe_spot found room, after it answered true
export function body_safe_x() -> float { return chr_safe_x } export function body_safe_x(character_st: CharacterState) -> float { return character_st.chr_safe_x }
export function body_safe_z() -> float { return chr_safe_z } export function body_safe_z(character_st: CharacterState) -> float { return character_st.chr_safe_z }
# the air a full breath holds, with the lungs the game gives # the air a full breath holds, with the lungs the game gives
export function body_breath_max() -> float { return CHAR_BREATH_MAX * CharacterBody.breath() } export function body_breath_max() -> float { return CHAR_BREATH_MAX * CharacterBody.breath() }

View file

@ -2,11 +2,11 @@
# every landing asks. The wanted spot if it will do, else the nearest ring out that is clear of every # every landing asks. The wanted spot if it will do, else the nearest ring out that is clear of every
# collider, no deeper than max_depth, within CHAR_DROP of the height being left, and on ground # collider, no deeper than max_depth, within CHAR_DROP of the height being left, and on ground
# gentle enough to walk off - a spot you can stand on but cannot leave is not a safe spot. # gentle enough to walk off - a spot you can stand on but cannot leave is not a safe spot.
function chr_safe_ok(x: float, z: float, from_y: float, max_depth: float) -> bool { function chr_safe_ok(character_st: mut CharacterState, x: float, z: float, from_y: float, max_depth: float) -> bool {
let g = CharacterGround.height(x, z) let g = CharacterGround.height(x, z)
if Math.abs(g - from_y) > CHAR_DROP { return false } if Math.abs(g - from_y) > CHAR_DROP { return false }
if CharacterGround.water(x, z) - g > max_depth { return false } if CharacterGround.water(x, z) - g > max_depth { return false }
if chr_push(x, z, 0.4, g, g + CHAR_BODY) { return false } if chr_push(character_st, x, z, 0.4, g, g + CHAR_BODY) { return false }
var lo = g var lo = g
var hi = g var hi = g
for k in 0 .. 4 { for k in 0 .. 4 {
@ -18,19 +18,19 @@ function chr_safe_ok(x: float, z: float, from_y: float, max_depth: float) -> boo
return not (hi - lo > CHAR_SLOPE_PROBE * 2.0) return not (hi - lo > CHAR_SLOPE_PROBE * 2.0)
} }
export function char_safe_spot(x: float, z: float, from_y: float, max_depth: float) -> bool { export function char_safe_spot(character_st: mut CharacterState, x: float, z: float, from_y: float, max_depth: float) -> bool {
chr_safe_x = x character_st.chr_safe_x = x
chr_safe_z = z character_st.chr_safe_z = z
if chr_safe_ok(x, z, from_y, max_depth) { return true } if chr_safe_ok(character_st, x, z, from_y, max_depth) { return true }
for r in 0 .. 9 { for r in 0 .. 9 {
let rad = 0.8 + float(r) * 1.1 let rad = 0.8 + float(r) * 1.1
for k in 0 .. 16 { for k in 0 .. 16 {
let a = float(k) * (chr_pi / 8.0) let a = float(k) * (chr_pi / 8.0)
let px = x + Math.sin(a) * rad let px = x + Math.sin(a) * rad
let pz = z + Math.cos(a) * rad let pz = z + Math.cos(a) * rad
if chr_safe_ok(px, pz, from_y, max_depth) { if chr_safe_ok(character_st, px, pz, from_y, max_depth) {
chr_safe_x = px character_st.chr_safe_x = px
chr_safe_z = pz character_st.chr_safe_z = pz
return true return true
} }
} }

View file

@ -1,54 +1,54 @@
# sit.ludic - sitting: on a seat's point, or on the ground where the body stands. The pose either # sit.ludic - sitting: on a seat's point, or on the ground where the body stands. The pose either
# side is a still one, so the sit's own weight (body_sit_t) eases 0 -> 1 over a quarter second and # side is a still one, so the sit's own weight (body_sit_t) eases 0 -> 1 over a quarter second and
# the body walks the last step to the seat over the same time rather than jumping there. # the body walks the last step to the seat over the same time rather than jumping there.
export function char_sit(x: float, z: float, yaw: float, mode: int) -> void { export function char_sit(character_st: mut CharacterState, x: float, z: float, yaw: float, mode: int) -> void {
chr_sit = true character_st.chr_sit = true
chr_sit_mode = mode character_st.chr_sit_mode = mode
chr_speed = 0.0 character_st.chr_speed = 0.0
chr_move = 0.0 character_st.chr_move = 0.0
chr_run = 0.0 character_st.chr_run = 0.0
chr_sit_x = x character_st.chr_sit_x = x
chr_sit_z = z character_st.chr_sit_z = z
chr_sit_yaw = yaw character_st.chr_sit_yaw = yaw
chr_water = CharacterGround.water(chr_x, chr_z) character_st.chr_water = CharacterGround.water(character_st.chr_x, character_st.chr_z)
} }
function chr_sit_tick(dt: float) -> void { function chr_sit_tick(character_st: mut CharacterState, dt: float) -> void {
var want = 0.0 var want = 0.0
if chr_sit { want = 1.0 } if character_st.chr_sit { want = 1.0 }
chr_sit_t = char_toward(chr_sit_t, want, 4.0 * dt) character_st.chr_sit_t = char_toward(character_st.chr_sit_t, want, 4.0 * dt)
if not chr_sit { return } if not character_st.chr_sit { return }
let k = Math.min(1.0, 8.0 * dt) let k = Math.min(1.0, 8.0 * dt)
chr_x = Math.lerp(chr_x, chr_sit_x, k) character_st.chr_x = Math.lerp(character_st.chr_x, character_st.chr_sit_x, k)
chr_z = Math.lerp(chr_z, chr_sit_z, k) character_st.chr_z = Math.lerp(character_st.chr_z, character_st.chr_sit_z, k)
chr_yaw = chr_yaw + char_wrap(chr_sit_yaw - chr_yaw) * k character_st.chr_yaw = character_st.chr_yaw + char_wrap(character_st.chr_sit_yaw - character_st.chr_yaw) * k
chr_y = CharacterGround.height(chr_x, chr_z) character_st.chr_y = CharacterGround.height(character_st.chr_x, character_st.chr_z)
chr_water = CharacterGround.water(chr_x, chr_z) character_st.chr_water = CharacterGround.water(character_st.chr_x, character_st.chr_z)
} }
# Sitting down where the body stands: not astride something, not in the air, not on a slope, # Sitting down where the body stands: not astride something, not in the air, not on a slope,
# not in the water. Asked again while sitting, it stands up. The answer is a CHAR_* for the game # not in the water. Asked again while sitting, it stands up. The answer is a CHAR_* for the game
# to word. # to word.
export function char_sit_here() -> int { export function char_sit_here(character_st: mut CharacterState) -> int {
if chr_sit { if character_st.chr_sit {
char_stand() char_stand(character_st)
return CHAR_STOOD return CHAR_STOOD
} }
if CharacterBody.riding() { return CHAR_NOT_RIDING } if CharacterBody.riding() { return CHAR_NOT_RIDING }
if chr_air { return CHAR_NOT_AIRBORNE } if character_st.chr_air { return CHAR_NOT_AIRBORNE }
if CharacterGround.slope(chr_x, chr_z) > 0.35 { return CHAR_NOT_STEEP } if CharacterGround.slope(character_st.chr_x, character_st.chr_z) > 0.35 { return CHAR_NOT_STEEP }
if chr_y < chr_water + 0.9 { return CHAR_NOT_WET } if character_st.chr_y < character_st.chr_water + 0.9 { return CHAR_NOT_WET }
char_sit(chr_x, chr_z, chr_yaw, CHAR_SIT_GROUND) char_sit(character_st, character_st.chr_x, character_st.chr_z, character_st.chr_yaw, CHAR_SIT_GROUND)
return CHAR_SAT return CHAR_SAT
} }
# standing up out of a seat put down against a boulder or a bank must not leave the body inside it # standing up out of a seat put down against a boulder or a bank must not leave the body inside it
export function char_stand() -> void { export function char_stand(character_st: mut CharacterState) -> void {
chr_sit = false character_st.chr_sit = false
if char_safe_spot(chr_x, chr_z, chr_y, 0.0) { if char_safe_spot(character_st, character_st.chr_x, character_st.chr_z, character_st.chr_y, 0.0) {
chr_x = chr_safe_x character_st.chr_x = character_st.chr_safe_x
chr_z = chr_safe_z character_st.chr_z = character_st.chr_safe_z
chr_y = CharacterGround.height(chr_x, chr_z) character_st.chr_y = CharacterGround.height(character_st.chr_x, character_st.chr_z)
} }
} }

View file

@ -1,77 +1,25 @@
# state.ludic - the body, its camera, and the queue of what happened to it # state.ludic - the body, its camera, and the queue of what happened to it
const chr_pi: float = 3.14159265 const chr_pi: float = 3.14159265
var chr_x: float = 0.0
var chr_y: float = 0.0
var chr_z: float = 0.0
var chr_yaw: float = 0.0 # facing, radians; 0 faces -z
var chr_speed: float = 0.0 # m/s along the facing
var chr_phase: float = 0.0 # the gait: one cycle is two steps, advanced by ground covered
var chr_move: float = 0.0 # 0 standing .. 1 moving
var chr_run: float = 0.0 # 0 walking .. 1 running
var chr_time: float = 0.0
var chr_water: float = -10000.0 # the water line under the body, every update
var chr_walk_speed: float = 3.2
var chr_run_speed: float = 6.8
var chr_vy: float = 0.0
var chr_air: bool = false
var chr_air_vx: float = 0.0
var chr_air_vz: float = 0.0
var chr_land: float = 0.0 # seconds of the landing's crouch left
var chr_steep: float = 0.0 # seconds of "that was too steep" left to say
var chr_can_run: bool = true
var chr_step: float = 0.0 # metres covered this frame
var chr_sit: bool = false
var chr_sit_mode: int = 0
var chr_sit_t: float = 0.0 # 0 standing .. 1 sat, eased over a quarter second
var chr_sit_x: float = 0.0 # the seat's point, walked to over the blend
var chr_sit_z: float = 0.0
var chr_sit_yaw: float = 0.0
var chr_swim: bool = false
var chr_dive: bool = false
var chr_depth: float = 0.0 # metres below the surface
var chr_breath: float = 0.0
var chr_float: float = 0.0 # 0 swimming .. 1 floating
var chr_swim_phase: float = 0.0
var chr_edge_said: float = 0.0
var chr_holds: int = CHAR_HOLD_SCREEN
# the camera: an orbit round a pivot at the chest, or the eyes # the camera: an orbit round a pivot at the chest, or the eyes
var chr_cam_yaw: float = 0.0
var chr_cam_pitch: float = -0.157
var chr_cam_dist: float = 4.2
var chr_lock: bool = false # the orbit never settles behind on its own
var chr_fps: bool = false
var chr_fps_bob: float = 0.0
var chr_piv_x: float = 0.0
var chr_piv_y: float = 0.0
var chr_piv_z: float = 0.0
var chr_eye_x: float = 0.0 # where the camera is and where it looks, once computed
var chr_eye_y: float = 0.0
var chr_eye_z: float = 0.0
var chr_eye_yaw: float = 0.0
var chr_eye_pitch: float = 0.0
var chr_eyes_now: bool = false # the last camera was the eyes: the figure is hidden
var chr_safe_x: float = 0.0
var chr_safe_z: float = 0.0
var chr_fact_q: Queue<CharacterFact> = null
export function char_facts() -> Queue<CharacterFact> { export function char_facts(base_st: mut BaseState, character_st: mut CharacterState) -> Queue<CharacterFact> {
if chr_fact_q == null { chr_fact_q = queue_new("character.facts") } if character_st.chr_fact_q == null { character_st.chr_fact_q = queue_new(base_st, "character.facts") }
return chr_fact_q return character_st.chr_fact_q
} }
function chr_say(what: int, v: float) -> void { function chr_say(base_st: mut BaseState, character_st: mut CharacterState, what: int, v: float) -> void {
let f = new CharacterFact let f = new CharacterFact
f.what = what f.what = what
f.x = chr_x f.x = character_st.chr_x
f.y = chr_y f.y = character_st.chr_y
f.z = chr_z f.z = character_st.chr_z
f.v = v f.v = v
q_push(char_facts(), f) q_push(base_st, char_facts(base_st, character_st), f)
} }

View file

@ -1,61 +1,61 @@
# steer.ludic - turning to the stick and easing to the speed it asks; then the gait and the orbit # steer.ludic - turning to the stick and easing to the speed it asks; then the gait and the orbit
function chr_turn_to(dx: float, dz: float, rate: float) -> float { function chr_turn_to(character_st: mut CharacterState, dx: float, dz: float, rate: float) -> float {
let diff = char_wrap(Math.atan2(-dx, -dz) - chr_yaw) let diff = char_wrap(Math.atan2(-dx, -dz) - character_st.chr_yaw)
chr_yaw = chr_yaw + Math.clamp(diff, -rate, rate) character_st.chr_yaw = character_st.chr_yaw + Math.clamp(diff, -rate, rate)
return diff return diff
} }
function chr_steer(dx: float, dz: float, mag: float, run: bool, dt: float) -> void { function chr_steer(character_st: mut CharacterState, dx: float, dz: float, mag: float, run: bool, dt: float) -> void {
var target = 0.0 var target = 0.0
if mag > 0.05 and not chr_air { if mag > 0.05 and not character_st.chr_air {
let diff = chr_turn_to(dx, dz, Math.lerp(10.0, 5.0, chr_run) * dt) let diff = chr_turn_to(character_st, dx, dz, Math.lerp(10.0, 5.0, character_st.chr_run) * dt)
let lined = Math.clamp((Math.cos(diff) + 0.3) / 1.3, 0.0, 1.0) let lined = Math.clamp((Math.cos(diff) + 0.3) / 1.3, 0.0, 1.0)
var top = chr_walk_speed * CharacterBody.pace() var top = character_st.chr_walk_speed * CharacterBody.pace()
if run { top = chr_run_speed * CharacterBody.pace() } if run { top = character_st.chr_run_speed * CharacterBody.pace() }
if CharacterBody.sneaking() { top = 1.3 } if CharacterBody.sneaking() { top = 1.3 }
target = mag * top * lined target = mag * top * lined
} else if CharacterBody.holding() and not chr_air { } else if CharacterBody.holding() and not character_st.chr_air {
# a hold turns the body to the thing being worked, so the work is seen being done # a hold turns the body to the thing being worked, so the work is seen being done
chr_turn_to(CharacterBody.hold_x() - chr_x, CharacterBody.hold_z() - chr_z, 6.0 * dt) chr_turn_to(character_st, CharacterBody.hold_x() - character_st.chr_x, CharacterBody.hold_z() - character_st.chr_z, 6.0 * dt)
} }
# in the water the top speed and the inertia are the water's: a swim is half a walk # in the water the top speed and the inertia are the water's: a swim is half a walk
if chr_swim { if character_st.chr_swim {
target = mag * 1.5 target = mag * 1.5
if run { target = mag * 2.4 } if run { target = mag * 2.4 }
if mag > 0.05 { chr_turn_to(dx, dz, 4.0 * dt) } if mag > 0.05 { chr_turn_to(character_st, dx, dz, 4.0 * dt) }
} }
if not chr_air { if not character_st.chr_air {
var acc = 12.0 var acc = 12.0
if target > chr_speed { acc = 9.0 } if target > character_st.chr_speed { acc = 9.0 }
if chr_swim { acc = 4.5 } if character_st.chr_swim { acc = 4.5 }
chr_speed = char_toward(chr_speed, target, acc * dt) character_st.chr_speed = char_toward(character_st.chr_speed, target, acc * dt)
} }
} }
# The gait's phase is distance over the stride, so the feet never slide; a boot comes down as the # The gait's phase is distance over the stride, so the feet never slide; a boot comes down as the
# phase crosses 0 and again as it crosses pi, and that is when a footfall is said. # phase crosses 0 and again as it crosses pi, and that is when a footfall is said.
function chr_gait(dist: float, dyaw: float, dpitch: float, dt: float) -> void { function chr_gait(base_st: mut BaseState, character_st: mut CharacterState, dist: float, dyaw: float, dpitch: float, dt: float) -> void {
if chr_land > 0.0 { chr_land = chr_land - dt } if character_st.chr_land > 0.0 { character_st.chr_land = character_st.chr_land - dt }
if chr_steep > 0.0 { chr_steep = chr_steep - dt } if character_st.chr_steep > 0.0 { character_st.chr_steep = character_st.chr_steep - dt }
if chr_swim { chr_swim_phase = (chr_swim_phase + dt * (1.6 + chr_speed * 0.9)) % (2.0 * chr_pi) } if character_st.chr_swim { character_st.chr_swim_phase = (character_st.chr_swim_phase + dt * (1.6 + character_st.chr_speed * 0.9)) % (2.0 * chr_pi) }
let stride = Math.lerp(1.5, 2.7, chr_run) let stride = Math.lerp(1.5, 2.7, character_st.chr_run)
let phase0 = chr_phase let phase0 = character_st.chr_phase
chr_phase = (chr_phase + dist / stride * (2.0 * chr_pi)) % (2.0 * chr_pi) character_st.chr_phase = (character_st.chr_phase + dist / stride * (2.0 * chr_pi)) % (2.0 * chr_pi)
if not chr_air and not chr_swim and not chr_sit and chr_speed > 0.4 { if not character_st.chr_air and not character_st.chr_swim and not character_st.chr_sit and character_st.chr_speed > 0.4 {
if chr_cross(phase0, chr_phase, 0.0) or chr_cross(phase0, chr_phase, chr_pi) { chr_say(CHAR_FOOTFALL, chr_speed) } if chr_cross(phase0, character_st.chr_phase, 0.0) or chr_cross(phase0, character_st.chr_phase, chr_pi) { chr_say(base_st, character_st, CHAR_FOOTFALL, character_st.chr_speed) }
} }
var mv = 0.0 var mv = 0.0
if chr_speed > 0.12 { mv = 1.0 } if character_st.chr_speed > 0.12 { mv = 1.0 }
chr_move = char_toward(chr_move, mv, 5.0 * dt) character_st.chr_move = char_toward(character_st.chr_move, mv, 5.0 * dt)
chr_run = char_toward(chr_run, Math.clamp((chr_speed - 3.6) / 2.8, 0.0, 1.0), 3.0 * dt) character_st.chr_run = char_toward(character_st.chr_run, Math.clamp((character_st.chr_speed - 3.6) / 2.8, 0.0, 1.0), 3.0 * dt)
chr_look(dyaw, dpitch) chr_look(character_st, dyaw, dpitch)
# the orbit settles slowly behind a walking body unless the player is holding the look # the orbit settles slowly behind a walking body unless the player is holding the look
if dyaw == 0.0 and chr_speed > 0.4 and not chr_lock and not CharacterInput.look_always() { if dyaw == 0.0 and character_st.chr_speed > 0.4 and not character_st.chr_lock and not CharacterInput.look_always() {
chr_cam_yaw = chr_cam_yaw + char_wrap(chr_yaw - chr_cam_yaw) * Math.min(1.0, 1.1 * dt) character_st.chr_cam_yaw = character_st.chr_cam_yaw + char_wrap(character_st.chr_yaw - character_st.chr_cam_yaw) * Math.min(1.0, 1.1 * dt)
} }
} }
function chr_look(dyaw: float, dpitch: float) -> void { function chr_look(character_st: mut CharacterState, dyaw: float, dpitch: float) -> void {
chr_cam_yaw = chr_cam_yaw + dyaw character_st.chr_cam_yaw = character_st.chr_cam_yaw + dyaw
chr_cam_pitch = Math.clamp(chr_cam_pitch + dpitch, -0.96, 0.61) character_st.chr_cam_pitch = Math.clamp(character_st.chr_cam_pitch + dpitch, -0.96, 0.61)
} }

View file

@ -1,22 +1,22 @@
# swim.ludic - afloat: buoyancy, the descent, the breath, and the shore stopping the body # swim.ludic - afloat: buoyancy, the descent, the breath, and the shore stopping the body
function chr_swim_tick(dx: float, dz: float, jump: bool, dt: float) -> void { function chr_swim_tick(base_st: mut BaseState, character_st: mut CharacterState, dx: float, dz: float, jump: bool, dt: float) -> void {
# standing still out here is floating, not swimming # standing still out here is floating, not swimming
if Math.abs(dx) + Math.abs(dz) > 0.05 { chr_float = 0.0 } else { chr_float = Math.min(1.0, chr_float + dt / 1.5) } if Math.abs(dx) + Math.abs(dz) > 0.05 { character_st.chr_float = 0.0 } else { character_st.chr_float = Math.min(1.0, character_st.chr_float + dt / 1.5) }
chr_dive_tick(jump, dt) chr_dive_tick(character_st, jump, dt)
chr_breath_tick(dt) chr_breath_tick(base_st, character_st, dt)
chr_current_tick(dt) chr_current_tick(base_st, character_st, dt)
let step = chr_speed * dt let step = character_st.chr_speed * dt
if step > 0.0 { if step > 0.0 {
chr_push(chr_x - Math.sin(chr_yaw) * step, chr_z - Math.cos(chr_yaw) * step, 0.35, chr_y, chr_y + CHAR_BODY) chr_push(character_st, character_st.chr_x - Math.sin(character_st.chr_yaw) * step, character_st.chr_z - Math.cos(character_st.chr_yaw) * step, 0.35, character_st.chr_y, character_st.chr_y + CHAR_BODY)
let mx = chr_out_x - chr_x let mx = character_st.chr_out_x - character_st.chr_x
let mz = chr_out_z - chr_z let mz = character_st.chr_out_z - character_st.chr_z
chr_step = Math.sqrt(mx * mx + mz * mz) character_st.chr_step = Math.sqrt(mx * mx + mz * mz)
chr_x = chr_out_x character_st.chr_x = character_st.chr_out_x
chr_z = chr_out_z character_st.chr_z = character_st.chr_out_z
} }
# the surface, with a slow bob so a floating body is not a statue on glass # the surface, with a slow bob so a floating body is not a statue on glass
let bob = 0.04 * Math.sin(chr_time * 3.8) let bob = 0.04 * Math.sin(character_st.chr_time * 3.8)
chr_y = chr_water - 0.30 + bob - chr_depth character_st.chr_y = character_st.chr_water - 0.30 + bob - character_st.chr_depth
# shallow enough to stand up in, and at the top rather than under it # shallow enough to stand up in, and at the top rather than under it
if chr_bed_depth(chr_x, chr_z) < CHAR_STAND_UP and chr_depth < 0.2 { chr_leave_water() } if chr_bed_depth(character_st.chr_x, character_st.chr_z) < CHAR_STAND_UP and character_st.chr_depth < 0.2 { chr_leave_water(base_st, character_st) }
} }

View file

@ -1,20 +1,20 @@
# system.ludic - the body as a system: reset, a tick from the input port, and nothing saved (where a # system.ludic - the body as a system: reset, a tick from the input port, and nothing saved (where a
# body stands is the game's to write, with the rest of the player) # body stands is the game's to write, with the rest of the player)
export function char_reset() -> void { export function char_reset(base_st: mut BaseState, character_st: mut CharacterState) -> void {
char_teleport(0.0, 0.0, 0.0) char_teleport(character_st, 0.0, 0.0, 0.0)
chr_time = 0.0 character_st.chr_time = 0.0
chr_phase = 0.0 character_st.chr_phase = 0.0
chr_breath = CHAR_BREATH_MAX character_st.chr_breath = CHAR_BREATH_MAX
chr_can_run = true character_st.chr_can_run = true
chr_sit_t = 0.0 character_st.chr_sit_t = 0.0
chr_steep = 0.0 character_st.chr_steep = 0.0
chr_land = 0.0 character_st.chr_land = 0.0
chr_edge_said = 0.0 character_st.chr_edge_said = 0.0
chr_fps = false character_st.chr_fps = false
q_clear(char_facts()) q_clear(base_st, char_facts(base_st, character_st))
} }
function chr_sys_tick(t: Tick) -> void { char_tick(t.dt) } function chr_sys_tick(base_st: mut BaseState, character_st: mut CharacterState, t: Tick) -> void { char_tick(base_st, character_st, t.dt) }
export function char_system() -> System { export function char_system() -> System {
let s = system_new("character", PH_INPUT) let s = system_new("character", PH_INPUT)

View file

@ -20,24 +20,32 @@ program CharacterTest {
return -10000.0 return -10000.0
} }
# a boulder at (0, 6), a metre round and 40 cm high, and a pillar at (-6, 0) # a boulder at (0, 6), a metre round and 40 cm high, and a pillar at (-6, 0)
var px: float = 0.0 state CharactertestState {
var pz: float = 0.0 px: float = 0.0
function fk_ring(x: float, z: float, cx: float, cz: float, r: float) -> bool { pz: float = 0.0
riding: bool = false
rides: int = 0
sneaking: bool = false
pace: float = 1.0
eyes: bool = true
mz: float = 0.0
}
function fk_ring(charactertest_st: mut CharactertestState, x: float, z: float, cx: float, cz: float, r: float) -> bool {
let dx = x - cx let dx = x - cx
let dz = z - cz let dz = z - cz
let d = Math.sqrt(dx * dx + dz * dz) let d = Math.sqrt(dx * dx + dz * dz)
if d >= r { return false } if d >= r { return false }
let k = r / Math.max(d, 0.001) let k = r / Math.max(d, 0.001)
px = cx + dx * k charactertest_st.px = cx + dx * k
pz = cz + dz * k charactertest_st.pz = cz + dz * k
return true return true
} }
function fk_push(x: float, z: float, r: float, y0: float, y1: float) -> bool { function fk_push(charactertest_st: mut CharactertestState, x: float, z: float, r: float, y0: float, y1: float) -> bool {
if y0 < 0.4 and fk_ring(x, z, 0.0, 6.0, 1.0 + r) { return true } if y0 < 0.4 and fk_ring(charactertest_st, x, z, 0.0, 6.0, 1.0 + r) { return true }
return fk_ring(x, z, -6.0, 0.0, 0.5 + r) return fk_ring(charactertest_st, x, z, -6.0, 0.0, 0.5 + r)
} }
function fk_px() -> float { return px } function fk_px(charactertest_st: CharactertestState) -> float { return charactertest_st.px }
function fk_pz() -> float { return pz } function fk_pz(charactertest_st: CharactertestState) -> float { return charactertest_st.pz }
function fk_top(x: float, z: float, r: float, from_y: float, step: float, base: float) -> float { function fk_top(x: float, z: float, r: float, from_y: float, step: float, base: float) -> float {
let dx = x let dx = x
let dz = z - 6.0 let dz = z - 6.0
@ -45,228 +53,222 @@ program CharacterTest {
return base return base
} }
var riding: bool = false function fk_riding(charactertest_st: CharactertestState) -> bool { return charactertest_st.riding }
var rides: int = 0 function fk_ride(charactertest_st: mut CharactertestState, ix: float, iz: float, run: bool, dt: float) -> void { charactertest_st.rides += 1 }
var sneaking: bool = false function fk_sneak(charactertest_st: CharactertestState) -> bool { return charactertest_st.sneaking }
var pace: float = 1.0 function fk_pace(charactertest_st: CharactertestState) -> float { return charactertest_st.pace }
var eyes: bool = true function fk_eyes(charactertest_st: CharactertestState) -> bool { return charactertest_st.eyes }
function fk_riding() -> bool { return riding }
function fk_ride(ix: float, iz: float, run: bool, dt: float) -> void { rides += 1 }
function fk_sneak() -> bool { return sneaking }
function fk_pace() -> float { return pace }
function fk_eyes() -> bool { return eyes }
var mz: float = 0.0 function fk_move_z(charactertest_st: CharactertestState) -> float { return charactertest_st.mz }
function fk_move_z() -> float { return mz }
bind CharacterGround { height: fn fk_height, water: fn fk_water, push: fn fk_push, pushed_x: fn fk_px, pushed_z: fn fk_pz, top_at: fn fk_top } bind CharacterGround { height: fn fk_height, water: fn fk_water, push: fn fk_push, pushed_x: fn fk_px, pushed_z: fn fk_pz, top_at: fn fk_top }
bind CharacterBody { riding: fn fk_riding, ride: fn fk_ride, sneaking: fn fk_sneak, pace: fn fk_pace, eyes_ok: fn fk_eyes } bind CharacterBody { riding: fn fk_riding, ride: fn fk_ride, sneaking: fn fk_sneak, pace: fn fk_pace, eyes_ok: fn fk_eyes }
bind CharacterInput { move_z: fn fk_move_z } bind CharacterInput { move_z: fn fk_move_z }
function fresh(x: float, z: float, yaw: float) -> void { function fresh(base_st: mut BaseState, character_st: mut CharacterState, x: float, z: float, yaw: float) -> void {
char_reset() char_reset(base_st, character_st)
char_release(CHAR_HOLD_SCREEN) char_release(character_st, CHAR_HOLD_SCREEN)
char_teleport(x, z, yaw) char_teleport(character_st, x, z, yaw)
} }
function facts() -> []CharacterFact { return q_drain(char_facts()) } function facts(base_st: mut BaseState, character_st: mut CharacterState) -> []CharacterFact { return q_drain(base_st, char_facts(base_st, character_st)) }
function count(fs: []CharacterFact, what: int) -> int { function count(fs: []CharacterFact, what: int) -> int {
var n = 0 var n = 0
for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } } for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } }
return n return n
} }
# walk the stick straight ahead (the camera looks where the body faces) for `secs` # walk the stick straight ahead (the camera looks where the body faces) for `secs`
function walk(secs: float, hz: float, run: bool) -> void { function walk(base_st: mut BaseState, character_st: mut CharacterState, secs: float, hz: float, run: bool) -> void {
let n = int(secs * hz) let n = int(secs * hz)
for i in 0 .. n { char_update(0.0, 1.0, run, false, 0.0, 0.0, 1.0 / hz) } for i in 0 .. n { char_update(base_st, character_st, 0.0, 1.0, run, false, 0.0, 0.0, 1.0 / hz) }
} }
# yaw 0 faces -z; the heading of a walk toward +x is -pi/2 # yaw 0 faces -z; the heading of a walk toward +x is -pi/2
const EAST: float = -1.5707963 const EAST: float = -1.5707963
const SOUTH: float = 3.14159265 const SOUTH: float = 3.14159265
test "a 45 cm deck is a step at 60 Hz and at 240" { test "a 45 cm deck is a step at 60 Hz and at 240" (base_st: mut BaseState, character_st: mut CharacterState) {
fresh(1.0, 0.0, EAST) fresh(base_st, character_st, 1.0, 0.0, EAST)
walk(2.0, 60.0, false) walk(base_st, character_st, 2.0, 60.0, false)
expect(body_x() > 5.0) expect(body_x(character_st) > 5.0)
expect_near(body_y(), 0.45, 0.01) expect_near(body_y(character_st), 0.45, 0.01)
fresh(1.0, 0.0, EAST) fresh(base_st, character_st, 1.0, 0.0, EAST)
walk(2.0, 240.0, false) walk(base_st, character_st, 2.0, 240.0, false)
expect(body_x() > 5.0) expect(body_x(character_st) > 5.0)
expect_near(body_y(), 0.45, 0.01) expect_near(body_y(character_st), 0.45, 0.01)
expect_eq(count(facts(), CHAR_TOO_STEEP), 0) expect_eq(count(facts(base_st, character_st), CHAR_TOO_STEEP), 0)
} }
test "a cliff is refused at the same place whatever the frame rate, and said once" { test "a cliff is refused at the same place whatever the frame rate, and said once" (base_st: mut BaseState, character_st: mut CharacterState) {
fresh(0.0, -8.0, 0.0) fresh(base_st, character_st, 0.0, -8.0, 0.0)
walk(3.0, 30.0, false) walk(base_st, character_st, 3.0, 30.0, false)
let at30 = body_z() let at30 = body_z(character_st)
expect(body_steep() > 0.0) expect(body_steep(character_st) > 0.0)
expect_eq(count(facts(), CHAR_TOO_STEEP), 1) expect_eq(count(facts(base_st, character_st), CHAR_TOO_STEEP), 1)
fresh(0.0, -8.0, 0.0) fresh(base_st, character_st, 0.0, -8.0, 0.0)
walk(3.0, 240.0, false) walk(base_st, character_st, 3.0, 240.0, false)
expect_near(body_z(), at30, 0.25) expect_near(body_z(character_st), at30, 0.25)
expect(body_z() > -10.0) expect(body_z(character_st) > -10.0)
expect(body_y() < 0.3) expect(body_y(character_st) < 0.3)
} }
test "a gentle rise is walked up" { test "a gentle rise is walked up" (base_st: mut BaseState, character_st: mut CharacterState) {
fresh(0.0, 8.0, SOUTH) fresh(base_st, character_st, 0.0, 8.0, SOUTH)
walk(3.0, 60.0, false) walk(base_st, character_st, 3.0, 60.0, false)
expect(body_z() > 14.0) expect(body_z(character_st) > 14.0)
expect(body_y() > 1.0) expect(body_y(character_st) > 1.0)
} }
test "the boots set the limit: a rise they cannot take is refused" { test "the boots set the limit: a rise they cannot take is refused" (base_st: mut BaseState, character_st: mut CharacterState) {
fresh(0.0, 8.0, SOUTH) fresh(base_st, character_st, 0.0, 8.0, SOUTH)
walk(0.2, 60.0, false) walk(base_st, character_st, 0.2, 60.0, false)
expect(not char_slope_blocks(body_y() + 0.1, 0.0, 1.0)) expect(not char_slope_blocks(character_st, body_y(character_st) + 0.1, 0.0, 1.0))
} }
test "the walk reaches the walk speed, the run the run speed, a sneak its own" { test "the walk reaches the walk speed, the run the run speed, a sneak its own" (base_st: mut BaseState, character_st: mut CharacterState, charactertest_st: mut CharactertestState) {
fresh(0.0, 0.0, SOUTH) fresh(base_st, character_st, 0.0, 0.0, SOUTH)
walk(2.0, 60.0, false) walk(base_st, character_st, 2.0, 60.0, false)
expect_near(body_speed(), 3.2, 0.05) expect_near(body_speed(character_st), 3.2, 0.05)
fresh(0.0, 0.0, SOUTH) fresh(base_st, character_st, 0.0, 0.0, SOUTH)
walk(2.0, 60.0, true) walk(base_st, character_st, 2.0, 60.0, true)
expect_near(body_speed(), 6.8, 0.05) expect_near(body_speed(character_st), 6.8, 0.05)
pace = 0.5 charactertest_st.pace = 0.5
fresh(0.0, 0.0, SOUTH) fresh(base_st, character_st, 0.0, 0.0, SOUTH)
walk(2.0, 60.0, false) walk(base_st, character_st, 2.0, 60.0, false)
expect_near(body_speed(), 1.6, 0.05) expect_near(body_speed(character_st), 1.6, 0.05)
pace = 1.0 charactertest_st.pace = 1.0
sneaking = true charactertest_st.sneaking = true
fresh(0.0, 0.0, SOUTH) fresh(base_st, character_st, 0.0, 0.0, SOUTH)
walk(2.0, 60.0, true) walk(base_st, character_st, 2.0, 60.0, true)
expect_near(body_speed(), 1.3, 0.05) expect_near(body_speed(character_st), 1.3, 0.05)
} }
test "a boulder is a wall to a body below its step and a step onto its top" { test "a boulder is a wall to a body below its step and a step onto its top" (base_st: mut BaseState, character_st: mut CharacterState) {
fresh(0.0, 3.0, SOUTH) fresh(base_st, character_st, 0.0, 3.0, SOUTH)
walk(2.0, 60.0, false) walk(base_st, character_st, 2.0, 60.0, false)
expect(body_z() < 5.1) expect(body_z(character_st) < 5.1)
expect_near(char_stand_at(0.0, 6.0, 0.0), 0.4, 0.001) expect_near(char_stand_at(0.0, 6.0, 0.0), 0.4, 0.001)
expect_near(char_stand_at(3.0, 6.0, 0.0), 0.0, 0.001) expect_near(char_stand_at(3.0, 6.0, 0.0), 0.0, 0.001)
# and the world's own ground never includes it # and the world's own ground never includes it
expect_near(fk_height(0.0, 6.0), 0.0, 0.001) expect_near(fk_height(0.0, 6.0), 0.0, 0.001)
} }
test "wading shelves into a swim, and the shallows give the ground back" { test "wading shelves into a swim, and the shallows give the ground back" (base_st: mut BaseState, character_st: mut CharacterState) {
fresh(18.0, 0.0, EAST) fresh(base_st, character_st, 18.0, 0.0, EAST)
walk(3.0, 60.0, false) walk(base_st, character_st, 3.0, 60.0, false)
let fs = facts() let fs = facts(base_st, character_st)
expect(body_swimming()) expect(body_swimming(character_st))
expect_eq(count(fs, CHAR_STARTED_SWIMMING), 1) expect_eq(count(fs, CHAR_STARTED_SWIMMING), 1)
expect_near(body_y(), -0.30, 0.06) expect_near(body_y(character_st), -0.30, 0.06)
expect(body_breath() > 40.0) expect(body_breath(character_st) > 40.0)
char_face(-EAST) char_face(character_st, -EAST)
char_look(-EAST, 0.0) char_look(character_st, -EAST, 0.0)
walk(8.0, 60.0, false) walk(base_st, character_st, 8.0, 60.0, false)
expect(not body_swimming()) expect(not body_swimming(character_st))
expect_eq(count(facts(), CHAR_LEFT_WATER), 1) expect_eq(count(facts(base_st, character_st), CHAR_LEFT_WATER), 1)
} }
test "the jump held dives, the air runs out, and the body surfaces" { test "the jump held dives, the air runs out, and the body surfaces" (base_st: mut BaseState, character_st: mut CharacterState) {
fresh(30.0, 0.0, 0.0) fresh(base_st, character_st, 30.0, 0.0, 0.0)
char_resume_swim(false, 0.0, 2.0) char_resume_swim(character_st, false, 0.0, 2.0)
for i in 0 .. 600 { char_update(0.0, 0.0, false, true, 0.0, 0.0, 1.0 / 60.0) } for i in 0 .. 600 { char_update(base_st, character_st, 0.0, 0.0, false, true, 0.0, 0.0, 1.0 / 60.0) }
let fs = facts() let fs = facts(base_st, character_st)
expect(count(fs, CHAR_OUT_OF_AIR) > 0) expect(count(fs, CHAR_OUT_OF_AIR) > 0)
for i in 0 .. 300 { char_update(0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0) } for i in 0 .. 300 { char_update(base_st, character_st, 0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0) }
expect(body_depth() < 0.05) expect(body_depth(character_st) < 0.05)
expect(body_breath() > 10.0) expect(body_breath(character_st) > 10.0)
} }
test "a jump leaves the ground, falls, lands, and says both" { test "a jump leaves the ground, falls, lands, and says both" (base_st: mut BaseState, character_st: mut CharacterState) {
fresh(0.0, 0.0, 0.0) fresh(base_st, character_st, 0.0, 0.0, 0.0)
char_update(0.0, 0.0, false, true, 0.0, 0.0, 1.0 / 60.0) char_update(base_st, character_st, 0.0, 0.0, false, true, 0.0, 0.0, 1.0 / 60.0)
expect(body_airborne()) expect(body_airborne(character_st))
var guard = 0 var guard = 0
while body_airborne() and guard < 200 { while body_airborne(character_st) and guard < 200 {
char_update(0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0) char_update(base_st, character_st, 0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0)
guard += 1 guard += 1
} }
let fs = facts() let fs = facts(base_st, character_st)
expect(not body_airborne()) expect(not body_airborne(character_st))
expect_eq(count(fs, CHAR_JUMPED), 1) expect_eq(count(fs, CHAR_JUMPED), 1)
expect_eq(count(fs, CHAR_LANDED), 1) expect_eq(count(fs, CHAR_LANDED), 1)
expect(guard > 30) expect(guard > 30)
char_allow_run(false) char_allow_run(character_st, false)
char_update(0.0, 0.0, false, true, 0.0, 0.0, 1.0 / 60.0) char_update(base_st, character_st, 0.0, 0.0, false, true, 0.0, 0.0, 1.0 / 60.0)
expect(not body_airborne()) expect(not body_airborne(character_st))
} }
test "the safe spot clears a collider, keeps out of deep water, and allows a wade when asked" { test "the safe spot clears a collider, keeps out of deep water, and allows a wade when asked" (character_st: mut CharacterState) {
expect(char_safe_spot(-6.0, 0.0, 0.0, 0.0)) expect(char_safe_spot(character_st, -6.0, 0.0, 0.0, 0.0))
let dx = body_safe_x() + 6.0 let dx = body_safe_x(character_st) + 6.0
let dz = body_safe_z() let dz = body_safe_z(character_st)
expect(dx * dx + dz * dz > 0.81) expect(dx * dx + dz * dz > 0.81)
expect(not char_safe_spot(40.0, 0.0, 0.0, 0.0)) expect(not char_safe_spot(character_st, 40.0, 0.0, 0.0, 0.0))
expect(char_safe_spot(20.5, 0.0, 0.0, 1.25)) expect(char_safe_spot(character_st, 20.5, 0.0, 0.0, 1.25))
expect(char_safe_spot(15.0, 0.0, 0.0, 0.0)) expect(char_safe_spot(character_st, 15.0, 0.0, 0.0, 0.0))
expect_near(body_safe_x(), 15.0, 0.001) expect_near(body_safe_x(character_st), 15.0, 0.001)
} }
test "sitting where you stand, and the refusals" { test "sitting where you stand, and the refusals" (base_st: mut BaseState, character_st: mut CharacterState, charactertest_st: mut CharactertestState) {
fresh(0.0, 0.0, 0.0) fresh(base_st, character_st, 0.0, 0.0, 0.0)
expect_eq(char_sit_here(), CHAR_SAT) expect_eq(char_sit_here(character_st), CHAR_SAT)
for i in 0 .. 30 { char_update(0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0) } for i in 0 .. 30 { char_update(base_st, character_st, 0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0) }
expect(body_sitting()) expect(body_sitting(character_st))
expect_near(body_sit_t(), 1.0, 0.01) expect_near(body_sit_t(character_st), 1.0, 0.01)
expect_eq(char_sit_here(), CHAR_STOOD) expect_eq(char_sit_here(character_st), CHAR_STOOD)
expect(not body_sitting()) expect(not body_sitting(character_st))
fresh(0.0, -14.0, 0.0) fresh(base_st, character_st, 0.0, -14.0, 0.0)
expect_eq(char_sit_here(), CHAR_NOT_STEEP) expect_eq(char_sit_here(character_st), CHAR_NOT_STEEP)
fresh(24.0, 0.0, 0.0) fresh(base_st, character_st, 24.0, 0.0, 0.0)
char_resume_swim(false, 0.0, 10.0) char_resume_swim(character_st, false, 0.0, 10.0)
expect_eq(char_sit_here(), CHAR_NOT_WET) expect_eq(char_sit_here(character_st), CHAR_NOT_WET)
riding = true charactertest_st.riding = true
fresh(0.0, 0.0, 0.0) fresh(base_st, character_st, 0.0, 0.0, 0.0)
expect_eq(char_sit_here(), CHAR_NOT_RIDING) expect_eq(char_sit_here(character_st), CHAR_NOT_RIDING)
riding = false charactertest_st.riding = false
} }
test "a hold stops the stick, and only its own reason lets it go" { test "a hold stops the stick, and only its own reason lets it go" (base_st: mut BaseState, character_st: mut CharacterState, charactertest_st: mut CharactertestState) {
fresh(0.0, 0.0, 0.0) fresh(base_st, character_st, 0.0, 0.0, 0.0)
mz = 1.0 charactertest_st.mz = 1.0
char_hold(CHAR_HOLD_STAGE) char_hold(character_st, CHAR_HOLD_STAGE)
char_hold(CHAR_HOLD_SCREEN) char_hold(character_st, CHAR_HOLD_SCREEN)
for i in 0 .. 60 { char_tick(1.0 / 60.0) } for i in 0 .. 60 { char_tick(base_st, character_st, 1.0 / 60.0) }
expect_near(body_z(), 0.0, 0.001) expect_near(body_z(character_st), 0.0, 0.001)
char_release(CHAR_HOLD_SCREEN) char_release(character_st, CHAR_HOLD_SCREEN)
char_release(CHAR_HOLD_SCREEN) char_release(character_st, CHAR_HOLD_SCREEN)
expect(body_held()) expect(body_held(character_st))
char_release(CHAR_HOLD_STAGE) char_release(character_st, CHAR_HOLD_STAGE)
for i in 0 .. 60 { char_tick(1.0 / 60.0) } for i in 0 .. 60 { char_tick(base_st, character_st, 1.0 / 60.0) }
expect(body_z() < -1.0) expect(body_z(character_st) < -1.0)
mz = 0.0 charactertest_st.mz = 0.0
} }
test "riding hands the stick over and leaves the body where the ride puts it" { test "riding hands the stick over and leaves the body where the ride puts it" (base_st: mut BaseState, character_st: mut CharacterState, charactertest_st: mut CharactertestState) {
fresh(0.0, 0.0, 0.0) fresh(base_st, character_st, 0.0, 0.0, 0.0)
riding = true charactertest_st.riding = true
rides = 0 charactertest_st.rides = 0
char_board(3.0, 3.0, 0.0) char_board(character_st, 3.0, 3.0, 0.0)
for i in 0 .. 10 { char_update(0.0, 1.0, false, false, 0.0, 0.0, 1.0 / 60.0) } for i in 0 .. 10 { char_update(base_st, character_st, 0.0, 1.0, false, false, 0.0, 0.0, 1.0 / 60.0) }
expect_eq(rides, 10) expect_eq(charactertest_st.rides, 10)
char_ride_at(4.0, 1.2, 3.0, 0.0, 2.0) char_ride_at(character_st, 4.0, 1.2, 3.0, 0.0, 2.0)
expect_near(body_x(), 4.0, 0.001) expect_near(body_x(character_st), 4.0, 0.001)
expect_near(body_y(), 1.2, 0.001) expect_near(body_y(character_st), 1.2, 0.001)
riding = false charactertest_st.riding = false
char_step_off(5.0, 0.0) char_step_off(character_st, 5.0, 0.0)
expect_near(body_y(), 0.45, 0.001) expect_near(body_y(character_st), 0.45, 0.001)
} }
test "the orbit sits behind and above the ground; the eyes at head height, refused when sat" { test "the orbit sits behind and above the ground; the eyes at head height, refused when sat" (base_st: mut BaseState, character_st: mut CharacterState) {
fresh(0.0, 0.0, 0.0) fresh(base_st, character_st, 0.0, 0.0, 0.0)
char_camera(1.0) char_camera(character_st, 1.0)
expect(body_eye_z() > 1.0) expect(body_eye_z(character_st) > 1.0)
expect(body_eye_y() > 0.55) expect(body_eye_y(character_st) > 0.55)
expect(not body_eyes_now()) expect(not body_eyes_now(character_st))
expect_eq(char_fps_toggle(), CHAR_EYES_ON) expect_eq(char_fps_toggle(character_st), CHAR_EYES_ON)
char_camera(1.0) char_camera(character_st, 1.0)
expect(body_eyes_now()) expect(body_eyes_now(character_st))
expect_near(body_eye_y(), 1.62, 0.05) expect_near(body_eye_y(character_st), 1.62, 0.05)
expect_eq(char_fps_toggle(), CHAR_EYES_OFF) expect_eq(char_fps_toggle(character_st), CHAR_EYES_OFF)
char_sit(0.0, 0.0, 0.0, CHAR_SIT_SEAT) char_sit(character_st, 0.0, 0.0, 0.0, CHAR_SIT_SEAT)
expect_eq(char_fps_toggle(), CHAR_EYES_SITTING) expect_eq(char_fps_toggle(character_st), CHAR_EYES_SITTING)
} }
} }

View file

@ -1,28 +1,28 @@
# update.ludic - a frame of the body: the stick in the camera's frame, the step, the gait, the camera # update.ludic - a frame of the body: the stick in the camera's frame, the step, the gait, the camera
export function char_update(ix0: float, iz0: float, run0: bool, jump0: bool, dyaw: float, dpitch: float, dt: float) -> void { export function char_update(base_st: mut BaseState, character_st: mut CharacterState, ix0: float, iz0: float, run0: bool, jump0: bool, dyaw: float, dpitch: float, dt: float) -> void {
var run = run0 and chr_can_run var run = run0 and character_st.chr_can_run
var ix = ix0 var ix = ix0
var iz = iz0 var iz = iz0
var jump = jump0 var jump = jump0
chr_step = 0.0 character_st.chr_step = 0.0
chr_time = chr_time + dt character_st.chr_time = character_st.chr_time + dt
chr_water = CharacterGround.water(chr_x, chr_z) character_st.chr_water = CharacterGround.water(character_st.chr_x, character_st.chr_z)
chr_sit_tick(dt) chr_sit_tick(character_st, dt)
if CharacterBody.riding() { if CharacterBody.riding() {
chr_ride_update(ix, iz, run, dyaw, dpitch, dt) chr_ride_update(character_st, ix, iz, run, dyaw, dpitch, dt)
return return
} }
if CharacterBody.sneaking() { run = false } if CharacterBody.sneaking() { run = false }
# sitting: any step stands up, and nothing else moves # sitting: any step stands up, and nothing else moves
if chr_sit and (Math.abs(ix) > 0.3 or Math.abs(iz) > 0.3 or jump) { chr_sit = false } if character_st.chr_sit and (Math.abs(ix) > 0.3 or Math.abs(iz) > 0.3 or jump) { character_st.chr_sit = false }
if chr_sit { if character_st.chr_sit {
ix = 0.0 ix = 0.0
iz = 0.0 iz = 0.0
jump = false jump = false
} }
# the stick in the camera's frame: forward on the ground is (-sin yaw, -cos yaw) # the stick in the camera's frame: forward on the ground is (-sin yaw, -cos yaw)
let cy = Math.cos(chr_cam_yaw) let cy = Math.cos(character_st.chr_cam_yaw)
let sy = Math.sin(chr_cam_yaw) let sy = Math.sin(character_st.chr_cam_yaw)
var dx = -sy * iz + cy * ix var dx = -sy * iz + cy * ix
var dz = -cy * iz - sy * ix var dz = -cy * iz - sy * ix
var mag = Math.sqrt(dx * dx + dz * dz) var mag = Math.sqrt(dx * dx + dz * dz)
@ -31,30 +31,30 @@ export function char_update(ix0: float, iz0: float, run0: bool, jump0: bool, dya
dz = dz / mag dz = dz / mag
mag = 1.0 mag = 1.0
} }
chr_steer(dx, dz, mag, run, dt) chr_steer(character_st, dx, dz, mag, run, dt)
let dist = chr_travel(dx, dz, -Math.sin(chr_yaw), -Math.cos(chr_yaw), jump, dt) let dist = chr_travel(base_st, character_st, dx, dz, -Math.sin(character_st.chr_yaw), -Math.cos(character_st.chr_yaw), jump, dt)
chr_gait(dist, dyaw, dpitch, dt) chr_gait(base_st, character_st, dist, dyaw, dpitch, dt)
char_camera(dt) char_camera(character_st, dt)
} }
# something else carries the body (CharacterBody.ride moves it with char_ride_at); the orbit # something else carries the body (CharacterBody.ride moves it with char_ride_at); the orbit
# drifts behind whatever it is on # drifts behind whatever it is on
function chr_ride_update(ix: float, iz: float, run: bool, dyaw: float, dpitch: float, dt: float) -> void { function chr_ride_update(character_st: mut CharacterState, ix: float, iz: float, run: bool, dyaw: float, dpitch: float, dt: float) -> void {
CharacterBody.ride(ix, iz, run, dt) CharacterBody.ride(ix, iz, run, dt)
chr_look(dyaw, dpitch) chr_look(character_st, dyaw, dpitch)
if dyaw == 0.0 and not CharacterInput.look_always() { chr_cam_yaw = chr_cam_yaw + char_wrap(chr_yaw - chr_cam_yaw) * Math.min(1.0, 1.5 * dt) } if dyaw == 0.0 and not CharacterInput.look_always() { character_st.chr_cam_yaw = character_st.chr_cam_yaw + char_wrap(character_st.chr_yaw - character_st.chr_cam_yaw) * Math.min(1.0, 1.5 * dt) }
char_camera(dt) char_camera(character_st, dt)
} }
# a frame from the input port: the game's devices, read once, stepping the body # a frame from the input port: the game's devices, read once, stepping the body
export function char_tick(dt: float) -> void { export function char_tick(base_st: mut BaseState, character_st: mut CharacterState, dt: float) -> void {
var ix = 0.0 var ix = 0.0
var iz = 0.0 var iz = 0.0
var run = false var run = false
var jump = false var jump = false
var dyaw = 0.0 var dyaw = 0.0
var dpitch = 0.0 var dpitch = 0.0
if chr_holds == 0 { if character_st.chr_holds == 0 {
ix = CharacterInput.move_x() ix = CharacterInput.move_x()
iz = CharacterInput.move_z() iz = CharacterInput.move_z()
run = CharacterInput.run() run = CharacterInput.run()
@ -62,7 +62,7 @@ export function char_tick(dt: float) -> void {
dyaw = CharacterInput.look_yaw() dyaw = CharacterInput.look_yaw()
dpitch = CharacterInput.look_pitch() dpitch = CharacterInput.look_pitch()
let wheel = CharacterInput.zoom() let wheel = CharacterInput.zoom()
if wheel != 0.0 { chr_cam_dist = Math.clamp(chr_cam_dist - wheel * 0.35, 1.8, 9.0) } if wheel != 0.0 { character_st.chr_cam_dist = Math.clamp(character_st.chr_cam_dist - wheel * 0.35, 1.8, 9.0) }
} }
char_update(ix, iz, run, jump, dyaw, dpitch, dt) char_update(base_st, character_st, ix, iz, run, jump, dyaw, dpitch, dt)
} }

View file

@ -1,125 +1,125 @@
# verbs.ludic - every change the rest of a game may make to the body # verbs.ludic - every change the rest of a game may make to the body
export function char_config(walk: float, run: float) -> void { export function char_config(character_st: mut CharacterState, walk: float, run: float) -> void {
chr_walk_speed = walk character_st.chr_walk_speed = walk
chr_run_speed = run character_st.chr_run_speed = run
} }
# put the body somewhere, standing, the camera behind it # put the body somewhere, standing, the camera behind it
export function char_teleport(x: float, z: float, yaw: float) -> void { export function char_teleport(character_st: mut CharacterState, x: float, z: float, yaw: float) -> void {
chr_x = x character_st.chr_x = x
chr_z = z character_st.chr_z = z
chr_yaw = yaw character_st.chr_yaw = yaw
chr_y = CharacterGround.height(x, z) character_st.chr_y = CharacterGround.height(x, z)
char_halt() char_halt(character_st)
chr_sit = false character_st.chr_sit = false
chr_swim = false character_st.chr_swim = false
chr_dive = false character_st.chr_dive = false
chr_depth = 0.0 character_st.chr_depth = 0.0
chr_float = 0.0 character_st.chr_float = 0.0
# the water where the body lands, not where it was: a trip reloaded mid-swim reads it next # the water where the body lands, not where it was: a trip reloaded mid-swim reads it next
chr_water = CharacterGround.water(x, z) character_st.chr_water = CharacterGround.water(x, z)
chr_cam_yaw = yaw character_st.chr_cam_yaw = yaw
chr_piv_x = chr_x character_st.chr_piv_x = character_st.chr_x
chr_piv_y = chr_y + 1.45 character_st.chr_piv_y = character_st.chr_y + 1.45
chr_piv_z = chr_z character_st.chr_piv_z = character_st.chr_z
char_camera(1.0) char_camera(character_st, 1.0)
} }
# a screen holds the body still by a reason, so two holders cannot release each other's hold # a screen holds the body still by a reason, so two holders cannot release each other's hold
export function char_hold(reason: int) -> void { chr_holds = chr_holds | reason } export function char_hold(character_st: mut CharacterState, reason: int) -> void { character_st.chr_holds = character_st.chr_holds | reason }
export function char_release(reason: int) -> void { if (chr_holds & reason) != 0 { chr_holds = chr_holds - reason } } export function char_release(character_st: mut CharacterState, reason: int) -> void { if (character_st.chr_holds & reason) != 0 { character_st.chr_holds = character_st.chr_holds - reason } }
# everything that carries the body stops: no fall, no stride, no run # everything that carries the body stops: no fall, no stride, no run
export function char_halt() -> void { export function char_halt(character_st: mut CharacterState) -> void {
chr_air = false character_st.chr_air = false
chr_vy = 0.0 character_st.chr_vy = 0.0
chr_speed = 0.0 character_st.chr_speed = 0.0
chr_move = 0.0 character_st.chr_move = 0.0
chr_run = 0.0 character_st.chr_run = 0.0
} }
# out of strength in the water: the stroke stops and the body floats # out of strength in the water: the stroke stops and the body floats
export function char_go_limp() -> void { export function char_go_limp(character_st: mut CharacterState) -> void {
chr_speed = 0.0 character_st.chr_speed = 0.0
chr_float = 1.0 character_st.chr_float = 1.0
} }
# set down somewhere without the teleport's reset (a swimmer turned back) # set down somewhere without the teleport's reset (a swimmer turned back)
export function char_push_to(x: float, z: float) -> void { export function char_push_to(character_st: mut CharacterState, x: float, z: float) -> void {
chr_x = x character_st.chr_x = x
chr_z = z character_st.chr_z = z
chr_speed = 0.0 character_st.chr_speed = 0.0
} }
# stood on the ground at a point, facing a way # stood on the ground at a point, facing a way
export function char_stand_on(x: float, z: float, yaw: float) -> void { export function char_stand_on(character_st: mut CharacterState, x: float, z: float, yaw: float) -> void {
chr_x = x character_st.chr_x = x
chr_z = z character_st.chr_z = z
chr_yaw = yaw character_st.chr_yaw = yaw
chr_y = CharacterGround.height(x, z) character_st.chr_y = CharacterGround.height(x, z)
} }
# a trip saved in the middle of the lake reloads in the middle of the lake, not on the bed # a trip saved in the middle of the lake reloads in the middle of the lake, not on the bed
export function char_resume_swim(dive: bool, depth: float, breath: float) -> void { export function char_resume_swim(character_st: mut CharacterState, dive: bool, depth: float, breath: float) -> void {
chr_water = CharacterGround.water(chr_x, chr_z) character_st.chr_water = CharacterGround.water(character_st.chr_x, character_st.chr_z)
chr_swim = true character_st.chr_swim = true
chr_dive = dive character_st.chr_dive = dive
chr_depth = depth character_st.chr_depth = depth
chr_breath = breath character_st.chr_breath = breath
chr_y = chr_water - 0.30 - chr_depth character_st.chr_y = character_st.chr_water - 0.30 - character_st.chr_depth
} }
# the map changed under the body: nothing about the old water may survive # the map changed under the body: nothing about the old water may survive
export function char_map_changed() -> void { export function char_map_changed(character_st: mut CharacterState) -> void {
chr_swim = false character_st.chr_swim = false
chr_dive = false character_st.chr_dive = false
chr_depth = 0.0 character_st.chr_depth = 0.0
chr_water = CharacterGround.water(chr_x, chr_z) character_st.chr_water = CharacterGround.water(character_st.chr_x, character_st.chr_z)
} }
export function char_face(yaw: float) -> void { chr_yaw = yaw } export function char_face(character_st: mut CharacterState, yaw: float) -> void { character_st.chr_yaw = yaw }
export function char_allow_run(on: bool) -> void { chr_can_run = on } export function char_allow_run(character_st: mut CharacterState, on: bool) -> void { character_st.chr_can_run = on }
export function char_look(yaw: float, pitch: float) -> void { export function char_look(character_st: mut CharacterState, yaw: float, pitch: float) -> void {
chr_cam_yaw = yaw character_st.chr_cam_yaw = yaw
chr_cam_pitch = pitch character_st.chr_cam_pitch = pitch
} }
export function char_look_pitch(pitch: float) -> void { chr_cam_pitch = pitch } export function char_look_pitch(character_st: mut CharacterState, pitch: float) -> void { character_st.chr_cam_pitch = pitch }
export function char_orbit(dist: float) -> void { chr_cam_dist = Math.clamp(dist, 1.8, 9.0) } export function char_orbit(character_st: mut CharacterState, dist: float) -> void { character_st.chr_cam_dist = Math.clamp(dist, 1.8, 9.0) }
export function char_lock_orbit(on: bool) -> void { chr_lock = on } export function char_lock_orbit(character_st: mut CharacterState, on: bool) -> void { character_st.chr_lock = on }
export function char_set_fps(on: bool) -> void { chr_fps = on } export function char_set_fps(character_st: mut CharacterState, on: bool) -> void { character_st.chr_fps = on }
# the eyes or the shoulder; refused while sitting or carried, and the answer says which # the eyes or the shoulder; refused while sitting or carried, and the answer says which
export function char_fps_toggle() -> int { export function char_fps_toggle(character_st: mut CharacterState) -> int {
if not chr_fps and not chr_eyes_allowed() { if not character_st.chr_fps and not chr_eyes_allowed(character_st) {
if chr_sit { return CHAR_EYES_SITTING } if character_st.chr_sit { return CHAR_EYES_SITTING }
return CHAR_EYES_RIDING return CHAR_EYES_RIDING
} }
chr_fps = not chr_fps character_st.chr_fps = not character_st.chr_fps
if chr_fps { return CHAR_EYES_ON } if character_st.chr_fps { return CHAR_EYES_ON }
return CHAR_EYES_OFF return CHAR_EYES_OFF
} }
# something to ride: getting on, being carried, and stepping off onto a spot char_safe_spot found # something to ride: getting on, being carried, and stepping off onto a spot char_safe_spot found
export function char_board(x: float, z: float, yaw: float) -> void { export function char_board(character_st: mut CharacterState, x: float, z: float, yaw: float) -> void {
chr_sit = false character_st.chr_sit = false
chr_x = x character_st.chr_x = x
chr_z = z character_st.chr_z = z
chr_yaw = yaw character_st.chr_yaw = yaw
chr_speed = 0.0 character_st.chr_speed = 0.0
chr_move = 0.0 character_st.chr_move = 0.0
chr_run = 0.0 character_st.chr_run = 0.0
} }
export function char_ride_at(x: float, y: float, z: float, yaw: float, speed: float) -> void { export function char_ride_at(character_st: mut CharacterState, x: float, y: float, z: float, yaw: float, speed: float) -> void {
chr_x = x character_st.chr_x = x
chr_y = y character_st.chr_y = y
chr_z = z character_st.chr_z = z
chr_yaw = yaw character_st.chr_yaw = yaw
chr_speed = speed character_st.chr_speed = speed
} }
export function char_step_off(x: float, z: float) -> void { export function char_step_off(character_st: mut CharacterState, x: float, z: float) -> void {
chr_x = x character_st.chr_x = x
chr_z = z character_st.chr_z = z
chr_y = CharacterGround.height(x, z) character_st.chr_y = CharacterGround.height(x, z)
} }

View file

@ -1,69 +1,69 @@
# walk.ludic - the step itself: swimming, in the air, or on the ground; the distance, for the gait # walk.ludic - the step itself: swimming, in the air, or on the ground; the distance, for the gait
function chr_travel(dx: float, dz: float, fx: float, fz: float, jump: bool, dt: float) -> float { function chr_travel(base_st: mut BaseState, character_st: mut CharacterState, dx: float, dz: float, fx: float, fz: float, jump: bool, dt: float) -> float {
if chr_swim { if character_st.chr_swim {
chr_swim_tick(dx, dz, jump, dt) chr_swim_tick(base_st, character_st, dx, dz, jump, dt)
return chr_speed * dt return character_st.chr_speed * dt
} }
if chr_air { return chr_fly(dx, dz, dt) } if character_st.chr_air { return chr_fly(base_st, character_st, dx, dz, dt) }
return chr_walk(fx, fz, jump, dt) return chr_walk(base_st, character_st, fx, fz, jump, dt)
} }
# ballistic: gravity, the takeoff's velocity carried, a little steering # ballistic: gravity, the takeoff's velocity carried, a little steering
function chr_fly(dx: float, dz: float, dt: float) -> float { function chr_fly(base_st: mut BaseState, character_st: mut CharacterState, dx: float, dz: float, dt: float) -> float {
chr_vy = chr_vy - 14.0 * dt character_st.chr_vy = character_st.chr_vy - 14.0 * dt
chr_air_vx = chr_air_vx + dx * (4.0 * dt) character_st.chr_air_vx = character_st.chr_air_vx + dx * (4.0 * dt)
chr_air_vz = chr_air_vz + dz * (4.0 * dt) character_st.chr_air_vz = character_st.chr_air_vz + dz * (4.0 * dt)
chr_push(chr_x + chr_air_vx * dt, chr_z + chr_air_vz * dt, 0.35, chr_y, chr_y + CHAR_BODY) chr_push(character_st, character_st.chr_x + character_st.chr_air_vx * dt, character_st.chr_z + character_st.chr_air_vz * dt, 0.35, character_st.chr_y, character_st.chr_y + CHAR_BODY)
chr_y = chr_y + chr_vy * dt character_st.chr_y = character_st.chr_y + character_st.chr_vy * dt
let ground = char_stand_at(chr_out_x, chr_out_z, chr_y) let ground = char_stand_at(character_st.chr_out_x, character_st.chr_out_z, character_st.chr_y)
chr_x = chr_out_x character_st.chr_x = character_st.chr_out_x
chr_z = chr_out_z character_st.chr_z = character_st.chr_out_z
if chr_y < ground and chr_vy < 0.0 { if character_st.chr_y < ground and character_st.chr_vy < 0.0 {
chr_y = ground character_st.chr_y = ground
chr_air = false character_st.chr_air = false
chr_land = 0.22 character_st.chr_land = 0.22
chr_speed = Math.sqrt(chr_air_vx * chr_air_vx + chr_air_vz * chr_air_vz) character_st.chr_speed = Math.sqrt(character_st.chr_air_vx * character_st.chr_air_vx + character_st.chr_air_vz * character_st.chr_air_vz)
chr_say(CHAR_LANDED, chr_speed) chr_say(base_st, character_st, CHAR_LANDED, character_st.chr_speed)
} }
return chr_speed * (dt * 0.4) return character_st.chr_speed * (dt * 0.4)
} }
# on the ground: a step, unless a slope refuses it or the water takes the body; then a jump # on the ground: a step, unless a slope refuses it or the water takes the body; then a jump
function chr_walk(fx: float, fz: float, jump: bool, dt: float) -> float { function chr_walk(base_st: mut BaseState, character_st: mut CharacterState, fx: float, fz: float, jump: bool, dt: float) -> float {
var dist = chr_speed * dt var dist = character_st.chr_speed * dt
if dist > 0.0 { dist = chr_walk_step(fx, fz, dist) } else { chr_y = CharacterGround.height(chr_x, chr_z) } if dist > 0.0 { dist = chr_walk_step(base_st, character_st, fx, fz, dist) } else { character_st.chr_y = CharacterGround.height(character_st.chr_x, character_st.chr_z) }
if jump and chr_can_run and chr_land < 0.05 { if jump and character_st.chr_can_run and character_st.chr_land < 0.05 {
chr_air = true character_st.chr_air = true
chr_vy = 5.0 character_st.chr_vy = 5.0
chr_air_vx = fx * chr_speed character_st.chr_air_vx = fx * character_st.chr_speed
chr_air_vz = fz * chr_speed character_st.chr_air_vz = fz * character_st.chr_speed
chr_say(CHAR_JUMPED, 0.0) chr_say(base_st, character_st, CHAR_JUMPED, 0.0)
} }
return dist return dist
} }
function chr_walk_step(fx: float, fz: float, dist: float) -> float { function chr_walk_step(base_st: mut BaseState, character_st: mut CharacterState, fx: float, fz: float, dist: float) -> float {
chr_push(chr_x + fx * dist, chr_z + fz * dist, 0.35, chr_y, chr_y + CHAR_BODY) chr_push(character_st, character_st.chr_x + fx * dist, character_st.chr_z + fz * dist, 0.35, character_st.chr_y, character_st.chr_y + CHAR_BODY)
let nx = chr_out_x let nx = character_st.chr_out_x
let nz = chr_out_z let nz = character_st.chr_out_z
let nh = char_stand_at(nx, nz, chr_y) let nh = char_stand_at(nx, nz, character_st.chr_y)
if char_slope_blocks(nh, fx, fz) { if char_slope_blocks(character_st, nh, fx, fz) {
if not (chr_steep > 0.0) { chr_say(CHAR_TOO_STEEP, nh - chr_y) } if not (character_st.chr_steep > 0.0) { chr_say(base_st, character_st, CHAR_TOO_STEEP, nh - character_st.chr_y) }
chr_steep = 1.2 character_st.chr_steep = 1.2
chr_speed = 0.0 character_st.chr_speed = 0.0
return 0.0 return 0.0
} }
# the lake shelves: shin-deep is a wade, and past CHAR_WADE_MAX it is a swim. Entering owns # the lake shelves: shin-deep is a wade, and past CHAR_WADE_MAX it is a swim. Entering owns
# the position and the height this frame, or the body sits on the bed for one frame. # the position and the height this frame, or the body sits on the bed for one frame.
if nh < CharacterGround.water(nx, nz) - CHAR_WADE_MAX { if nh < CharacterGround.water(nx, nz) - CHAR_WADE_MAX {
chr_enter_water(nx, nz) chr_enter_water(base_st, character_st, nx, nz)
return dist return dist
} }
let mx = nx - chr_x let mx = nx - character_st.chr_x
let mz = nz - chr_z let mz = nz - character_st.chr_z
chr_step = Math.sqrt(mx * mx + mz * mz) character_st.chr_step = Math.sqrt(mx * mx + mz * mz)
chr_x = nx character_st.chr_x = nx
chr_z = nz character_st.chr_z = nz
chr_y = nh character_st.chr_y = nh
return dist return dist
} }

View file

@ -1,73 +1,73 @@
# water.ludic - one axis decides it: how far the bed is below the surface where the body stands. # water.ludic - one axis decides it: how far the bed is below the surface where the body stands.
# Under CHAR_WADE_MAX the walk wades; past it the feet leave the bottom. Coming out wants a # Under CHAR_WADE_MAX the walk wades; past it the feet leave the bottom. Coming out wants a
# shallower bed (CHAR_STAND_UP) than going in did, so a shelving shore does not flicker. # shallower bed (CHAR_STAND_UP) than going in did, so a shelving shore does not flicker.
function chr_enter_water(x: float, z: float) -> void { function chr_enter_water(base_st: mut BaseState, character_st: mut CharacterState, x: float, z: float) -> void {
if chr_swim { return } if character_st.chr_swim { return }
chr_swim = true character_st.chr_swim = true
chr_dive = false character_st.chr_dive = false
chr_depth = 0.0 character_st.chr_depth = 0.0
chr_breath = body_breath_max() character_st.chr_breath = body_breath_max()
chr_float = 0.0 character_st.chr_float = 0.0
chr_air = false character_st.chr_air = false
chr_vy = 0.0 character_st.chr_vy = 0.0
chr_sit = false character_st.chr_sit = false
chr_x = x character_st.chr_x = x
chr_z = z character_st.chr_z = z
chr_water = CharacterGround.water(x, z) character_st.chr_water = CharacterGround.water(x, z)
chr_y = chr_water - 0.30 character_st.chr_y = character_st.chr_water - 0.30
chr_say(CHAR_STARTED_SWIMMING, 0.0) chr_say(base_st, character_st, CHAR_STARTED_SWIMMING, 0.0)
} }
function chr_leave_water() -> void { function chr_leave_water(base_st: mut BaseState, character_st: mut CharacterState) -> void {
if not chr_swim { return } if not character_st.chr_swim { return }
chr_swim = false character_st.chr_swim = false
chr_dive = false character_st.chr_dive = false
chr_depth = 0.0 character_st.chr_depth = 0.0
chr_y = CharacterGround.height(chr_x, chr_z) character_st.chr_y = CharacterGround.height(character_st.chr_x, character_st.chr_z)
chr_speed = Math.min(chr_speed, 1.0) character_st.chr_speed = Math.min(character_st.chr_speed, 1.0)
chr_say(CHAR_LEFT_WATER, 0.0) chr_say(base_st, character_st, CHAR_LEFT_WATER, 0.0)
} }
# the descent: the jump held pushes the body under, letting go floats it back up # the descent: the jump held pushes the body under, letting go floats it back up
function chr_dive_tick(jump: bool, dt: float) -> void { function chr_dive_tick(character_st: mut CharacterState, jump: bool, dt: float) -> void {
let bed = Math.max(chr_bed_depth(chr_x, chr_z), 0.0) let bed = Math.max(chr_bed_depth(character_st.chr_x, character_st.chr_z), 0.0)
let max_d = Math.min(6.0, Math.max(bed - 0.4, 0.0)) let max_d = Math.min(6.0, Math.max(bed - 0.4, 0.0))
if jump and max_d > 0.2 { if jump and max_d > 0.2 {
chr_dive = true character_st.chr_dive = true
chr_depth = Math.min(chr_depth + 1.2 * dt, max_d) character_st.chr_depth = Math.min(character_st.chr_depth + 1.2 * dt, max_d)
} else { } else {
chr_depth = Math.max(chr_depth - 1.6 * dt, 0.0) character_st.chr_depth = Math.max(character_st.chr_depth - 1.6 * dt, 0.0)
if chr_depth < 0.05 { chr_dive = false } if character_st.chr_depth < 0.05 { character_st.chr_dive = false }
} }
} }
# air is spent only under the surface, and comes back fast at the top # air is spent only under the surface, and comes back fast at the top
function chr_breath_tick(dt: float) -> void { function chr_breath_tick(base_st: mut BaseState, character_st: mut CharacterState, dt: float) -> void {
if not (chr_depth > 0.55) { if not (character_st.chr_depth > 0.55) {
chr_breath = Math.min(chr_breath + dt * 12.0, body_breath_max()) character_st.chr_breath = Math.min(character_st.chr_breath + dt * 12.0, body_breath_max())
return return
} }
chr_breath = chr_breath - dt character_st.chr_breath = character_st.chr_breath - dt
if chr_breath < 0.0 { if character_st.chr_breath < 0.0 {
chr_breath = 0.0 character_st.chr_breath = 0.0
chr_dive = false character_st.chr_dive = false
chr_depth = Math.max(chr_depth - 3.0 * dt, 0.0) character_st.chr_depth = Math.max(character_st.chr_depth - 3.0 * dt, 0.0)
chr_say(CHAR_OUT_OF_AIR, dt) chr_say(base_st, character_st, CHAR_OUT_OF_AIR, dt)
} }
} }
# The edge of the world is a current, not a wall: it takes the outward part of a stroke, so # The edge of the world is a current, not a wall: it takes the outward part of a stroke, so
# turning round always works. Said once every twelve seconds while it is strong. # turning round always works. Said once every twelve seconds while it is strong.
function chr_current_tick(dt: float) -> void { function chr_current_tick(base_st: mut BaseState, character_st: mut CharacterState, dt: float) -> void {
let fx = -Math.sin(chr_yaw) let fx = -Math.sin(character_st.chr_yaw)
let fz = -Math.cos(chr_yaw) let fz = -Math.cos(character_st.chr_yaw)
let take = Math.clamp(CharacterGround.current(chr_x, chr_z, fx, fz), 0.0, 1.0) let take = Math.clamp(CharacterGround.current(character_st.chr_x, character_st.chr_z, fx, fz), 0.0, 1.0)
if take > 0.0 { if take > 0.0 {
chr_speed = chr_speed * (1.0 - take) character_st.chr_speed = character_st.chr_speed * (1.0 - take)
if take > 0.55 and chr_edge_said < 0.0 { if take > 0.55 and character_st.chr_edge_said < 0.0 {
chr_edge_said = 12.0 character_st.chr_edge_said = 12.0
chr_say(CHAR_CURRENT, take) chr_say(base_st, character_st, CHAR_CURRENT, take)
} }
} }
chr_edge_said = chr_edge_said - dt character_st.chr_edge_said = character_st.chr_edge_said - dt
} }

View file

@ -1,48 +1,45 @@
# calendar.ludic - where a day falls: its month, its year, and its season, a pure function of the # calendar.ludic - where a day falls: its month, its year, and its season, a pure function of the
# day. The game says how long a month is, how many make a year, and which season each month is. # day. The game says how long a month is, how many make a year, and which season each month is.
var cal_month_days: int = 30
var cal_year_months: int = 12
var cal_season: []int = null
export function clock_calendar(month_days: int, season_of_month: []int) -> void { export function clock_calendar(clock_st: mut ClockState, month_days: int, season_of_month: []int) -> void {
cal_month_days = month_days clock_st.cal_month_days = month_days
cal_year_months = len(season_of_month) clock_st.cal_year_months = len(season_of_month)
cal_season = season_of_month clock_st.cal_season = season_of_month
} }
export function clock_month_days() -> int { return cal_month_days } export function clock_month_days(clock_st: ClockState) -> int { return clock_st.cal_month_days }
export function clock_year_months() -> int { return cal_year_months } export function clock_year_months(clock_st: ClockState) -> int { return clock_st.cal_year_months }
# the month a day falls in, 0 .. months - 1 # the month a day falls in, 0 .. months - 1
export function clock_month_of(day: int) -> int { return ((day - 1) / cal_month_days) % cal_year_months } export function clock_month_of(clock_st: ClockState, day: int) -> int { return ((day - 1) / clock_st.cal_month_days) % clock_st.cal_year_months }
export function clock_year_of(day: int) -> int { return (day - 1) / (cal_month_days * cal_year_months) + 1 } export function clock_year_of(clock_st: ClockState, day: int) -> int { return (day - 1) / (clock_st.cal_month_days * clock_st.cal_year_months) + 1 }
export function clock_day_of_month_of(day: int) -> int { return (day - 1) % cal_month_days + 1 } export function clock_day_of_month_of(clock_st: ClockState, day: int) -> int { return (day - 1) % clock_st.cal_month_days + 1 }
export function clock_season_of(day: int) -> int { export function clock_season_of(clock_st: ClockState, day: int) -> int {
if cal_season == null { return 0 } if clock_st.cal_season == null { return 0 }
return cal_season[clock_month_of(day)] return clock_st.cal_season[clock_month_of(clock_st, day)]
} }
export function clock_month() -> int { return clock_month_of(ck_day) } export function clock_month(clock_st: ClockState) -> int { return clock_month_of(clock_st, clock_st.ck_day) }
export function clock_year() -> int { return clock_year_of(ck_day) } export function clock_year(clock_st: ClockState) -> int { return clock_year_of(clock_st, clock_st.ck_day) }
export function clock_day_of_month() -> int { return clock_day_of_month_of(ck_day) } export function clock_day_of_month(clock_st: ClockState) -> int { return clock_day_of_month_of(clock_st, clock_st.ck_day) }
export function clock_season() -> int { return clock_season_of(ck_day) } export function clock_season(clock_st: ClockState) -> int { return clock_season_of(clock_st, clock_st.ck_day) }
# days from `day` until the season is another; 0 when a year has only one # days from `day` until the season is another; 0 when a year has only one
export function clock_days_to_change_from(day: int) -> int { export function clock_days_to_change_from(clock_st: ClockState, day: int) -> int {
let here = clock_season_of(day) let here = clock_season_of(clock_st, day)
for d in 1 .. cal_month_days * cal_year_months { for d in 1 .. clock_st.cal_month_days * clock_st.cal_year_months {
if clock_season_of(day + d) != here { return d } if clock_season_of(clock_st, day + d) != here { return d }
} }
return 0 return 0
} }
export function clock_days_to_change() -> int { return clock_days_to_change_from(ck_day) } export function clock_days_to_change(clock_st: ClockState) -> int { return clock_days_to_change_from(clock_st, clock_st.ck_day) }
# the first day (counted from day one) that falls in a season; -1 when none does # the first day (counted from day one) that falls in a season; -1 when none does
export function clock_first_day_of(season: int) -> int { export function clock_first_day_of(clock_st: ClockState, season: int) -> int {
for d in 1 .. cal_month_days * cal_year_months + 1 { for d in 1 .. clock_st.cal_month_days * clock_st.cal_year_months + 1 {
if clock_season_of(d) == season { return d } if clock_season_of(clock_st, d) == season { return d }
} }
return -1 return -1
} }

View file

@ -1,21 +1,21 @@
# queries.ludic - what the world asks the clock # queries.ludic - what the world asks the clock
export function clock_day() -> int { return ck_day } export function clock_day(clock_st: ClockState) -> int { return clock_st.ck_day }
export function clock_hours() -> float { return ck_hours } export function clock_hours(clock_st: ClockState) -> float { return clock_st.ck_hours }
export function clock_moon() -> float { return ck_moon } export function clock_moon(clock_st: ClockState) -> float { return clock_st.ck_moon }
export function clock_scale() -> float { return ck_scale } export function clock_scale(clock_st: ClockState) -> float { return clock_st.ck_scale }
# game seconds per real second right now, the rest speed-up included # game seconds per real second right now, the rest speed-up included
export function clock_rate() -> float { return ck_scale * ck_rest() } export function clock_rate(clock_st: ClockState) -> float { return clock_st.ck_scale * ck_rest() }
# the game hours `dt` real seconds are worth at the current rate # the game hours `dt` real seconds are worth at the current rate
export function clock_hours_in(dt: float) -> float { return dt * clock_rate() / 3600.0 } export function clock_hours_in(clock_st: ClockState, dt: float) -> float { return dt * clock_rate(clock_st) / 3600.0 }
export function clock_is_night() -> bool { return ck_hours > ck_night_from or ck_hours < ck_night_to } export function clock_is_night(clock_st: ClockState) -> bool { return clock_st.ck_hours > clock_st.ck_night_from or clock_st.ck_hours < clock_st.ck_night_to }
# "07:05" # "07:05"
export function clock_hhmm() -> string { export function clock_hhmm(clock_st: ClockState) -> string {
let h = int(Math.floor(ck_hours)) let h = int(Math.floor(clock_st.ck_hours))
let m = int(Math.floor((ck_hours - float(h)) * 60.0)) let m = int(Math.floor((clock_st.ck_hours - float(h)) * 60.0))
var hs = `{h}` var hs = `{h}`
var ms = `{m}` var ms = `{m}`
if h < 10 { hs = `0{h}` } if h < 10 { hs = `0{h}` }

View file

@ -20,23 +20,28 @@ export property DayTurned {
how: int = 0 how: int = 0
} }
var ck_day: int = 1 export state ClockState {
var ck_hours: float = 0.0 cal_month_days: int = 30
var ck_moon: float = 0.0 cal_year_months: int = 12
var ck_scale: float = 60.0 # game seconds per real second cal_season: []int = null
var ck_start_hours: float = 8.0 ck_day: int = 1
var ck_night_from: float = 20.0 ck_hours: float = 0.0
var ck_night_to: float = 6.0 ck_moon: float = 0.0
var ck_moon_days: float = 29.5 ck_scale: float = 60.0 # game seconds per real second
var ck_moon_lo: float = 0.0 ck_start_hours: float = 8.0
var ck_moon_hi: float = 1.0 ck_night_from: float = 20.0
var ck_dice: Rng = null ck_night_to: float = 6.0
var ck_days: Queue<DayTurned> = null ck_moon_days: float = 29.5
ck_moon_lo: float = 0.0
ck_moon_hi: float = 1.0
ck_dice: Rng = null
ck_days: Queue<DayTurned> = null
}
# the facts: each day that turned, in order; the game drains it and runs its morning # the facts: each day that turned, in order; the game drains it and runs its morning
export function clock_days() -> Queue<DayTurned> { export function clock_days(base_st: mut BaseState, clock_st: mut ClockState) -> Queue<DayTurned> {
if ck_days == null { ck_days = queue_new("clock.days") } if clock_st.ck_days == null { clock_st.ck_days = queue_new(base_st, "clock.days") }
return ck_days return clock_st.ck_days
} }
function ck_owns() -> bool { return ClockWorld.owns_world() } function ck_owns() -> bool { return ClockWorld.owns_world() }
@ -44,15 +49,15 @@ function ck_owns() -> bool { return ClockWorld.owns_world() }
function ck_rest() -> float { return ClockWorld.rest_scale() } function ck_rest() -> float { return ClockWorld.rest_scale() }
# when a trip starts, when the night is, and how long the moon's month is # when a trip starts, when the night is, and how long the moon's month is
export function clock_config(start_hours: float, night_from: float, night_to: float) -> void { export function clock_config(clock_st: mut ClockState, start_hours: float, night_from: float, night_to: float) -> void {
ck_start_hours = start_hours clock_st.ck_start_hours = start_hours
ck_night_from = night_from clock_st.ck_night_from = night_from
ck_night_to = night_to clock_st.ck_night_to = night_to
} }
# a new trip's moon falls in [lo, hi) of its month; the month is `days` long # a new trip's moon falls in [lo, hi) of its month; the month is `days` long
export function clock_config_moon(days: float, lo: float, hi: float) -> void { export function clock_config_moon(clock_st: mut ClockState, days: float, lo: float, hi: float) -> void {
ck_moon_days = days clock_st.ck_moon_days = days
ck_moon_lo = lo clock_st.ck_moon_lo = lo
ck_moon_hi = hi clock_st.ck_moon_hi = hi
} }

View file

@ -1,20 +1,20 @@
# system.ludic - the clock as a system: it runs first (PH_INPUT) so every later system reads this # system.ludic - the clock as a system: it runs first (PH_INPUT) so every later system reads this
# frame's hour, and it saves its own section # frame's hour, and it saves its own section
function clock_tick(t: Tick) -> void { clock_advance(t.hours) } function clock_tick(base_st: mut BaseState, clock_st: mut ClockState, t: Tick) -> void { clock_advance(base_st, clock_st, t.hours) }
export function clock_save() -> Val { export function clock_save(clock_st: ClockState) -> Val {
let v = Value.object() let v = Value.object()
sv_put_int(v, "day", ck_day) sv_put_int(v, "day", clock_st.ck_day)
sv_put_float(v, "hours", ck_hours) sv_put_float(v, "hours", clock_st.ck_hours)
sv_put_float(v, "moon", ck_moon) sv_put_float(v, "moon", clock_st.ck_moon)
return v return v
} }
export function clock_load(v: Val, version: int) -> void { export function clock_load(base_st: mut BaseState, clock_st: mut ClockState, v: Val, version: int) -> void {
ck_day = sv_int(v, "day", 1) clock_st.ck_day = sv_int(v, "day", 1)
clock_set_hours(sv_float(v, "hours", ck_start_hours)) clock_set_hours(clock_st, sv_float(v, "hours", clock_st.ck_start_hours))
ck_moon = sv_float(v, "moon", 0.5) clock_st.ck_moon = sv_float(v, "moon", 0.5)
q_clear(clock_days()) q_clear(base_st, clock_days(base_st, clock_st))
} }
export function clock_system() -> System { export function clock_system() -> System {

View file

@ -4,123 +4,125 @@ import "ludic.clock"
import "ludic.base" import "ludic.base"
program ClockTest { program ClockTest {
numbers float numbers float
var owns: bool = true state ClocktestState {
var rest: float = 1.0 owns: bool = true
rest: float = 1.0
}
function fake_owns() -> bool { return owns } function fake_owns(clocktest_st: ClocktestState) -> bool { return clocktest_st.owns }
function fake_rest() -> float { return rest } function fake_rest(clocktest_st: ClocktestState) -> float { return clocktest_st.rest }
function fake_seed() -> int { return 42 } function fake_seed() -> int { return 42 }
bind ClockWorld { owns_world: fn fake_owns, rest_scale: fn fake_rest, seed: fn fake_seed } bind ClockWorld { owns_world: fn fake_owns, rest_scale: fn fake_rest, seed: fn fake_seed }
function fresh() -> void { function fresh(base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) -> void {
owns = true clocktest_st.owns = true
rest = 1.0 clocktest_st.rest = 1.0
clock_config(8.0, 20.0, 6.0) clock_config(clock_st, 8.0, 20.0, 6.0)
clock_config_moon(29.5, 0.2, 0.8) clock_config_moon(clock_st, 29.5, 0.2, 0.8)
clock_set_scale(60.0) clock_set_scale(clock_st, 60.0)
let seasons = new []int let seasons = new []int
for m in 0 .. 12 { push(seasons, m / 3) } for m in 0 .. 12 { push(seasons, m / 3) }
clock_calendar(10, seasons) clock_calendar(clock_st, 10, seasons)
clock_reset() clock_reset(base_st, clock_st)
} }
function turned() -> []DayTurned { return q_drain(clock_days()) } function turned(base_st: mut BaseState, clock_st: mut ClockState) -> []DayTurned { return q_drain(base_st, clock_days(base_st, clock_st)) }
function waiting() -> int { return q_len(clock_days()) } function waiting(base_st: mut BaseState, clock_st: mut ClockState) -> int { return q_len(clock_days(base_st, clock_st)) }
function run_hours(h: float) -> void { function run_hours(h: float) -> void {
let s = clock_system() let s = clock_system()
s.tick(tick_new(0.016, 0, h)) s.tick(tick_new(0.016, 0, h))
} }
test "a trip starts on day one at the start hour, the moon inside its range" { test "a trip starts on day one at the start hour, the moon inside its range" (base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) {
fresh() fresh(base_st, clock_st, clocktest_st)
expect_eq(clock_day(), 1) expect_eq(clock_day(clock_st), 1)
expect(clock_hours() == 8.0) expect(clock_hours(clock_st) == 8.0)
expect(clock_moon() >= 0.2 and clock_moon() < 0.8) expect(clock_moon(clock_st) >= 0.2 and clock_moon(clock_st) < 0.8)
expect(clock_hhmm() == "08:00") expect(clock_hhmm(clock_st) == "08:00")
} }
test "the same seed gives the same moon" { test "the same seed gives the same moon" (base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) {
fresh() fresh(base_st, clock_st, clocktest_st)
let a = clock_moon() let a = clock_moon(clock_st)
clock_reset() clock_reset(base_st, clock_st)
expect(clock_moon() == a) expect(clock_moon(clock_st) == a)
} }
test "a real minute is a game hour at scale 60, and resting speeds it" { test "a real minute is a game hour at scale 60, and resting speeds it" (base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) {
fresh() fresh(base_st, clock_st, clocktest_st)
expect(clock_hours_in(60.0) == 1.0) expect(clock_hours_in(clock_st, 60.0) == 1.0)
rest = 4.0 clocktest_st.rest = 4.0
expect(clock_rate() == 240.0) expect(clock_rate(clock_st) == 240.0)
} }
test "past midnight the day turns, once, as a fact" { test "past midnight the day turns, once, as a fact" (base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) {
fresh() fresh(base_st, clock_st, clocktest_st)
run_hours(15.0) run_hours(15.0)
expect_eq(waiting(), 0) expect_eq(waiting(base_st, clock_st), 0)
run_hours(2.5) run_hours(2.5)
expect_eq(clock_day(), 2) expect_eq(clock_day(clock_st), 2)
expect(clock_hours() == 1.5) expect(clock_hours(clock_st) == 1.5)
let t = turned() let t = turned(base_st, clock_st)
expect_eq(len(t), 1) expect_eq(len(t), 1)
expect_eq(t[0].day, 2) expect_eq(t[0].day, 2)
expect_eq(t[0].how, DAY_STAYED_UP) expect_eq(t[0].how, DAY_STAYED_UP)
} }
test "a machine that does not own the world does not move it" { test "a machine that does not own the world does not move it" (base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) {
fresh() fresh(base_st, clock_st, clocktest_st)
owns = false clocktest_st.owns = false
run_hours(20.0) run_hours(20.0)
expect(clock_hours() == 8.0) expect(clock_hours(clock_st) == 8.0)
expect_eq(clock_day(), 1) expect_eq(clock_day(clock_st), 1)
} }
test "a night slept turns the day and moves the moon a month's share" { test "a night slept turns the day and moves the moon a month's share" (base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) {
fresh() fresh(base_st, clock_st, clocktest_st)
let m0 = clock_moon() let m0 = clock_moon(clock_st)
clock_set_hours(6.5) clock_set_hours(clock_st, 6.5)
clock_new_day() clock_new_day(base_st, clock_st)
expect_eq(clock_day(), 2) expect_eq(clock_day(clock_st), 2)
expect(clock_moon() > m0) expect(clock_moon(clock_st) > m0)
expect_eq(turned()[0].how, DAY_SLEPT) expect_eq(turned(base_st, clock_st)[0].how, DAY_SLEPT)
} }
test "night is outside the day's hours, and the hour wraps" { test "night is outside the day's hours, and the hour wraps" (base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) {
fresh() fresh(base_st, clock_st, clocktest_st)
clock_set_hours(21.0) clock_set_hours(clock_st, 21.0)
expect(clock_is_night()) expect(clock_is_night(clock_st))
clock_set_hours(12.0) clock_set_hours(clock_st, 12.0)
expect(not clock_is_night()) expect(not clock_is_night(clock_st))
clock_set_hours(25.0) clock_set_hours(clock_st, 25.0)
expect(clock_hours() == 1.0) expect(clock_hours(clock_st) == 1.0)
} }
test "the calendar is a function of the day" { test "the calendar is a function of the day" (base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) {
fresh() fresh(base_st, clock_st, clocktest_st)
expect_eq(clock_season_of(1), 0) expect_eq(clock_season_of(clock_st, 1), 0)
expect_eq(clock_month_of(31), 3) expect_eq(clock_month_of(clock_st, 31), 3)
expect_eq(clock_season_of(31), 1) expect_eq(clock_season_of(clock_st, 31), 1)
expect_eq(clock_day_of_month_of(35), 5) expect_eq(clock_day_of_month_of(clock_st, 35), 5)
expect_eq(clock_year_of(121), 2) expect_eq(clock_year_of(clock_st, 121), 2)
expect_eq(clock_first_day_of(2), 61) expect_eq(clock_first_day_of(clock_st, 2), 61)
clock_set_day(28) clock_set_day(clock_st, 28)
expect_eq(clock_days_to_change(), 3) expect_eq(clock_days_to_change(clock_st), 3)
expect_eq(clock_season(), 0) expect_eq(clock_season(clock_st), 0)
} }
test "a save section reads back the same clock" { test "a save section reads back the same clock" (base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) {
fresh() fresh(base_st, clock_st, clocktest_st)
clock_set_day(17) clock_set_day(clock_st, 17)
clock_set_hours(13.25) clock_set_hours(clock_st, 13.25)
clock_set_moon(0.375) clock_set_moon(clock_st, 0.375)
let root = save_tree() let root = save_tree()
save_section(root, "clock", 1, clock_save()) save_section(root, "clock", 1, clock_save(clock_st))
let text = save_encode(root) let text = save_encode(root)
clock_reset() clock_reset(base_st, clock_st)
let n = load_section(save_decode(text), "clock") let n = load_section(save_decode(text), "clock")
clock_load(n.data, n.version) clock_load(base_st, clock_st, n.data, n.version)
expect_eq(clock_day(), 17) expect_eq(clock_day(clock_st), 17)
expect(clock_hours() == 13.25) expect(clock_hours(clock_st) == 13.25)
expect(clock_moon() == 0.375) expect(clock_moon(clock_st) == 0.375)
} }
} }

View file

@ -1,43 +1,43 @@
# verbs.ludic - the only way the clock changes # verbs.ludic - the only way the clock changes
export function clock_set_scale(s: float) -> void { ck_scale = s } export function clock_set_scale(clock_st: mut ClockState, s: float) -> void { clock_st.ck_scale = s }
export function clock_set_day(d: int) -> void { ck_day = d } export function clock_set_day(clock_st: mut ClockState, d: int) -> void { clock_st.ck_day = d }
export function clock_set_moon(m: float) -> void { ck_moon = m } export function clock_set_moon(clock_st: mut ClockState, m: float) -> void { clock_st.ck_moon = m }
# the hour, wrapped into [0, 24); setting it never turns the day - that is clock_new_day's # the hour, wrapped into [0, 24); setting it never turns the day - that is clock_new_day's
export function clock_set_hours(h: float) -> void { export function clock_set_hours(clock_st: mut ClockState, h: float) -> void {
var x = h % 24.0 var x = h % 24.0
if x < 0.0 { x = x + 24.0 } if x < 0.0 { x = x + 24.0 }
ck_hours = x clock_st.ck_hours = x
} }
function ck_turn(how: int) -> void { function ck_turn(base_st: mut BaseState, clock_st: mut ClockState, how: int) -> void {
ck_day += 1 clock_st.ck_day += 1
ck_moon = (ck_moon + 1.0 / ck_moon_days) % 1.0 clock_st.ck_moon = (clock_st.ck_moon + 1.0 / clock_st.ck_moon_days) % 1.0
let d = new DayTurned let d = new DayTurned
d.day = ck_day d.day = clock_st.ck_day
d.how = how d.how = how
q_push(clock_days(), d) q_push(base_st, clock_days(base_st, clock_st), d)
} }
# a new day however it came (a night slept, a collapse); the caller sets the hour first # a new day however it came (a night slept, a collapse); the caller sets the hour first
export function clock_new_day() -> void { ck_turn(DAY_SLEPT) } export function clock_new_day(base_st: mut BaseState, clock_st: mut ClockState) -> void { ck_turn(base_st, clock_st, DAY_SLEPT) }
# run the hours on by `gh` game hours; past midnight the day turns. Only the machine that owns # run the hours on by `gh` game hours; past midnight the day turns. Only the machine that owns
# the world moves it - another machine is told the time. # the world moves it - another machine is told the time.
export function clock_advance(gh: float) -> void { export function clock_advance(base_st: mut BaseState, clock_st: mut ClockState, gh: float) -> void {
if not ck_owns() { return } if not ck_owns() { return }
ck_hours = ck_hours + gh clock_st.ck_hours = clock_st.ck_hours + gh
while not (ck_hours < 24.0) { while not (clock_st.ck_hours < 24.0) {
ck_hours = ck_hours - 24.0 clock_st.ck_hours = clock_st.ck_hours - 24.0
ck_turn(DAY_STAYED_UP) ck_turn(base_st, clock_st, DAY_STAYED_UP)
} }
} }
# a new trip: day one at the start hour, the moon somewhere in its month # a new trip: day one at the start hour, the moon somewhere in its month
export function clock_reset() -> void { export function clock_reset(base_st: mut BaseState, clock_st: mut ClockState) -> void {
ck_dice = rng_new(ClockWorld.seed()) clock_st.ck_dice = rng_new(ClockWorld.seed())
ck_day = 1 clock_st.ck_day = 1
ck_hours = ck_start_hours clock_st.ck_hours = clock_st.ck_start_hours
ck_moon = rng_span(ck_dice, ck_moon_lo, ck_moon_hi) % 1.0 clock_st.ck_moon = rng_span(clock_st.ck_dice, clock_st.ck_moon_lo, clock_st.ck_moon_hi) % 1.0
q_clear(clock_days()) q_clear(base_st, clock_days(base_st, clock_st))
} }

View file

@ -1,15 +1,15 @@
# bearing.ludic - the frame's marks as the strip reads them: how many, each one, where it bears # bearing.ludic - the frame's marks as the strip reads them: how many, each one, where it bears
# from the viewer's facing and how far it is. A heading is the world's (0 toward -z, a quarter # from the viewer's facing and how far it is. A heading is the world's (0 toward -z, a quarter
# turn toward -x), a bearing is the heading less the viewer's yaw, in -pi .. pi. # turn toward -x), a bearing is the heading less the viewer's yaw, in -pi .. pi.
export function compass_count() -> int { export function compass_count(compass_st: CompassState) -> int {
if cmp_list == null { return 0 } if compass_st.cmp_list == null { return 0 }
return len(cmp_list) return len(compass_st.cmp_list)
} }
# the i-th mark, or an empty one past the end # the i-th mark, or an empty one past the end
export function compass_at(i: int) -> CompassMark { export function compass_at(compass_st: CompassState, i: int) -> CompassMark {
if i < 0 or i >= compass_count() { return new CompassMark } if i < 0 or i >= compass_count(compass_st) { return new CompassMark }
return cmp_list[i] return compass_st.cmp_list[i]
} }
export function compass_wrap(a: float) -> float { export function compass_wrap(a: float) -> float {
@ -33,13 +33,13 @@ export function compass_dist_to(x: float, z: float) -> float {
return Math.sqrt(dx * dx + dz * dz) return Math.sqrt(dx * dx + dz * dz)
} }
export function compass_bearing(i: int) -> float { export function compass_bearing(compass_st: CompassState, i: int) -> float {
let m = compass_at(i) let m = compass_at(compass_st, i)
return compass_bearing_to(m.x, m.z) return compass_bearing_to(m.x, m.z)
} }
export function compass_dist(i: int) -> float { export function compass_dist(compass_st: CompassState, i: int) -> float {
let m = compass_at(i) let m = compass_at(compass_st, i)
return compass_dist_to(m.x, m.z) return compass_dist_to(m.x, m.z)
} }
@ -47,8 +47,8 @@ export function compass_dist(i: int) -> float {
export function compass_strip_t(bearing: float, half: float) -> float { return bearing / Math.max(half, 0.001) } export function compass_strip_t(bearing: float, half: float) -> float { return bearing / Math.max(half, 0.001) }
# the objective: the first tracked mark of the frame, or -1 # the objective: the first tracked mark of the frame, or -1
export function compass_tracked() -> int { export function compass_tracked(compass_st: CompassState) -> int {
for i in 0 .. compass_count() { if cmp_list[i].prio >= COMPASS_TRACKED { return i } } for i in 0 .. compass_count(compass_st) { if compass_st.cmp_list[i].prio >= COMPASS_TRACKED { return i } }
return -1 return -1
} }

View file

@ -1,56 +1,56 @@
# gather.ludic - a frame's marks: begin, every provider marks, end sorts and culls. While a # gather.ludic - a frame's marks: begin, every provider marks, end sorts and culls. While a
# capture is open the marks go to whoever is listening instead, whatever the tier. # capture is open the marks go to whoever is listening instead, whatever the tier.
export function compass_begin() -> void { cmp_list = new []CompassMark } export function compass_begin(compass_st: mut CompassState) -> void { compass_st.cmp_list = new []CompassMark }
# a provider's verb: a thing at (x, z), shown from compass `tier` up # a provider's verb: a thing at (x, z), shown from compass `tier` up
export function compass_mark(x: float, z: float, icon: int, colour: int, tier: int, prio: int, label: string) -> void { export function compass_mark(compass_st: mut CompassState, x: float, z: float, icon: int, colour: int, tier: int, prio: int, label: string) -> void {
if cmp_list == null { compass_begin() } if compass_st.cmp_list == null { compass_begin(compass_st) }
let m = cmp_new(x, z, icon, colour, tier, prio, label) let m = cmp_new(x, z, icon, colour, tier, prio, label)
if cmp_capturing { if compass_st.cmp_capturing {
push(cmp_caught, m) push(compass_st.cmp_caught, m)
return return
} }
if CompassWorld.tier() < tier { return } if CompassWorld.tier() < tier { return }
push(cmp_list, m) push(compass_st.cmp_list, m)
} }
# highest priority first, the providers' order kept within one; then the cap # highest priority first, the providers' order kept within one; then the cap
export function compass_end() -> void { export function compass_end(compass_st: mut CompassState) -> void {
if cmp_list == null { compass_begin() } if compass_st.cmp_list == null { compass_begin(compass_st) }
let out = new []CompassMark let out = new []CompassMark
for p in 0 .. 3 { for p in 0 .. 3 {
let want = COMPASS_TRACKED - p let want = COMPASS_TRACKED - p
for i in 0 .. len(cmp_list) { if cmp_band(cmp_list[i].prio) == want and len(out) < cmp_cap { push(out, cmp_list[i]) } } for i in 0 .. len(compass_st.cmp_list) { if cmp_band(compass_st.cmp_list[i].prio) == want and len(out) < compass_st.cmp_cap { push(out, compass_st.cmp_list[i]) } }
} }
cmp_list = out compass_st.cmp_list = out
} }
function cmp_band(p: int) -> int { return Math.clamp(p, COMPASS_PLAIN, COMPASS_TRACKED) } function cmp_band(p: int) -> int { return Math.clamp(p, COMPASS_PLAIN, COMPASS_TRACKED) }
# once a frame: ask every registered provider, in the registry's order # once a frame: ask every registered provider, in the registry's order
export function compass_gather() -> void { export function compass_gather(compass_st: mut CompassState) -> void {
compass_begin() compass_begin(compass_st)
if CompassWorld.tier() <= 0 { return } if CompassWorld.tier() <= 0 { return }
for i in 0 .. COMPASS_FROM_COUNT { for i in 0 .. COMPASS_FROM_COUNT {
let f = CompassProviders[i].gather let f = CompassProviders[i].gather
if f != null { f() } if f != null { f() }
} }
compass_end() compass_end(compass_st)
} }
# someone else wants to know where the providers point (a guide's arrow): until the end every # someone else wants to know where the providers point (a guide's arrow): until the end every
# mark is theirs, whatever the tier, and the frame's list is left alone # mark is theirs, whatever the tier, and the frame's list is left alone
export function compass_capture_begin() -> void { export function compass_capture_begin(compass_st: mut CompassState) -> void {
cmp_capturing = true compass_st.cmp_capturing = true
cmp_caught = new []CompassMark compass_st.cmp_caught = new []CompassMark
} }
export function compass_capture_end() -> []CompassMark { export function compass_capture_end(compass_st: mut CompassState) -> []CompassMark {
cmp_capturing = false compass_st.cmp_capturing = false
var got = cmp_caught var got = compass_st.cmp_caught
cmp_caught = new []CompassMark compass_st.cmp_caught = new []CompassMark
if got == null { got = new []CompassMark } if got == null { got = new []CompassMark }
return got return got
} }
export function compass_capturing() -> bool { return cmp_capturing } export function compass_capturing(compass_st: CompassState) -> bool { return compass_st.cmp_capturing }

View file

@ -22,15 +22,19 @@ export property CompassProvider {
export open registry CompassProviders of CompassProvider as COMPASS_FROM export open registry CompassProviders of CompassProvider as COMPASS_FROM
var cmp_cap: int = 64 export state CompassState {
var cmp_list: []CompassMark = null # this frame's, sorted and culled cmp_cap: int = 64
var cmp_capturing: bool = false cmp_list: []CompassMark = null # this frame's, sorted and culled
var cmp_caught: []CompassMark = null # what providers said while someone was listening cmp_capturing: bool = false
cmp_caught: []CompassMark = null # what providers said while someone was listening
cmp_ranges: []float = null
cmp_radar_from: int = 2 # marks of at least this tier are blips; the tracked one always is
}
# how many marks a frame keeps (the lowest priorities go first past it) # how many marks a frame keeps (the lowest priorities go first past it)
export function compass_config(cap: int) -> void { export function compass_config(compass_st: mut CompassState, cap: int) -> void {
cmp_cap = Math.max(cap, 1) compass_st.cmp_cap = Math.max(cap, 1)
compass_begin() compass_begin(compass_st)
} }
function cmp_new(x: float, z: float, icon: int, colour: int, tier: int, prio: int, label: string) -> CompassMark { function cmp_new(x: float, z: float, icon: int, colour: int, tier: int, prio: int, label: string) -> CompassMark {

View file

@ -1,40 +1,38 @@
# radar.ludic - the radar's tiers: a range for each compass tier (0 is no radar), which marks it # radar.ludic - the radar's tiers: a range for each compass tier (0 is no radar), which marks it
# shows, and where each falls on a unit disc with ahead at the top # shows, and where each falls on a unit disc with ahead at the top
var cmp_ranges: []float = null
var cmp_radar_from: int = 2 # marks of at least this tier are blips; the tracked one always is
export function compass_config_radar(ranges: []float, from_tier: int) -> void { export function compass_config_radar(compass_st: mut CompassState, ranges: []float, from_tier: int) -> void {
cmp_ranges = new []float compass_st.cmp_ranges = new []float
for i in 0 .. len(ranges) { push(cmp_ranges, ranges[i]) } for i in 0 .. len(ranges) { push(compass_st.cmp_ranges, ranges[i]) }
cmp_radar_from = from_tier compass_st.cmp_radar_from = from_tier
} }
# how far the radar reaches with the viewer's compass, 0 without one; a tier past the table has # how far the radar reaches with the viewer's compass, 0 without one; a tier past the table has
# the last range # the last range
export function compass_radar_range() -> float { export function compass_radar_range(compass_st: CompassState) -> float {
if cmp_ranges == null or len(cmp_ranges) == 0 { return 0.0 } if compass_st.cmp_ranges == null or len(compass_st.cmp_ranges) == 0 { return 0.0 }
let t = CompassWorld.tier() let t = CompassWorld.tier()
if t < 0 { return 0.0 } if t < 0 { return 0.0 }
if t >= len(cmp_ranges) { return cmp_ranges[len(cmp_ranges) - 1] } if t >= len(compass_st.cmp_ranges) { return compass_st.cmp_ranges[len(compass_st.cmp_ranges) - 1] }
return cmp_ranges[t] return compass_st.cmp_ranges[t]
} }
export function compass_radar_on() -> bool { return compass_radar_range() > 0.0 } export function compass_radar_on(compass_st: CompassState) -> bool { return compass_radar_range(compass_st) > 0.0 }
# the i-th mark is a blip: a radar is carried, it is shown there and it is in range # the i-th mark is a blip: a radar is carried, it is shown there and it is in range
export function compass_radar_has(i: int) -> bool { export function compass_radar_has(compass_st: CompassState, i: int) -> bool {
if i < 0 or i >= compass_count() or not compass_radar_on() { return false } if i < 0 or i >= compass_count(compass_st) or not compass_radar_on(compass_st) { return false }
let m = cmp_list[i] let m = compass_st.cmp_list[i]
if m.tier < cmp_radar_from and m.prio < COMPASS_TRACKED { return false } if m.tier < compass_st.cmp_radar_from and m.prio < COMPASS_TRACKED { return false }
return compass_dist(i) <= compass_radar_range() return compass_dist(compass_st, i) <= compass_radar_range(compass_st)
} }
# where on a unit disc: x right, y down, so ahead is (0, -1) and the edge is the range # where on a unit disc: x right, y down, so ahead is (0, -1) and the edge is the range
export function compass_radar_x(i: int) -> float { return -Math.sin(compass_bearing(i)) * cmp_radar_frac(i) } export function compass_radar_x(compass_st: CompassState, i: int) -> float { return -Math.sin(compass_bearing(compass_st, i)) * cmp_radar_frac(compass_st, i) }
export function compass_radar_y(i: int) -> float { return -Math.cos(compass_bearing(i)) * cmp_radar_frac(i) } export function compass_radar_y(compass_st: CompassState, i: int) -> float { return -Math.cos(compass_bearing(compass_st, i)) * cmp_radar_frac(compass_st, i) }
function cmp_radar_frac(i: int) -> float { function cmp_radar_frac(compass_st: CompassState, i: int) -> float {
let r = compass_radar_range() let r = compass_radar_range(compass_st)
if r <= 0.0 { return 0.0 } if r <= 0.0 { return 0.0 }
return Math.min(compass_dist(i) / r, 1.0) return Math.min(compass_dist(compass_st, i) / r, 1.0)
} }

View file

@ -6,120 +6,122 @@ import "ludic.base"
program CompassTest { program CompassTest {
numbers float numbers float
var vx: float = 0.0 state CompasstestState {
var vz: float = 0.0 vx: float = 0.0
var yaw: float = 0.0 vz: float = 0.0
var tier: int = 4 yaw: float = 0.0
var objective: bool = true tier: int = 4
objective: bool = true
}
function fk_x() -> float { return vx } function fk_x(compasstest_st: CompasstestState) -> float { return compasstest_st.vx }
function fk_z() -> float { return vz } function fk_z(compasstest_st: CompasstestState) -> float { return compasstest_st.vz }
function fk_yaw() -> float { return yaw } function fk_yaw(compasstest_st: CompasstestState) -> float { return compasstest_st.yaw }
function fk_tier() -> int { return tier } function fk_tier(compasstest_st: CompasstestState) -> int { return compasstest_st.tier }
bind CompassWorld { x: fn fk_x, z: fn fk_z, yaw: fn fk_yaw, tier: fn fk_tier } bind CompassWorld { x: fn fk_x, z: fn fk_z, yaw: fn fk_yaw, tier: fn fk_tier }
function places() -> void { function places(compass_st: mut CompassState) -> void {
compass_mark(0.0, -100.0, 1, 10, 1, COMPASS_PLAIN, "camp") compass_mark(compass_st, 0.0, -100.0, 1, 10, 1, COMPASS_PLAIN, "camp")
compass_mark(-50.0, 0.0, 2, 11, 2, COMPASS_PLAIN, "sign") compass_mark(compass_st, -50.0, 0.0, 2, 11, 2, COMPASS_PLAIN, "sign")
compass_mark(0.0, 300.0, 3, 12, 4, COMPASS_PLAIN, "legend") compass_mark(compass_st, 0.0, 300.0, 3, 12, 4, COMPASS_PLAIN, "legend")
compass_mark(10.0, 10.0, 4, 13, 1, COMPASS_NOTE, "my mark") compass_mark(compass_st, 10.0, 10.0, 4, 13, 1, COMPASS_NOTE, "my mark")
} }
function goal() -> void { if objective { compass_mark(80.0, 0.0, 5, 14, 2, COMPASS_TRACKED, "the dock") } } function goal(compass_st: mut CompassState, compasstest_st: CompasstestState) -> void { if compasstest_st.objective { compass_mark(compass_st, 80.0, 0.0, 5, 14, 2, COMPASS_TRACKED, "the dock") } }
def CompassProviders places { gather: fn places } def CompassProviders places { gather: fn places }
def CompassProviders goal { gather: fn goal } def CompassProviders goal { gather: fn goal }
function fresh() -> void { function fresh(compass_st: mut CompassState, compasstest_st: mut CompasstestState) -> void {
compass_config(64) compass_config(compass_st, 64)
compass_config_radar([0.0, 0.0, 0.0, 120.0, 260.0], 2) compass_config_radar(compass_st, [0.0, 0.0, 0.0, 120.0, 260.0], 2)
vx = 0.0 compasstest_st.vx = 0.0
vz = 0.0 compasstest_st.vz = 0.0
yaw = 0.0 compasstest_st.yaw = 0.0
tier = 4 compasstest_st.tier = 4
objective = true compasstest_st.objective = true
} }
function labels() -> string { function labels(compass_st: CompassState) -> string {
var s = "" var s = ""
for i in 0 .. compass_count() { s = s + compass_at(i).label + ";" } for i in 0 .. compass_count(compass_st) { s = s + compass_at(compass_st, i).label + ";" }
return s return s
} }
test "every provider is asked, the tracked mark first, then the player's, then the rest in order" { test "every provider is asked, the tracked mark first, then the player's, then the rest in order" (compass_st: mut CompassState, compasstest_st: mut CompasstestState) {
fresh() fresh(compass_st, compasstest_st)
compass_gather() compass_gather(compass_st)
expect_eq(compass_count(), 5) expect_eq(compass_count(compass_st), 5)
expect(labels() == "the dock;my mark;camp;sign;legend;") expect(labels(compass_st) == "the dock;my mark;camp;sign;legend;")
expect_eq(compass_tracked(), 0) expect_eq(compass_tracked(compass_st), 0)
} }
test "a mark shows from its tier up, and without a compass nothing is gathered" { test "a mark shows from its tier up, and without a compass nothing is gathered" (compass_st: mut CompassState, compasstest_st: mut CompasstestState) {
fresh() fresh(compass_st, compasstest_st)
tier = 1 compasstest_st.tier = 1
compass_gather() compass_gather(compass_st)
expect(labels() == "my mark;camp;") expect(labels(compass_st) == "my mark;camp;")
expect_eq(compass_tracked(), -1) expect_eq(compass_tracked(compass_st), -1)
tier = 0 compasstest_st.tier = 0
compass_gather() compass_gather(compass_st)
expect_eq(compass_count(), 0) expect_eq(compass_count(compass_st), 0)
} }
test "past the cap the lowest priorities go" { test "past the cap the lowest priorities go" (compass_st: mut CompassState, compasstest_st: mut CompasstestState) {
fresh() fresh(compass_st, compasstest_st)
compass_config(2) compass_config(compass_st, 2)
compass_gather() compass_gather(compass_st)
expect(labels() == "the dock;my mark;") expect(labels(compass_st) == "the dock;my mark;")
} }
test "a bearing is from the viewer's facing, a distance from where it stands" { test "a bearing is from the viewer's facing, a distance from where it stands" (compass_st: mut CompassState, compasstest_st: mut CompasstestState) {
fresh() fresh(compass_st, compasstest_st)
compass_gather() compass_gather(compass_st)
expect_near(compass_bearing(2), 0.0, 0.001) expect_near(compass_bearing(compass_st, 2), 0.0, 0.001)
expect_near(compass_dist(2), 100.0, 0.01) expect_near(compass_dist(compass_st, 2), 100.0, 0.01)
expect_near(compass_bearing(3), 1.5708, 0.001) expect_near(compass_bearing(compass_st, 3), 1.5708, 0.001)
expect_near(compass_bearing(0), -1.5708, 0.001) expect_near(compass_bearing(compass_st, 0), -1.5708, 0.001)
yaw = 1.5708 compasstest_st.yaw = 1.5708
expect_near(compass_bearing(3), 0.0, 0.001) expect_near(compass_bearing(compass_st, 3), 0.0, 0.001)
expect_near(compass_bearing(2), -1.5708, 0.001) expect_near(compass_bearing(compass_st, 2), -1.5708, 0.001)
vx = -50.0 compasstest_st.vx = -50.0
vz = 100.0 compasstest_st.vz = 100.0
expect_near(compass_dist(3), 100.0, 0.01) expect_near(compass_dist(compass_st, 3), 100.0, 0.01)
expect_near(compass_wrap(7.0), 7.0 - 6.2832, 0.001) expect_near(compass_wrap(7.0), 7.0 - 6.2832, 0.001)
expect_near(compass_strip_t(0.5, 1.0), 0.5, 0.001) expect_near(compass_strip_t(0.5, 1.0), 0.5, 0.001)
} }
test "a capture hears every mark whatever the tier and leaves the frame's list alone" { test "a capture hears every mark whatever the tier and leaves the frame's list alone" (compass_st: mut CompassState, compasstest_st: mut CompasstestState) {
fresh() fresh(compass_st, compasstest_st)
compass_gather() compass_gather(compass_st)
tier = 1 compasstest_st.tier = 1
compass_capture_begin() compass_capture_begin(compass_st)
expect(compass_capturing()) expect(compass_capturing(compass_st))
goal() goal(compass_st, compasstest_st)
places() places(compass_st)
let got = compass_capture_end() let got = compass_capture_end(compass_st)
expect(not compass_capturing()) expect(not compass_capturing(compass_st))
expect_eq(len(got), 5) expect_eq(len(got), 5)
expect(got[0].label == "the dock") expect(got[0].label == "the dock")
expect_eq(compass_count(), 5) expect_eq(compass_count(compass_st), 5)
} }
test "the radar: its range by tier, the blips in it, and where each falls on the disc" { test "the radar: its range by tier, the blips in it, and where each falls on the disc" (compass_st: mut CompassState, compasstest_st: mut CompasstestState) {
fresh() fresh(compass_st, compasstest_st)
tier = 2 compasstest_st.tier = 2
compass_gather() compass_gather(compass_st)
expect(not compass_radar_on()) expect(not compass_radar_on(compass_st))
expect(not compass_radar_has(0)) expect(not compass_radar_has(compass_st, 0))
tier = 3 compasstest_st.tier = 3
compass_gather() compass_gather(compass_st)
expect(compass_radar_on()) expect(compass_radar_on(compass_st))
expect_near(compass_radar_range(), 120.0, 0.01) expect_near(compass_radar_range(compass_st), 120.0, 0.01)
expect(compass_radar_has(0)) expect(compass_radar_has(compass_st, 0))
expect(not compass_radar_has(1)) expect(not compass_radar_has(compass_st, 1))
expect(not compass_radar_has(2)) expect(not compass_radar_has(compass_st, 2))
expect(compass_radar_has(3)) expect(compass_radar_has(compass_st, 3))
expect_near(compass_radar_x(3), -50.0 / 120.0, 0.001) expect_near(compass_radar_x(compass_st, 3), -50.0 / 120.0, 0.001)
expect_near(compass_radar_y(3), 0.0, 0.001) expect_near(compass_radar_y(compass_st, 3), 0.0, 0.001)
expect_near(compass_radar_x(0), 80.0 / 120.0, 0.001) expect_near(compass_radar_x(compass_st, 0), 80.0 / 120.0, 0.001)
tier = 9 compasstest_st.tier = 9
expect_near(compass_radar_range(), 260.0, 0.01) expect_near(compass_radar_range(compass_st), 260.0, 0.01)
} }
test "the eight points go round from the heading 0 toward -x" { test "the eight points go round from the heading 0 toward -x" {

View file

@ -1,42 +1,40 @@
# hold.ludic - a recipe made by holding a button for its `hold` seconds: letting go wastes # hold.ludic - a recipe made by holding a button for its `hold` seconds: letting go wastes
# nothing, and it stops by itself the moment it can no longer be made # nothing, and it stops by itself the moment it can no longer be made
var craft_hold_i: int = -1
var craft_hold_t: float = 0.0
export function craft_holds(i: int) -> bool { return craft_ok(i) and CraftRecipes[i].hold > 0.0 } export function craft_holds(i: int) -> bool { return craft_ok(i) and CraftRecipes[i].hold > 0.0 }
# the recipe being held, or -1 # the recipe being held, or -1
export function craft_holding() -> int { return craft_hold_i } export function craft_holding(crafting_st: CraftingState) -> int { return crafting_st.craft_hold_i }
# starts holding one that is made by holding and can be made now # starts holding one that is made by holding and can be made now
export function craft_hold_start(i: int) -> bool { export function craft_hold_start(crafting_st: mut CraftingState, i: int) -> bool {
if not craft_holds(i) or not craft_can(i) { return false } if not craft_holds(i) or not craft_can(i) { return false }
craft_hold_i = i crafting_st.craft_hold_i = i
craft_hold_t = 0.0 crafting_st.craft_hold_t = 0.0
return true return true
} }
export function craft_hold_stop() -> void { export function craft_hold_stop(crafting_st: mut CraftingState) -> void {
craft_hold_i = -1 crafting_st.craft_hold_i = -1
craft_hold_t = 0.0 crafting_st.craft_hold_t = 0.0
} }
# how far through the hold on `i` is, 0 to 1 # how far through the hold on `i` is, 0 to 1
export function craft_hold_frac(i: int) -> float { export function craft_hold_frac(crafting_st: CraftingState, i: int) -> float {
if craft_hold_i != i or not craft_holds(i) { return 0.0 } if crafting_st.craft_hold_i != i or not craft_holds(i) { return 0.0 }
return Math.clamp(craft_hold_t / CraftRecipes[i].hold, 0.0, 1.0) return Math.clamp(crafting_st.craft_hold_t / CraftRecipes[i].hold, 0.0, 1.0)
} }
# `dt` real seconds of holding; true on the frame it is made # `dt` real seconds of holding; true on the frame it is made
export function craft_hold_run(dt: float) -> bool { export function craft_hold_run(base_st: mut BaseState, crafting_st: mut CraftingState, dt: float) -> bool {
if craft_hold_i < 0 { return false } if crafting_st.craft_hold_i < 0 { return false }
if not craft_can(craft_hold_i) { if not craft_can(crafting_st.craft_hold_i) {
craft_hold_stop() craft_hold_stop(crafting_st)
return false return false
} }
craft_hold_t = craft_hold_t + dt crafting_st.craft_hold_t = crafting_st.craft_hold_t + dt
if craft_hold_t <= CraftRecipes[craft_hold_i].hold { return false } if crafting_st.craft_hold_t <= CraftRecipes[crafting_st.craft_hold_i].hold { return false }
let i = craft_hold_i let i = crafting_st.craft_hold_i
craft_hold_stop() craft_hold_stop(crafting_st)
return craft_make(i) return craft_make(base_st, crafting_st, i)
} }

View file

@ -1,8 +1,8 @@
# system.ludic - crafting as a system: a reset, and nothing saved (a hold never outlives its # system.ludic - crafting as a system: a reset, and nothing saved (a hold never outlives its
# screen; what a station holds is the station's) # screen; what a station holds is the station's)
export function craft_reset() -> void { export function craft_reset(base_st: mut BaseState, crafting_st: mut CraftingState) -> void {
craft_hold_stop() craft_hold_stop(crafting_st)
q_clear(craft_facts()) q_clear(base_st, craft_facts(base_st, crafting_st))
} }
export function craft_system() -> System { export function craft_system() -> System {

View file

@ -21,49 +21,51 @@ program CraftingTest {
def CraftRecipes chair { out: TORCH, ins: [WOOD], ns: [3], station: BENCH } def CraftRecipes chair { out: TORCH, ins: [WOOD], ns: [3], station: BENCH }
def CraftRecipes secret { out: TORCH, ins: [WOOD], ns: [1], learned: false } def CraftRecipes secret { out: TORCH, ins: [WOOD], ns: [1], learned: false }
var pack: []int = null state CraftingtestState {
var bench_near: bool = true pack: []int = null
var pot_takes: bool = true bench_near: bool = true
var pot_got: int = -1 pot_takes: bool = true
pot_got: int = -1
}
function count(it: int) -> int { return pack[it] } function count(craftingtest_st: CraftingtestState, it: int) -> int { return craftingtest_st.pack[it] }
function take(it: int, n: int) -> bool { function take(craftingtest_st: mut CraftingtestState, it: int, n: int) -> bool {
if pack[it] < n { return false } if craftingtest_st.pack[it] < n { return false }
pack[it] = pack[it] - n craftingtest_st.pack[it] = craftingtest_st.pack[it] - n
return true return true
} }
function add(it: int, n: int) -> int { function add(craftingtest_st: mut CraftingtestState, it: int, n: int) -> int {
pack[it] = pack[it] + n craftingtest_st.pack[it] = craftingtest_st.pack[it] + n
return n return n
} }
function room(it: int) -> int { function room(craftingtest_st: CraftingtestState, it: int) -> int {
if it == TORCH { return 3 - pack[it] } if it == TORCH { return 3 - craftingtest_st.pack[it] }
return 99 return 99
} }
function leaves(it: int) -> int { function leaves(it: int) -> int {
if it == WATER { return BOTTLE } if it == WATER { return BOTTLE }
return -1 return -1
} }
function ready(st: int) -> bool { function ready(craftingtest_st: CraftingtestState, st: int) -> bool {
if st == BENCH { return bench_near } if st == BENCH { return craftingtest_st.bench_near }
return true return true
} }
function start(st: int, recipe: int) -> bool { function start(craftingtest_st: mut CraftingtestState, st: int, recipe: int) -> bool {
if not pot_takes { return false } if not craftingtest_st.pot_takes { return false }
pot_got = recipe craftingtest_st.pot_got = recipe
return true return true
} }
bind CraftPack { count: fn count, take: fn take, add: fn add, room: fn room, leaves: fn leaves } bind CraftPack { count: fn count, take: fn take, add: fn add, room: fn room, leaves: fn leaves }
bind CraftStations { ready: fn ready, start: fn start } bind CraftStations { ready: fn ready, start: fn start }
function fresh() -> void { function fresh(base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) -> void {
pack = new []int craftingtest_st.pack = new []int
for i in 0 .. 8 { push(pack, 0) } for i in 0 .. 8 { push(craftingtest_st.pack, 0) }
craft_reset() craft_reset(base_st, crafting_st)
} }
function facts() -> []Crafted { return q_drain(craft_facts()) } function facts(base_st: mut BaseState, crafting_st: mut CraftingState) -> []Crafted { return q_drain(base_st, craft_facts(base_st, crafting_st)) }
test "the registry is the game's, in the order written" { test "the registry is the game's, in the order written" {
expect_eq(CRAFT_COUNT, 5) expect_eq(CRAFT_COUNT, 5)
@ -74,105 +76,105 @@ program CraftingTest {
expect_eq(craft_find(99), -1) expect_eq(craft_find(99), -1)
} }
test "made at once from the makings, and said" { test "made at once from the makings, and said" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) {
fresh() fresh(base_st, crafting_st, craftingtest_st)
expect(not craft_can(CRAFT_TORCH)) expect(not craft_can(CRAFT_TORCH))
pack[WOOD] = 2 craftingtest_st.pack[WOOD] = 2
pack[CLOTH] = 1 craftingtest_st.pack[CLOTH] = 1
expect(craft_make(CRAFT_TORCH)) expect(craft_make(base_st, crafting_st, CRAFT_TORCH))
expect_eq(pack[WOOD], 1) expect_eq(craftingtest_st.pack[WOOD], 1)
expect_eq(pack[CLOTH], 0) expect_eq(craftingtest_st.pack[CLOTH], 0)
expect_eq(pack[TORCH], 1) expect_eq(craftingtest_st.pack[TORCH], 1)
let fs = facts() let fs = facts(base_st, crafting_st)
expect_eq(len(fs), 1) expect_eq(len(fs), 1)
expect_eq(fs[0].item, TORCH) expect_eq(fs[0].item, TORCH)
expect(not fs[0].queued) expect(not fs[0].queued)
} }
test "nothing is taken when it cannot be made" { test "nothing is taken when it cannot be made" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) {
fresh() fresh(base_st, crafting_st, craftingtest_st)
pack[WOOD] = 1 craftingtest_st.pack[WOOD] = 1
expect(not craft_make(CRAFT_TORCH)) expect(not craft_make(base_st, crafting_st, CRAFT_TORCH))
expect_eq(pack[WOOD], 1) expect_eq(craftingtest_st.pack[WOOD], 1)
expect_eq(len(facts()), 0) expect_eq(len(facts(base_st, crafting_st)), 0)
} }
test "the station decides, and so does the room for what it makes" { test "the station decides, and so does the room for what it makes" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) {
fresh() fresh(base_st, crafting_st, craftingtest_st)
pack[WOOD] = 9 craftingtest_st.pack[WOOD] = 9
bench_near = false craftingtest_st.bench_near = false
expect(craft_has(CRAFT_CHAIR)) expect(craft_has(CRAFT_CHAIR))
expect(not craft_can(CRAFT_CHAIR)) expect(not craft_can(CRAFT_CHAIR))
bench_near = true craftingtest_st.bench_near = true
expect(craft_can(CRAFT_CHAIR)) expect(craft_can(CRAFT_CHAIR))
pack[TORCH] = 3 craftingtest_st.pack[TORCH] = 3
expect(not craft_can(CRAFT_CHAIR)) expect(not craft_can(CRAFT_CHAIR))
} }
test "an unlearned recipe is never made" { test "an unlearned recipe is never made" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) {
fresh() fresh(base_st, crafting_st, craftingtest_st)
pack[WOOD] = 5 craftingtest_st.pack[WOOD] = 5
expect(not craft_can(CRAFT_SECRET)) expect(not craft_can(CRAFT_SECRET))
} }
test "a timed recipe goes into its station, and water leaves its bottle" { test "a timed recipe goes into its station, and water leaves its bottle" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) {
fresh() fresh(base_st, crafting_st, craftingtest_st)
pack[HERB] = 2 craftingtest_st.pack[HERB] = 2
pack[WATER] = 1 craftingtest_st.pack[WATER] = 1
expect(craft_make(CRAFT_TEA)) expect(craft_make(base_st, crafting_st, CRAFT_TEA))
expect_eq(pot_got, CRAFT_TEA) expect_eq(craftingtest_st.pot_got, CRAFT_TEA)
expect_eq(pack[TEA], 0) expect_eq(craftingtest_st.pack[TEA], 0)
expect_eq(pack[WATER], 0) expect_eq(craftingtest_st.pack[WATER], 0)
expect_eq(pack[BOTTLE], 1) expect_eq(craftingtest_st.pack[BOTTLE], 1)
let fs = facts() let fs = facts(base_st, crafting_st)
expect(fs[0].queued) expect(fs[0].queued)
expect_eq(fs[0].station, POT) expect_eq(fs[0].station, POT)
} }
test "a station that says no gives everything back" { test "a station that says no gives everything back" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) {
fresh() fresh(base_st, crafting_st, craftingtest_st)
pot_takes = false craftingtest_st.pot_takes = false
pack[HERB] = 2 craftingtest_st.pack[HERB] = 2
pack[WATER] = 1 craftingtest_st.pack[WATER] = 1
expect(not craft_make(CRAFT_TEA)) expect(not craft_make(base_st, crafting_st, CRAFT_TEA))
expect_eq(pack[HERB], 2) expect_eq(craftingtest_st.pack[HERB], 2)
expect_eq(pack[WATER], 1) expect_eq(craftingtest_st.pack[WATER], 1)
expect_eq(pack[BOTTLE], 0) expect_eq(craftingtest_st.pack[BOTTLE], 0)
expect_eq(len(facts()), 0) expect_eq(len(facts(base_st, crafting_st)), 0)
} }
test "a hold makes it after its seconds, and not before" { test "a hold makes it after its seconds, and not before" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) {
fresh() fresh(base_st, crafting_st, craftingtest_st)
pack[HERB] = 4 craftingtest_st.pack[HERB] = 4
expect(craft_holds(CRAFT_ROPE)) expect(craft_holds(CRAFT_ROPE))
expect(not craft_holds(CRAFT_TORCH)) expect(not craft_holds(CRAFT_TORCH))
expect(craft_hold_start(CRAFT_ROPE)) expect(craft_hold_start(crafting_st, CRAFT_ROPE))
expect(not craft_hold_run(1.0)) expect(not craft_hold_run(base_st, crafting_st, 1.0))
expect(craft_hold_frac(CRAFT_ROPE) == 0.5) expect(craft_hold_frac(crafting_st, CRAFT_ROPE) == 0.5)
expect(not craft_hold_run(0.9)) expect(not craft_hold_run(base_st, crafting_st, 0.9))
expect(craft_hold_run(0.2)) expect(craft_hold_run(base_st, crafting_st, 0.2))
expect_eq(pack[ROPE], 2) expect_eq(craftingtest_st.pack[ROPE], 2)
expect_eq(craft_holding(), -1) expect_eq(craft_holding(crafting_st), -1)
} }
test "letting go wastes nothing, and losing the makings stops the hold" { test "letting go wastes nothing, and losing the makings stops the hold" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) {
fresh() fresh(base_st, crafting_st, craftingtest_st)
pack[HERB] = 4 craftingtest_st.pack[HERB] = 4
craft_hold_start(CRAFT_ROPE) craft_hold_start(crafting_st, CRAFT_ROPE)
craft_hold_run(1.5) craft_hold_run(base_st, crafting_st, 1.5)
craft_hold_stop() craft_hold_stop(crafting_st)
expect_eq(pack[HERB], 4) expect_eq(craftingtest_st.pack[HERB], 4)
craft_hold_start(CRAFT_ROPE) craft_hold_start(crafting_st, CRAFT_ROPE)
pack[HERB] = 3 craftingtest_st.pack[HERB] = 3
expect(not craft_hold_run(5.0)) expect(not craft_hold_run(base_st, crafting_st, 5.0))
expect_eq(craft_holding(), -1) expect_eq(craft_holding(crafting_st), -1)
expect_eq(pack[ROPE], 0) expect_eq(craftingtest_st.pack[ROPE], 0)
} }
test "a click recipe cannot be held" { test "a click recipe cannot be held" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) {
fresh() fresh(base_st, crafting_st, craftingtest_st)
pack[WOOD] = 1 craftingtest_st.pack[WOOD] = 1
pack[CLOTH] = 1 craftingtest_st.pack[CLOTH] = 1
expect(not craft_hold_start(CRAFT_TORCH)) expect(not craft_hold_start(crafting_st, CRAFT_TORCH))
} }
} }

View file

@ -1,9 +1,13 @@
# verbs.ludic - can it be made, taking and giving back its makings, and making it # verbs.ludic - can it be made, taking and giving back its makings, and making it
var craft_fact_q: Queue<Crafted> = null export state CraftingState {
craft_hold_i: int = -1
craft_hold_t: float = 0.0
craft_fact_q: Queue<Crafted> = null
}
export function craft_facts() -> Queue<Crafted> { export function craft_facts(base_st: mut BaseState, crafting_st: mut CraftingState) -> Queue<Crafted> {
if craft_fact_q == null { craft_fact_q = queue_new("crafting.crafted") } if crafting_st.craft_fact_q == null { crafting_st.craft_fact_q = queue_new(base_st, "crafting.crafted") }
return craft_fact_q return crafting_st.craft_fact_q
} }
# every making is in the pack # every making is in the pack
@ -46,7 +50,7 @@ export function craft_refund(i: int) -> void {
} }
} }
function craft_fact_push(i: int, queued: bool) -> void { function craft_fact_push(base_st: mut BaseState, crafting_st: mut CraftingState, i: int, queued: bool) -> void {
let r = CraftRecipes[i] let r = CraftRecipes[i]
let c = new Crafted let c = new Crafted
c.recipe = i c.recipe = i
@ -54,12 +58,12 @@ function craft_fact_push(i: int, queued: bool) -> void {
c.n = r.n c.n = r.n
c.station = r.station c.station = r.station
c.queued = queued c.queued = queued
q_push(craft_facts(), c) q_push(base_st, craft_facts(base_st, crafting_st), c)
} }
# make it: a timed recipe at a station goes into the station (given back if it will not take it), # make it: a timed recipe at a station goes into the station (given back if it will not take it),
# anything else is handed over at once. False, and the pack untouched, when it cannot be made. # anything else is handed over at once. False, and the pack untouched, when it cannot be made.
export function craft_make(i: int) -> bool { export function craft_make(base_st: mut BaseState, crafting_st: mut CraftingState, i: int) -> bool {
if not craft_can(i) { return false } if not craft_can(i) { return false }
let r = CraftRecipes[i] let r = CraftRecipes[i]
craft_take(i) craft_take(i)
@ -68,10 +72,10 @@ export function craft_make(i: int) -> bool {
craft_refund(i) craft_refund(i)
return false return false
} }
craft_fact_push(i, true) craft_fact_push(base_st, crafting_st, i, true)
return true return true
} }
CraftPack.add(r.out, r.n) CraftPack.add(r.out, r.n)
craft_fact_push(i, false) craft_fact_push(base_st, crafting_st, i, false)
return true return true
} }

View file

@ -2,32 +2,32 @@
function ef_live(e: Effect) -> bool { return e.delay < 0.01 } function ef_live(e: Effect) -> bool { return e.delay < 0.01 }
# what the rules should add for a kind right now (a delayed one counts only once it begins) # what the rules should add for a kind right now (a delayed one counts only once it begins)
export function effects_sum(kind: int) -> float { export function effects_sum(effects_st: mut EffectsState, kind: int) -> float {
let all = ef_all() let all = ef_all(effects_st)
var t = 0.0 var t = 0.0
for i in 0 .. len(all) { if all[i].kind == kind and ef_live(all[i]) { t = t + all[i].mag } } for i in 0 .. len(all) { if all[i].kind == kind and ef_live(all[i]) { t = t + all[i].mag } }
return t return t
} }
export function effects_has(kind: int) -> bool { export function effects_has(effects_st: mut EffectsState, kind: int) -> bool {
let all = ef_all() let all = ef_all(effects_st)
for i in 0 .. len(all) { if all[i].kind == kind and ef_live(all[i]) { return true } } for i in 0 .. len(all) { if all[i].kind == kind and ef_live(all[i]) { return true } }
return false return false
} }
export function effects_bonus_count() -> int { export function effects_bonus_count(effects_st: mut EffectsState) -> int {
let all = ef_all() let all = ef_all(effects_st)
var n = 0 var n = 0
for i in 0 .. len(all) { if all[i].bonus { n += 1 } } for i in 0 .. len(all) { if all[i].bonus { n += 1 } }
return n return n
} }
# everything held, delayed or not, in the order added # everything held, delayed or not, in the order added
export function effects_count() -> int { return len(ef_all()) } export function effects_count(effects_st: mut EffectsState) -> int { return len(ef_all(effects_st)) }
# a copy, so a reader cannot change the ring # a copy, so a reader cannot change the ring
export function effects_at(i: int) -> Effect { export function effects_at(effects_st: mut EffectsState, i: int) -> Effect {
let src = ef_all()[i] let src = ef_all(effects_st)[i]
let e = new Effect let e = new Effect
e.kind = src.kind e.kind = src.kind
e.mag = src.mag e.mag = src.mag
@ -39,9 +39,9 @@ export function effects_at(i: int) -> Effect {
} }
# the indices of the ones that have begun, for a read-out # the indices of the ones that have begun, for a read-out
export function effects_live() -> []int { export function effects_live(effects_st: mut EffectsState) -> []int {
let out = new []int let out = new []int
let all = ef_all() let all = ef_all(effects_st)
for i in 0 .. len(all) { if ef_live(all[i]) { push(out, i) } } for i in 0 .. len(all) { if ef_live(all[i]) { push(out, i) } }
return out return out
} }

View file

@ -20,27 +20,29 @@ export property EffectEnded {
bonus: bool = false bonus: bool = false
} }
var ef_list: []Effect = null export state EffectsState {
var ef_max: int = 8 ef_list: []Effect = null
var ef_bonus_max: int = 3 ef_max: int = 8
var ef_ended: Queue<EffectEnded> = null ef_bonus_max: int = 3
ef_ended: Queue<EffectEnded> = null
}
function ef_all() -> []Effect { function ef_all(effects_st: mut EffectsState) -> []Effect {
if ef_list == null { ef_list = new []Effect } if effects_st.ef_list == null { effects_st.ef_list = new []Effect }
return ef_list return effects_st.ef_list
} }
# the facts: what ended and why, oldest first; the game drains it # the facts: what ended and why, oldest first; the game drains it
export function effects_ended() -> Queue<EffectEnded> { export function effects_ended(base_st: mut BaseState, effects_st: mut EffectsState) -> Queue<EffectEnded> {
if ef_ended == null { ef_ended = queue_new("effects.ended") } if effects_st.ef_ended == null { effects_st.ef_ended = queue_new(base_st, "effects.ended") }
return ef_ended return effects_st.ef_ended
} }
# how many may run at once, and how many of those may be bonuses # how many may run at once, and how many of those may be bonuses
export function effects_config(max: int, bonus_max: int) -> void { export function effects_config(effects_st: mut EffectsState, max: int, bonus_max: int) -> void {
ef_max = max effects_st.ef_max = max
ef_bonus_max = bonus_max effects_st.ef_bonus_max = bonus_max
} }
export function effects_capacity() -> int { return ef_max } export function effects_capacity(effects_st: EffectsState) -> int { return effects_st.ef_max }
export function effects_bonus_capacity() -> int { return ef_bonus_max } export function effects_bonus_capacity(effects_st: EffectsState) -> int { return effects_st.ef_bonus_max }

View file

@ -1,14 +1,14 @@
# system.ludic - the effects as a system: reset, a tick in game hours, and its own save section # system.ludic - the effects as a system: reset, a tick in game hours, and its own save section
export function effects_reset() -> void { export function effects_reset(base_st: mut BaseState, effects_st: mut EffectsState) -> void {
effects_clear() effects_clear(effects_st)
q_clear(effects_ended()) q_clear(base_st, effects_ended(base_st, effects_st))
} }
function effects_tick(t: Tick) -> void { effects_run(t.hours * 60.0) } function effects_tick(base_st: mut BaseState, effects_st: mut EffectsState, t: Tick) -> void { effects_run(base_st, effects_st, t.hours * 60.0) }
export function effects_save() -> Val { export function effects_save(effects_st: mut EffectsState) -> Val {
let l = Value.list() let l = Value.list()
let all = ef_all() let all = ef_all(effects_st)
for i in 0 .. len(all) { for i in 0 .. len(all) {
let o = Value.object() let o = Value.object()
sv_put_int(o, "k", all[i].kind) sv_put_int(o, "k", all[i].kind)
@ -24,12 +24,12 @@ export function effects_save() -> Val {
return v return v
} }
export function effects_load(v: Val, version: int) -> void { export function effects_load(effects_st: mut EffectsState, v: Val, version: int) -> void {
effects_clear() effects_clear(effects_st)
if Value.has(v, "list") == 0 { return } if Value.has(v, "list") == 0 { return }
let l = Value.get(v, "list") let l = Value.get(v, "list")
for i in 0 .. Value.count(l) { for i in 0 .. Value.count(l) {
if len(ef_all()) >= ef_max { break } if len(ef_all(effects_st)) >= effects_st.ef_max { break }
let o = Value.at(l, i) let o = Value.at(l, i)
let e = new Effect let e = new Effect
e.kind = sv_int(o, "k", 0) e.kind = sv_int(o, "k", 0)
@ -38,7 +38,7 @@ export function effects_load(v: Val, version: int) -> void {
e.delay = sv_float(o, "d", 0.0) e.delay = sv_float(o, "d", 0.0)
e.bonus = sv_bool(o, "b", false) e.bonus = sv_bool(o, "b", false)
e.label = sv_str(o, "l", "") e.label = sv_str(o, "l", "")
push(ef_all(), e) push(ef_all(effects_st), e)
} }
} }

View file

@ -8,120 +8,120 @@ program EffectsTest {
const WARMTH: int = 1 const WARMTH: int = 1
const CARRY: int = 2 const CARRY: int = 2
function fresh() -> void { function fresh(base_st: mut BaseState, effects_st: mut EffectsState) -> void {
effects_config(8, 3) effects_config(effects_st, 8, 3)
effects_reset() effects_reset(base_st, effects_st)
} }
function ended() -> []EffectEnded { return q_drain(effects_ended()) } function ended(base_st: mut BaseState, effects_st: mut EffectsState) -> []EffectEnded { return q_drain(base_st, effects_ended(base_st, effects_st)) }
function waiting() -> int { return q_len(effects_ended()) } function waiting(base_st: mut BaseState, effects_st: mut EffectsState) -> int { return q_len(effects_ended(base_st, effects_st)) }
function tick_minutes(m: float) -> void { function tick_minutes(m: float) -> void {
let s = effects_system() let s = effects_system()
s.tick(tick_new(0.016, 0, m / 60.0)) s.tick(tick_new(0.016, 0, m / 60.0))
} }
test "the sum is per kind, and a kind with nothing running is zero" { test "the sum is per kind, and a kind with nothing running is zero" (base_st: mut BaseState, effects_st: mut EffectsState) {
fresh() fresh(base_st, effects_st)
effects_add(LEGS, 8.0, 20.0, false, "Fizz") effects_add(base_st, effects_st, LEGS, 8.0, 20.0, false, "Fizz")
effects_add(LEGS, 6.0, 45.0, false, "Trail mix") effects_add(base_st, effects_st, LEGS, 6.0, 45.0, false, "Trail mix")
effects_add(WARMTH, 12.0, 50.0, false, "Stew") effects_add(base_st, effects_st, WARMTH, 12.0, 50.0, false, "Stew")
expect(effects_sum(LEGS) == 14.0) expect(effects_sum(effects_st, LEGS) == 14.0)
expect(effects_sum(WARMTH) == 12.0) expect(effects_sum(effects_st, WARMTH) == 12.0)
expect(effects_sum(CARRY) == 0.0) expect(effects_sum(effects_st, CARRY) == 0.0)
expect(effects_has(WARMTH)) expect(effects_has(effects_st, WARMTH))
expect(not effects_has(CARRY)) expect(not effects_has(effects_st, CARRY))
} }
test "the same kind from the same label replaces itself rather than stacking" { test "the same kind from the same label replaces itself rather than stacking" (base_st: mut BaseState, effects_st: mut EffectsState) {
fresh() fresh(base_st, effects_st)
effects_add(LEGS, 8.0, 20.0, false, "Fizz") effects_add(base_st, effects_st, LEGS, 8.0, 20.0, false, "Fizz")
effects_add(LEGS, 8.0, 20.0, false, "Fizz") effects_add(base_st, effects_st, LEGS, 8.0, 20.0, false, "Fizz")
expect_eq(effects_count(), 1) expect_eq(effects_count(effects_st), 1)
expect(effects_sum(LEGS) == 8.0) expect(effects_sum(effects_st, LEGS) == 8.0)
} }
test "a fourth bonus pushes the oldest out and says so" { test "a fourth bonus pushes the oldest out and says so" (base_st: mut BaseState, effects_st: mut EffectsState) {
fresh() fresh(base_st, effects_st)
effects_add(LEGS, 1.0, 10.0, true, "a") effects_add(base_st, effects_st, LEGS, 1.0, 10.0, true, "a")
effects_add(LEGS, 1.0, 30.0, true, "b") effects_add(base_st, effects_st, LEGS, 1.0, 30.0, true, "b")
effects_add(WARMTH, 1.0, 40.0, true, "c") effects_add(base_st, effects_st, WARMTH, 1.0, 40.0, true, "c")
effects_add(CARRY, 1.0, 50.0, true, "d") effects_add(base_st, effects_st, CARRY, 1.0, 50.0, true, "d")
expect_eq(effects_bonus_count(), 3) expect_eq(effects_bonus_count(effects_st), 3)
let gone = ended() let gone = ended(base_st, effects_st)
expect_eq(len(gone), 1) expect_eq(len(gone), 1)
expect(gone[0].label == "a") expect(gone[0].label == "a")
expect_eq(gone[0].why, EFFECT_PUSHED_OUT) expect_eq(gone[0].why, EFFECT_PUSHED_OUT)
} }
test "a full ring drops the one nearest its end" { test "a full ring drops the one nearest its end" (base_st: mut BaseState, effects_st: mut EffectsState) {
fresh() fresh(base_st, effects_st)
effects_config(2, 3) effects_config(effects_st, 2, 3)
effects_add(LEGS, 1.0, 10.0, false, "short") effects_add(base_st, effects_st, LEGS, 1.0, 10.0, false, "short")
effects_add(WARMTH, 1.0, 90.0, false, "long") effects_add(base_st, effects_st, WARMTH, 1.0, 90.0, false, "long")
effects_add(CARRY, 1.0, 50.0, false, "new") effects_add(base_st, effects_st, CARRY, 1.0, 50.0, false, "new")
expect_eq(effects_count(), 2) expect_eq(effects_count(effects_st), 2)
expect(not effects_has(LEGS)) expect(not effects_has(effects_st, LEGS))
expect_eq(ended()[0].why, EFFECT_CROWDED_OUT) expect_eq(ended(base_st, effects_st)[0].why, EFFECT_CROWDED_OUT)
} }
test "a delayed one counts only once it has begun" { test "a delayed one counts only once it has begun" (base_st: mut BaseState, effects_st: mut EffectsState) {
fresh() fresh(base_st, effects_st)
effects_add(LEGS, 8.0, 20.0, false, "Fizz") effects_add(base_st, effects_st, LEGS, 8.0, 20.0, false, "Fizz")
effects_add_after(20.0, LEGS, -5.0, 15.0, false, "Sugar crash") effects_add_after(base_st, effects_st, 20.0, LEGS, -5.0, 15.0, false, "Sugar crash")
expect(effects_sum(LEGS) == 8.0) expect(effects_sum(effects_st, LEGS) == 8.0)
expect_eq(len(effects_live()), 1) expect_eq(len(effects_live(effects_st)), 1)
tick_minutes(25.0) tick_minutes(25.0)
expect(effects_sum(LEGS) == -5.0) expect(effects_sum(effects_st, LEGS) == -5.0)
tick_minutes(20.0) tick_minutes(20.0)
expect_eq(effects_count(), 0) expect_eq(effects_count(effects_st), 0)
} }
test "running down in game hours reports each ending" { test "running down in game hours reports each ending" (base_st: mut BaseState, effects_st: mut EffectsState) {
fresh() fresh(base_st, effects_st)
effects_add(WARMTH, -10.0, 10.0, false, "Wet through") effects_add(base_st, effects_st, WARMTH, -10.0, 10.0, false, "Wet through")
tick_minutes(5.0) tick_minutes(5.0)
expect_eq(waiting(), 0) expect_eq(waiting(base_st, effects_st), 0)
tick_minutes(6.0) tick_minutes(6.0)
expect_eq(effects_count(), 0) expect_eq(effects_count(effects_st), 0)
let gone = ended() let gone = ended(base_st, effects_st)
expect_eq(gone[0].why, EFFECT_RAN_OUT) expect_eq(gone[0].why, EFFECT_RAN_OUT)
expect(gone[0].label == "Wet through") expect(gone[0].label == "Wet through")
} }
test "clear stops everything and says nothing" { test "clear stops everything and says nothing" (base_st: mut BaseState, effects_st: mut EffectsState) {
fresh() fresh(base_st, effects_st)
effects_add(LEGS, 1.0, 10.0, false, "a") effects_add(base_st, effects_st, LEGS, 1.0, 10.0, false, "a")
effects_clear() effects_clear(effects_st)
expect_eq(effects_count(), 0) expect_eq(effects_count(effects_st), 0)
expect_eq(waiting(), 0) expect_eq(waiting(base_st, effects_st), 0)
} }
test "a save section reads back the same ring" { test "a save section reads back the same ring" (base_st: mut BaseState, effects_st: mut EffectsState) {
fresh() fresh(base_st, effects_st)
effects_add(LEGS, 8.5, 20.0, true, "Fizz") effects_add(base_st, effects_st, LEGS, 8.5, 20.0, true, "Fizz")
effects_add_after(20.0, LEGS, -5.0, 15.0, false, "Sugar crash") effects_add_after(base_st, effects_st, 20.0, LEGS, -5.0, 15.0, false, "Sugar crash")
let root = save_tree() let root = save_tree()
save_section(root, "effects", 1, effects_save()) save_section(root, "effects", 1, effects_save(effects_st))
let text = save_encode(root) let text = save_encode(root)
effects_clear() effects_clear(effects_st)
let n = load_section(save_decode(text), "effects") let n = load_section(save_decode(text), "effects")
expect(n.found) expect(n.found)
effects_load(n.data, n.version) effects_load(effects_st, n.data, n.version)
expect_eq(effects_count(), 2) expect_eq(effects_count(effects_st), 2)
let a = effects_at(0) let a = effects_at(effects_st, 0)
expect(a.mag == 8.5) expect(a.mag == 8.5)
expect(a.bonus) expect(a.bonus)
let b = effects_at(1) let b = effects_at(effects_st, 1)
expect(b.delay == 20.0) expect(b.delay == 20.0)
expect(b.label == "Sugar crash") expect(b.label == "Sugar crash")
} }
test "the copy a reader gets does not change the ring" { test "the copy a reader gets does not change the ring" (base_st: mut BaseState, effects_st: mut EffectsState) {
fresh() fresh(base_st, effects_st)
effects_add(LEGS, 3.0, 10.0, false, "a") effects_add(base_st, effects_st, LEGS, 3.0, 10.0, false, "a")
let e = effects_at(0) let e = effects_at(effects_st, 0)
e.mag = 100.0 e.mag = 100.0
expect(effects_sum(LEGS) == 3.0) expect(effects_sum(effects_st, LEGS) == 3.0)
} }
} }

View file

@ -1,26 +1,26 @@
# verbs.ludic - the only way the ring changes. The same kind from the same label replaces # verbs.ludic - the only way the ring changes. The same kind from the same label replaces
# itself; a bonus past the cap pushes out the oldest bonus; a full ring drops the nearest end. # itself; a bonus past the cap pushes out the oldest bonus; a full ring drops the nearest end.
function ef_end(i: int, why: int) -> void { function ef_end(base_st: mut BaseState, effects_st: mut EffectsState, i: int, why: int) -> void {
let all = ef_all() let all = ef_all(effects_st)
let e = new EffectEnded let e = new EffectEnded
e.kind = all[i].kind e.kind = all[i].kind
e.label = all[i].label e.label = all[i].label
e.why = why e.why = why
e.bonus = all[i].bonus e.bonus = all[i].bonus
q_push(effects_ended(), e) q_push(base_st, effects_ended(base_st, effects_st), e)
ef_drop(i) ef_drop(effects_st, i)
} }
function ef_drop(i: int) -> void { function ef_drop(effects_st: mut EffectsState, i: int) -> void {
let all = ef_all() let all = ef_all(effects_st)
let rest = new []Effect let rest = new []Effect
for k in 0 .. len(all) { if k != i { push(rest, all[k]) } } for k in 0 .. len(all) { if k != i { push(rest, all[k]) } }
ef_list = rest effects_st.ef_list = rest
} }
# the one with the least time left (of the bonuses only, when asked); -1 when there is none # the one with the least time left (of the bonuses only, when asked); -1 when there is none
function ef_nearest_end(bonus_only: bool) -> int { function ef_nearest_end(effects_st: mut EffectsState, bonus_only: bool) -> int {
let all = ef_all() let all = ef_all(effects_st)
var at = -1 var at = -1
for i in 0 .. len(all) { for i in 0 .. len(all) {
if bonus_only and not all[i].bonus { continue } if bonus_only and not all[i].bonus { continue }
@ -29,16 +29,16 @@ function ef_nearest_end(bonus_only: bool) -> int {
return at return at
} }
function ef_make_room(bonus: bool) -> void { function ef_make_room(base_st: mut BaseState, effects_st: mut EffectsState, bonus: bool) -> void {
if bonus { if bonus {
while effects_bonus_count() >= ef_bonus_max and ef_nearest_end(true) >= 0 { ef_end(ef_nearest_end(true), EFFECT_PUSHED_OUT) } while effects_bonus_count(effects_st) >= effects_st.ef_bonus_max and ef_nearest_end(effects_st, true) >= 0 { ef_end(base_st, effects_st, ef_nearest_end(effects_st, true), EFFECT_PUSHED_OUT) }
} }
if len(ef_all()) >= ef_max and ef_nearest_end(false) >= 0 { ef_end(ef_nearest_end(false), EFFECT_CROWDED_OUT) } if len(ef_all(effects_st)) >= effects_st.ef_max and ef_nearest_end(effects_st, false) >= 0 { ef_end(base_st, effects_st, ef_nearest_end(effects_st, false), EFFECT_CROWDED_OUT) }
} }
# begin one `after` game minutes from now, lasting `minutes` once it has begun # begin one `after` game minutes from now, lasting `minutes` once it has begun
export function effects_add_after(after: float, kind: int, mag: float, minutes: float, bonus: bool, label: string) -> void { export function effects_add_after(base_st: mut BaseState, effects_st: mut EffectsState, after: float, kind: int, mag: float, minutes: float, bonus: bool, label: string) -> void {
let all = ef_all() let all = ef_all(effects_st)
for i in 0 .. len(all) { for i in 0 .. len(all) {
if all[i].kind == kind and all[i].label == label { if all[i].kind == kind and all[i].label == label {
all[i].mag = mag all[i].mag = mag
@ -46,7 +46,7 @@ export function effects_add_after(after: float, kind: int, mag: float, minutes:
return return
} }
} }
ef_make_room(bonus) ef_make_room(base_st, effects_st, bonus)
let e = new Effect let e = new Effect
e.kind = kind e.kind = kind
e.mag = mag e.mag = mag
@ -54,27 +54,27 @@ export function effects_add_after(after: float, kind: int, mag: float, minutes:
e.delay = after e.delay = after
e.bonus = bonus e.bonus = bonus
e.label = label e.label = label
push(ef_all(), e) push(ef_all(effects_st), e)
} }
export function effects_add(kind: int, mag: float, minutes: float, bonus: bool, label: string) -> void { export function effects_add(base_st: mut BaseState, effects_st: mut EffectsState, kind: int, mag: float, minutes: float, bonus: bool, label: string) -> void {
effects_add_after(0.0, kind, mag, minutes, bonus, label) effects_add_after(base_st, effects_st, 0.0, kind, mag, minutes, bonus, label)
} }
# everything stops at once, and says nothing: a new trip or a night's sleep # everything stops at once, and says nothing: a new trip or a night's sleep
export function effects_clear() -> void { ef_list = new []Effect } export function effects_clear(effects_st: mut EffectsState) -> void { effects_st.ef_list = new []Effect }
# run them down by `minutes` of game time; a delayed one waits first # run them down by `minutes` of game time; a delayed one waits first
export function effects_run(minutes: float) -> void { export function effects_run(base_st: mut BaseState, effects_st: mut EffectsState, minutes: float) -> void {
var i = 0 var i = 0
while i < len(ef_all()) { while i < len(ef_all(effects_st)) {
let e = ef_all()[i] let e = ef_all(effects_st)[i]
if e.delay > 0.0 { if e.delay > 0.0 {
e.delay = e.delay - minutes e.delay = e.delay - minutes
i += 1 i += 1
continue continue
} }
e.left = e.left - minutes e.left = e.left - minutes
if e.left < 0.0 { ef_end(i, EFFECT_RAN_OUT) } else { i += 1 } if e.left < 0.0 { ef_end(base_st, effects_st, i, EFFECT_RAN_OUT) } else { i += 1 }
} }
} }

View file

@ -1,31 +1,31 @@
# decide.ludic - lighting and feeding in three steps, so a machine that does not own the fire can # decide.ludic - lighting and feeding in three steps, so a machine that does not own the fire can
# ask the one that does: what it would come to (fire_decide), what it costs in wood # ask the one that does: what it would come to (fire_decide), what it costs in wood
# (fire_cost), and doing it (fire_apply - only ever on the machine that owns the world) # (fire_cost), and doing it (fire_apply - only ever on the machine that owns the world)
export function fire_decide(wood: int) -> int { export function fire_decide(fire_st: FireState, wood: int) -> int {
if fr_lit { if fire_st.fr_lit {
if wood < 1 { return FIRE_NO_LOG } if wood < 1 { return FIRE_NO_LOG }
return FIRE_FEED return FIRE_FEED
} }
if not FireWorld.allowed() { return FIRE_BANNED } if not FireWorld.allowed() { return FIRE_BANNED }
if wood < fr_light_wood { return FIRE_NO_WOOD } if wood < fire_st.fr_light_wood { return FIRE_NO_WOOD }
if FireWorld.is_wet() and not FireWorld.sheltered() { return FIRE_SOAKED } if FireWorld.is_wet() and not FireWorld.sheltered() { return FIRE_SOAKED }
return FIRE_LIGHT return FIRE_LIGHT
} }
export function fire_cost(how: int) -> int { export function fire_cost(fire_st: FireState, how: int) -> int {
if how == FIRE_LIGHT { return fr_light_wood } if how == FIRE_LIGHT { return fire_st.fr_light_wood }
if how == FIRE_FEED { return 1 } if how == FIRE_FEED { return 1 }
return 0 return 0
} }
export function fire_apply(how: int) -> void { export function fire_apply(base_st: mut BaseState, fire_st: mut FireState, how: int) -> void {
if how == FIRE_FEED { fr_fuel = fr_fuel + fr_log_minutes } if how == FIRE_FEED { fire_st.fr_fuel = fire_st.fr_fuel + fire_st.fr_log_minutes }
if how == FIRE_LIGHT { if how == FIRE_LIGHT {
fr_lit = true fire_st.fr_lit = true
fr_fuel = fr_light_minutes fire_st.fr_fuel = fire_st.fr_light_minutes
fr_fact(FIRE_LIT) fr_fact(base_st, fire_st, FIRE_LIT)
} }
} }
# the logs it takes to light one # the logs it takes to light one
export function fire_light_wood() -> int { return fr_light_wood } export function fire_light_wood(fire_st: FireState) -> int { return fire_st.fr_light_wood }

View file

@ -1,16 +1,16 @@
# queries.ludic - what the world asks the fire # queries.ludic - what the world asks the fire
export function fire_lit() -> bool { return fr_lit } export function fire_lit(fire_st: FireState) -> bool { return fire_st.fr_lit }
# game minutes of fuel left # game minutes of fuel left
export function fire_fuel() -> float { return fr_fuel } export function fire_fuel(fire_st: FireState) -> float { return fire_st.fr_fuel }
export function fire_low() -> bool { return fr_lit and fr_fuel < fr_low_minutes } export function fire_low(fire_st: FireState) -> bool { return fire_st.fr_lit and fire_st.fr_fuel < fire_st.fr_low_minutes }
# game hours today it has burned in the rain with something over it # game hours today it has burned in the rain with something over it
export function fire_rain_hours() -> float { return fr_rain_h } export function fire_rain_hours(fire_st: FireState) -> float { return fire_st.fr_rain_h }
# does it warm a body `dist` metres from it? # does it warm a body `dist` metres from it?
export function fire_warms(dist: float) -> bool { return fr_lit and dist < fr_warm_radius } export function fire_warms(fire_st: FireState, dist: float) -> bool { return fire_st.fr_lit and dist < fire_st.fr_warm_radius }
# will it cook for someone `dist` metres from it? (a station that stands on it asks only fire_lit) # will it cook for someone `dist` metres from it? (a station that stands on it asks only fire_lit)
export function fire_cooks(dist: float) -> bool { return fr_lit and dist < fr_cook_radius } export function fire_cooks(fire_st: FireState, dist: float) -> bool { return fire_st.fr_lit and dist < fire_st.fr_cook_radius }

View file

@ -10,12 +10,18 @@ export port FireWorld {
function fr_yes() -> bool { return true } function fr_yes() -> bool { return true }
function fr_no() -> bool { return false } function fr_no() -> bool { return false }
var fr_light_wood: int = 3 # logs to light one export state FireState {
var fr_light_minutes: float = 120.0 # game minutes of fuel a new fire has fr_light_wood: int = 3 # logs to light one
var fr_log_minutes: float = 60.0 # game minutes a log adds fr_light_minutes: float = 120.0 # game minutes of fuel a new fire has
var fr_low_minutes: float = 15.0 # under this, the fire is burning low fr_log_minutes: float = 60.0 # game minutes a log adds
var fr_warm_radius: float = 3.6 # metres it warms a body fr_low_minutes: float = 15.0 # under this, the fire is burning low
var fr_cook_radius: float = 3.5 # metres it cooks from fr_warm_radius: float = 3.6 # metres it warms a body
fr_cook_radius: float = 3.5 # metres it cooks from
fr_lit: bool = false
fr_fuel: float = 0.0 # game minutes left
fr_rain_h: float = 0.0 # game hours today it burned in the rain under cover
fr_facts: Queue<FireFact> = null
}
# what lighting or feeding would come to, as the world stands # what lighting or feeding would come to, as the world stands
export const FIRE_NO_WOOD: int = 0 # not enough to light one export const FIRE_NO_WOOD: int = 0 # not enough to light one
@ -35,34 +41,30 @@ export property FireFact {
fuel: float = 0.0 fuel: float = 0.0
} }
var fr_lit: bool = false
var fr_fuel: float = 0.0 # game minutes left
var fr_rain_h: float = 0.0 # game hours today it burned in the rain under cover
var fr_facts: Queue<FireFact> = null
# the facts, oldest first; the game drains them into its notices # the facts, oldest first; the game drains them into its notices
export function fire_facts() -> Queue<FireFact> { export function fire_facts(base_st: mut BaseState, fire_st: mut FireState) -> Queue<FireFact> {
if fr_facts == null { fr_facts = queue_new("fire.facts") } if fire_st.fr_facts == null { fire_st.fr_facts = queue_new(base_st, "fire.facts") }
return fr_facts return fire_st.fr_facts
} }
function fr_fact(what: int) -> void { function fr_fact(base_st: mut BaseState, fire_st: mut FireState, what: int) -> void {
let f = new FireFact let f = new FireFact
f.what = what f.what = what
f.fuel = fr_fuel f.fuel = fire_st.fr_fuel
q_push(fire_facts(), f) q_push(base_st, fire_facts(base_st, fire_st), f)
} }
# a game's numbers: logs to light, minutes a new fire and a log are worth, when it is low # a game's numbers: logs to light, minutes a new fire and a log are worth, when it is low
export function fire_config(light_wood: int, light_minutes: float, log_minutes: float, low_minutes: float) -> void { export function fire_config(fire_st: mut FireState, light_wood: int, light_minutes: float, log_minutes: float, low_minutes: float) -> void {
fr_light_wood = light_wood fire_st.fr_light_wood = light_wood
fr_light_minutes = light_minutes fire_st.fr_light_minutes = light_minutes
fr_log_minutes = log_minutes fire_st.fr_log_minutes = log_minutes
fr_low_minutes = low_minutes fire_st.fr_low_minutes = low_minutes
} }
# how far it warms a body and how far it cooks from, in metres # how far it warms a body and how far it cooks from, in metres
export function fire_config_reach(warm: float, cook: float) -> void { export function fire_config_reach(fire_st: mut FireState, warm: float, cook: float) -> void {
fr_warm_radius = warm fire_st.fr_warm_radius = warm
fr_cook_radius = cook fire_st.fr_cook_radius = cook
} }

View file

@ -1,19 +1,19 @@
# system.ludic - the fire as a system: PH_SIMULATE, and its own save section # system.ludic - the fire as a system: PH_SIMULATE, and its own save section
function fire_tick(t: Tick) -> void { fire_run(t.hours) } function fire_tick(base_st: mut BaseState, fire_st: mut FireState, t: Tick) -> void { fire_run(base_st, fire_st, t.hours) }
export function fire_save() -> Val { export function fire_save(fire_st: FireState) -> Val {
let v = Value.object() let v = Value.object()
sv_put_bool(v, "lit", fr_lit) sv_put_bool(v, "lit", fire_st.fr_lit)
sv_put_float(v, "fuel", fr_fuel) sv_put_float(v, "fuel", fire_st.fr_fuel)
sv_put_float(v, "rain_h", fr_rain_h) sv_put_float(v, "rain_h", fire_st.fr_rain_h)
return v return v
} }
export function fire_load(v: Val, version: int) -> void { export function fire_load(base_st: mut BaseState, fire_st: mut FireState, v: Val, version: int) -> void {
fire_reset() fire_reset(base_st, fire_st)
fr_lit = sv_bool(v, "lit", false) fire_st.fr_lit = sv_bool(v, "lit", false)
fr_fuel = sv_float(v, "fuel", 0.0) fire_st.fr_fuel = sv_float(v, "fuel", 0.0)
fr_rain_h = sv_float(v, "rain_h", 0.0) fire_st.fr_rain_h = sv_float(v, "rain_h", 0.0)
} }
export function fire_system() -> System { export function fire_system() -> System {

View file

@ -4,15 +4,17 @@ import "ludic.fire"
import "ludic.base" import "ludic.base"
program FireTest { program FireTest {
numbers float numbers float
var owns: bool = true state FiretestState {
var wet: bool = false owns: bool = true
var tarp: bool = false wet: bool = false
var ban: bool = false tarp: bool = false
ban: bool = false
}
function fake_owns() -> bool { return owns } function fake_owns(firetest_st: FiretestState) -> bool { return firetest_st.owns }
function fake_wet() -> bool { return wet } function fake_wet(firetest_st: FiretestState) -> bool { return firetest_st.wet }
function fake_tarp() -> bool { return tarp } function fake_tarp(firetest_st: FiretestState) -> bool { return firetest_st.tarp }
function fake_allowed() -> bool { return not ban } function fake_allowed(firetest_st: FiretestState) -> bool { return not firetest_st.ban }
bind FireWorld { bind FireWorld {
owns_world: fn fake_owns owns_world: fn fake_owns
@ -21,125 +23,125 @@ program FireTest {
allowed: fn fake_allowed allowed: fn fake_allowed
} }
function fresh() -> void { function fresh(base_st: mut BaseState, fire_st: mut FireState) -> void {
fire_config(3, 120.0, 60.0, 15.0) fire_config(fire_st, 3, 120.0, 60.0, 15.0)
fire_config_reach(3.6, 3.5) fire_config_reach(fire_st, 3.6, 3.5)
fire_reset() fire_reset(base_st, fire_st)
} }
function facts() -> []FireFact { return q_drain(fire_facts()) } function facts(base_st: mut BaseState, fire_st: mut FireState) -> []FireFact { return q_drain(base_st, fire_facts(base_st, fire_st)) }
function count(fs: []FireFact, what: int) -> int { function count(fs: []FireFact, what: int) -> int {
var n = 0 var n = 0
for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } } for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } }
return n return n
} }
function light() -> void { fire_apply(fire_decide(3)) } function light(base_st: mut BaseState, fire_st: mut FireState) -> void { fire_apply(base_st, fire_st, fire_decide(fire_st, 3)) }
test "three logs light it, and that is a fact" { test "three logs light it, and that is a fact" (base_st: mut BaseState, fire_st: mut FireState) {
fresh() fresh(base_st, fire_st)
expect_eq(fire_decide(2), FIRE_NO_WOOD) expect_eq(fire_decide(fire_st, 2), FIRE_NO_WOOD)
expect_eq(fire_decide(3), FIRE_LIGHT) expect_eq(fire_decide(fire_st, 3), FIRE_LIGHT)
expect_eq(fire_cost(FIRE_LIGHT), 3) expect_eq(fire_cost(fire_st, FIRE_LIGHT), 3)
light() light(base_st, fire_st)
expect(fire_lit()) expect(fire_lit(fire_st))
expect(fire_fuel() == 120.0) expect(fire_fuel(fire_st) == 120.0)
expect_eq(count(facts(), FIRE_LIT), 1) expect_eq(count(facts(base_st, fire_st), FIRE_LIT), 1)
} }
test "a lit fire takes a log for an hour" { test "a lit fire takes a log for an hour" (base_st: mut BaseState, fire_st: mut FireState) {
fresh() fresh(base_st, fire_st)
light() light(base_st, fire_st)
expect_eq(fire_decide(0), FIRE_NO_LOG) expect_eq(fire_decide(fire_st, 0), FIRE_NO_LOG)
expect_eq(fire_decide(1), FIRE_FEED) expect_eq(fire_decide(fire_st, 1), FIRE_FEED)
expect_eq(fire_cost(FIRE_FEED), 1) expect_eq(fire_cost(fire_st, FIRE_FEED), 1)
fire_apply(FIRE_FEED) fire_apply(base_st, fire_st, FIRE_FEED)
expect(fire_fuel() == 180.0) expect(fire_fuel(fire_st) == 180.0)
} }
test "a ban and a soaked ring refuse, a tarp keeps it dry" { test "a ban and a soaked ring refuse, a tarp keeps it dry" (base_st: mut BaseState, fire_st: mut FireState, firetest_st: mut FiretestState) {
fresh() fresh(base_st, fire_st)
ban = true firetest_st.ban = true
expect_eq(fire_decide(5), FIRE_BANNED) expect_eq(fire_decide(fire_st, 5), FIRE_BANNED)
ban = false firetest_st.ban = false
wet = true firetest_st.wet = true
expect_eq(fire_decide(5), FIRE_SOAKED) expect_eq(fire_decide(fire_st, 5), FIRE_SOAKED)
expect_eq(fire_cost(FIRE_SOAKED), 0) expect_eq(fire_cost(fire_st, FIRE_SOAKED), 0)
tarp = true firetest_st.tarp = true
expect_eq(fire_decide(5), FIRE_LIGHT) expect_eq(fire_decide(fire_st, 5), FIRE_LIGHT)
} }
test "it burns a minute a minute, runs low, and goes out as facts" { test "it burns a minute a minute, runs low, and goes out as facts" (base_st: mut BaseState, fire_st: mut FireState) {
fresh() fresh(base_st, fire_st)
light() light(base_st, fire_st)
facts() facts(base_st, fire_st)
fire_run(1.5) fire_run(base_st, fire_st, 1.5)
expect(fire_fuel() == 30.0) expect(fire_fuel(fire_st) == 30.0)
expect(not fire_low()) expect(not fire_low(fire_st))
fire_run(0.3) fire_run(base_st, fire_st, 0.3)
expect(fire_low()) expect(fire_low(fire_st))
expect_eq(count(facts(), FIRE_LOW_FUEL), 1) expect_eq(count(facts(base_st, fire_st), FIRE_LOW_FUEL), 1)
fire_run(0.5) fire_run(base_st, fire_st, 0.5)
expect(not fire_lit()) expect(not fire_lit(fire_st))
expect(fire_fuel() == 0.0) expect(fire_fuel(fire_st) == 0.0)
expect_eq(count(facts(), FIRE_WENT_OUT), 1) expect_eq(count(facts(base_st, fire_st), FIRE_WENT_OUT), 1)
} }
test "the rain burns it twice as fast, and under a tarp it counts the hours" { test "the rain burns it twice as fast, and under a tarp it counts the hours" (base_st: mut BaseState, fire_st: mut FireState, firetest_st: mut FiretestState) {
fresh() fresh(base_st, fire_st)
light() light(base_st, fire_st)
wet = true firetest_st.wet = true
fire_run(0.5) fire_run(base_st, fire_st, 0.5)
expect(fire_fuel() == 60.0) expect(fire_fuel(fire_st) == 60.0)
tarp = true firetest_st.tarp = true
fire_run(0.5) fire_run(base_st, fire_st, 0.5)
expect(fire_fuel() == 30.0) expect(fire_fuel(fire_st) == 30.0)
expect(fire_rain_hours() == 0.5) expect(fire_rain_hours(fire_st) == 0.5)
fire_morning() fire_morning(fire_st)
expect(fire_rain_hours() == 0.0) expect(fire_rain_hours(fire_st) == 0.0)
} }
test "a machine that does not own the world burns nothing, but is told" { test "a machine that does not own the world burns nothing, but is told" (base_st: mut BaseState, fire_st: mut FireState, firetest_st: mut FiretestState) {
fresh() fresh(base_st, fire_st)
light() light(base_st, fire_st)
owns = false firetest_st.owns = false
fire_run(5.0) fire_run(base_st, fire_st, 5.0)
expect(fire_fuel() == 120.0) expect(fire_fuel(fire_st) == 120.0)
facts() facts(base_st, fire_st)
fire_told(false, 0.0, true) fire_told(base_st, fire_st, false, 0.0, true)
expect(not fire_lit()) expect(not fire_lit(fire_st))
expect_eq(count(facts(), FIRE_WENT_OUT), 1) expect_eq(count(facts(base_st, fire_st), FIRE_WENT_OUT), 1)
fire_told(true, 50.0, true) fire_told(base_st, fire_st, true, 50.0, true)
fire_told(false, 0.0, false) fire_told(base_st, fire_st, false, 0.0, false)
expect_eq(q_len(fire_facts()), 0) expect_eq(q_len(fire_facts(base_st, fire_st)), 0)
} }
test "it warms and cooks close, and only while lit" { test "it warms and cooks close, and only while lit" (base_st: mut BaseState, fire_st: mut FireState) {
fresh() fresh(base_st, fire_st)
expect(not fire_warms(1.0)) expect(not fire_warms(fire_st, 1.0))
light() light(base_st, fire_st)
expect(fire_warms(3.5)) expect(fire_warms(fire_st, 3.5))
expect(not fire_warms(3.7)) expect(not fire_warms(fire_st, 3.7))
expect(fire_cooks(3.4)) expect(fire_cooks(fire_st, 3.4))
expect(not fire_cooks(3.55)) expect(not fire_cooks(fire_st, 3.55))
fire_out() fire_out(fire_st)
expect(not fire_cooks(0.0)) expect(not fire_cooks(fire_st, 0.0))
} }
test "a save section reads back the same fire" { test "a save section reads back the same fire" (base_st: mut BaseState, fire_st: mut FireState, firetest_st: mut FiretestState) {
fresh() fresh(base_st, fire_st)
light() light(base_st, fire_st)
wet = true firetest_st.wet = true
tarp = true firetest_st.tarp = true
fire_run(0.25) fire_run(base_st, fire_st, 0.25)
let root = save_tree() let root = save_tree()
save_section(root, "fire", 1, fire_save()) save_section(root, "fire", 1, fire_save(fire_st))
let text = save_encode(root) let text = save_encode(root)
fire_reset() fire_reset(base_st, fire_st)
let n = load_section(save_decode(text), "fire") let n = load_section(save_decode(text), "fire")
fire_load(n.data, n.version) fire_load(base_st, fire_st, n.data, n.version)
expect(fire_lit()) expect(fire_lit(fire_st))
expect(fire_fuel() == 105.0) expect(fire_fuel(fire_st) == 105.0)
expect(fire_rain_hours() == 0.25) expect(fire_rain_hours(fire_st) == 0.25)
} }
} }

View file

@ -1,55 +1,55 @@
# verbs.ludic - the fire burning down, and the only other ways it changes # verbs.ludic - the fire burning down, and the only other ways it changes
function fr_out() -> void { function fr_out(base_st: mut BaseState, fire_st: mut FireState) -> void {
fr_lit = false fire_st.fr_lit = false
fr_fuel = 0.0 fire_st.fr_fuel = 0.0
fr_fact(FIRE_WENT_OUT) fr_fact(base_st, fire_st, FIRE_WENT_OUT)
} }
# `gh` game hours of burning; in the rain it burns twice as fast with nothing over the ring, # `gh` game hours of burning; in the rain it burns twice as fast with nothing over the ring,
# and under cover it counts the hours it held. Only the machine that owns the world burns it. # and under cover it counts the hours it held. Only the machine that owns the world burns it.
export function fire_run(gh: float) -> void { export function fire_run(base_st: mut BaseState, fire_st: mut FireState, gh: float) -> void {
if not fr_lit or not FireWorld.owns_world() { return } if not fire_st.fr_lit or not FireWorld.owns_world() { return }
let was = fr_fuel let was = fire_st.fr_fuel
fr_fuel = fr_fuel - gh * 60.0 fire_st.fr_fuel = fire_st.fr_fuel - gh * 60.0
if FireWorld.is_wet() { if FireWorld.is_wet() {
if FireWorld.sheltered() { fr_rain_h = fr_rain_h + gh } else { fr_fuel = fr_fuel - gh * 60.0 } if FireWorld.sheltered() { fire_st.fr_rain_h = fire_st.fr_rain_h + gh } else { fire_st.fr_fuel = fire_st.fr_fuel - gh * 60.0 }
} }
if fr_fuel < 0.0 { if fire_st.fr_fuel < 0.0 {
fr_out() fr_out(base_st, fire_st)
return return
} }
if was >= fr_low_minutes and fr_fuel < fr_low_minutes { fr_fact(FIRE_LOW_FUEL) } if was >= fire_st.fr_low_minutes and fire_st.fr_fuel < fire_st.fr_low_minutes { fr_fact(base_st, fire_st, FIRE_LOW_FUEL) }
} }
# put out, and nothing said (the night burns it down) # put out, and nothing said (the night burns it down)
export function fire_out() -> void { export function fire_out(fire_st: mut FireState) -> void {
fr_lit = false fire_st.fr_lit = false
fr_fuel = 0.0 fire_st.fr_fuel = 0.0
} }
# burning with `fuel` minutes, and nothing said (staging, a test) # burning with `fuel` minutes, and nothing said (staging, a test)
export function fire_set_burning(fuel: float) -> void { export function fire_set_burning(fire_st: mut FireState, fuel: float) -> void {
fr_lit = true fire_st.fr_lit = true
fr_fuel = fuel fire_st.fr_fuel = fuel
} }
# what the machine that owns the world says the fire is; going out is a fact when `say` # what the machine that owns the world says the fire is; going out is a fact when `say`
export function fire_told(lit: bool, fuel: float, say: bool) -> void { export function fire_told(base_st: mut BaseState, fire_st: mut FireState, lit: bool, fuel: float, say: bool) -> void {
let was = fr_lit let was = fire_st.fr_lit
fr_lit = lit fire_st.fr_lit = lit
fr_fuel = fuel fire_st.fr_fuel = fuel
if was and not lit and say { fr_fact(FIRE_WENT_OUT) } if was and not lit and say { fr_fact(base_st, fire_st, FIRE_WENT_OUT) }
} }
# the rain hours outright (staging, a test) # the rain hours outright (staging, a test)
export function fire_set_rain_hours(h: float) -> void { fr_rain_h = h } export function fire_set_rain_hours(fire_st: mut FireState, h: float) -> void { fire_st.fr_rain_h = h }
# a new day: the rain hours start again # a new day: the rain hours start again
export function fire_morning() -> void { fr_rain_h = 0.0 } export function fire_morning(fire_st: mut FireState) -> void { fire_st.fr_rain_h = 0.0 }
export function fire_reset() -> void { export function fire_reset(base_st: mut BaseState, fire_st: mut FireState) -> void {
fr_lit = false fire_st.fr_lit = false
fr_fuel = 0.0 fire_st.fr_fuel = 0.0
fr_rain_h = 0.0 fire_st.fr_rain_h = 0.0
q_clear(fire_facts()) q_clear(base_st, fire_facts(base_st, fire_st))
} }

View file

@ -1,16 +1,16 @@
# fight.ludic - what takes, how big it is, and the green band the marker has to cross in # fight.ludic - what takes, how big it is, and the green band the marker has to cross in
# a legend on a lure, likelier in deep water and with skill; else an everyday one by weight # a legend on a lure, likelier in deep water and with skill; else an everyday one by weight
function fishing_pick() -> int { function fishing_pick(fishing_st: mut FishingState) -> int {
let leg = fishing_legend() let leg = fishing_legend()
if leg >= 0 and fishing_lure_on { if leg >= 0 and fishing_st.fishing_lure_on {
let k = FishSpecies[leg] let k = FishSpecies[leg]
var p = k.chance var p = k.chance
if FishingWorld.afloat() or FishingWorld.depth(fishing_bx, fishing_bz) >= fishing_deep { p = k.chance_deep } if FishingWorld.afloat() or FishingWorld.depth(fishing_st.fishing_bx, fishing_st.fishing_bz) >= fishing_st.fishing_deep { p = k.chance_deep }
if fishing_rand() < p + FishingWorld.skill() { return leg } if fishing_rand(fishing_st) < p + FishingWorld.skill() { return leg }
} }
var total = 0.0 var total = 0.0
for s in 0 .. FISH_COUNT { if not FishSpecies[s].legend { total = total + FishSpecies[s].weight } } for s in 0 .. FISH_COUNT { if not FishSpecies[s].legend { total = total + FishSpecies[s].weight } }
var r = fishing_rand() * total var r = fishing_rand(fishing_st) * total
var last = -1 var last = -1
for s in 0 .. FISH_COUNT { for s in 0 .. FISH_COUNT {
if FishSpecies[s].legend { continue } if FishSpecies[s].legend { continue }
@ -21,37 +21,37 @@ function fishing_pick() -> int {
return last return last
} }
function fishing_size(s: int) -> int { function fishing_size(fishing_st: mut FishingState, s: int) -> int {
let k = FishSpecies[s] let k = FishSpecies[s]
let spread = int(fishing_rand() * float(k.cm_spread + 1)) let spread = int(fishing_rand(fishing_st) * float(k.cm_spread + 1))
return k.cm_min + spread + int(FishingWorld.skill() * k.skill_cm) return k.cm_min + spread + int(FishingWorld.skill() * k.skill_cm)
} }
# the green's half width this crossing: a fish that closes it does so at every rung, and Hard # the green's half width this crossing: a fish that closes it does so at every rung, and Hard
# closes it for everything else # closes it for everything else
export function fishing_window() -> float { export function fishing_window(fishing_st: FishingState) -> float {
if not fishing_kind_ok(fishing_sp) { return 0.12 } if not fishing_kind_ok(fishing_st.fishing_sp) { return 0.12 }
let k = FishSpecies[fishing_sp] let k = FishSpecies[fishing_st.fishing_sp]
let hw = FishingWorld.window(fishing_sp) let hw = FishingWorld.window(fishing_st.fishing_sp)
if k.closes > 0.0 { return Math.max(hw * (1.0 - k.closes * float(fishing_won)), hw * k.floor) } if k.closes > 0.0 { return Math.max(hw * (1.0 - k.closes * float(fishing_st.fishing_won)), hw * k.floor) }
if FishingWorld.level() == FISHING_HARD { return Math.max(hw * (1.0 - 0.10 * float(fishing_won)), hw * 0.55) } if FishingWorld.level() == FISHING_HARD { return Math.max(hw * (1.0 - 0.10 * float(fishing_st.fishing_won)), hw * 0.55) }
return hw return hw
} }
export function fishing_window_lo() -> float { return Math.clamp(fishing_win_c - fishing_window(), 0.0, 1.0) } export function fishing_window_lo(fishing_st: FishingState) -> float { return Math.clamp(fishing_st.fishing_win_c - fishing_window(fishing_st), 0.0, 1.0) }
export function fishing_window_hi() -> float { return Math.clamp(fishing_win_c + fishing_window(), 0.0, 1.0) } export function fishing_window_hi(fishing_st: FishingState) -> float { return Math.clamp(fishing_st.fishing_win_c + fishing_window(fishing_st), 0.0, 1.0) }
# where it sits: the middle on Relaxed, somewhere new each crossing above it # where it sits: the middle on Relaxed, somewhere new each crossing above it
function fishing_new_window() -> void { function fishing_new_window(fishing_st: mut FishingState) -> void {
let lv = FishingWorld.level() let lv = FishingWorld.level()
if lv == FISHING_RELAXED or fishing_gentle_on { if lv == FISHING_RELAXED or fishing_st.fishing_gentle_on {
fishing_win_c = 0.5 fishing_st.fishing_win_c = 0.5
return return
} }
let hw = fishing_window() + 0.05 let hw = fishing_window(fishing_st) + 0.05
fishing_win_c = hw + fishing_rand() * (1.0 - hw - hw) fishing_st.fishing_win_c = hw + fishing_rand(fishing_st) * (1.0 - hw - hw)
} }
function fishing_in_window() -> bool { function fishing_in_window(fishing_st: FishingState) -> bool {
return fishing_mark > fishing_win_c - fishing_window() and fishing_mark < fishing_win_c + fishing_window() return fishing_st.fishing_mark > fishing_st.fishing_win_c - fishing_window(fishing_st) and fishing_st.fishing_mark < fishing_st.fishing_win_c + fishing_window(fishing_st)
} }

View file

@ -1,64 +1,64 @@
# run.ludic - `dt` real seconds of a line in the water: the wait, the bite, and the fight # run.ludic - `dt` real seconds of a line in the water: the wait, the bite, and the fight
function fishing_run_wait() -> void { function fishing_run_wait(base_st: mut BaseState, fishing_st: mut FishingState) -> void {
if fishing_t >= 0.0 { return } if fishing_st.fishing_t >= 0.0 { return }
fishing_st = FISHING_BITE_ON fishing_st.fishing_st = FISHING_BITE_ON
fishing_t = 1.6 fishing_st.fishing_t = 1.6
fishing_sp = fishing_pick() fishing_st.fishing_sp = fishing_pick(fishing_st)
fishing_say(FISHING_BITE) fishing_say(base_st, fishing_st, FISHING_BITE)
} }
# a fish that runs: the marker is all over the bar and nothing done with it helps # a fish that runs: the marker is all over the bar and nothing done with it helps
function fishing_run_runs(dt: float) -> float { function fishing_run_runs(base_st: mut BaseState, fishing_st: mut FishingState, dt: float) -> float {
let k = FishSpecies[fishing_sp] let k = FishSpecies[fishing_st.fishing_sp]
if fishing_run_t > 0.0 { if fishing_st.fishing_run_t > 0.0 {
fishing_run_t = fishing_run_t - dt fishing_st.fishing_run_t = fishing_st.fishing_run_t - dt
if fishing_run_t <= 0.0 { if fishing_st.fishing_run_t <= 0.0 {
fishing_run_t = 0.0 fishing_st.fishing_run_t = 0.0
fishing_say(FISHING_TIRED) fishing_say(base_st, fishing_st, FISHING_TIRED)
} }
return 4.2 return 4.2
} }
if fishing_run_left > 0 { if fishing_st.fishing_run_left > 0 {
fishing_run_next = fishing_run_next - dt fishing_st.fishing_run_next = fishing_st.fishing_run_next - dt
if fishing_run_next <= 0.0 { if fishing_st.fishing_run_next <= 0.0 {
fishing_run_left -= 1 fishing_st.fishing_run_left -= 1
fishing_run_t = 2.2 fishing_st.fishing_run_t = 2.2
fishing_run_next = 4.0 + fishing_rand() * 4.0 fishing_st.fishing_run_next = 4.0 + fishing_rand(fishing_st) * 4.0
fishing_say(FISHING_RUNS) fishing_say(base_st, fishing_st, FISHING_RUNS)
} }
} }
return k.speed + k.speed_per_hit * float(fishing_won) return k.speed + k.speed_per_hit * float(fishing_st.fishing_won)
} }
function fishing_run_reel(dt: float) -> void { function fishing_run_reel(base_st: mut BaseState, fishing_st: mut FishingState, dt: float) -> void {
let sp = fishing_run_runs(dt) let sp = fishing_run_runs(base_st, fishing_st, dt)
if fishing_gentle_on { if fishing_st.fishing_gentle_on {
fishing_ring_t = fishing_ring_t - dt fishing_st.fishing_ring_t = fishing_st.fishing_ring_t - dt
if fishing_ring_t <= 0.0 { fishing_lost(FISHING_LINE_SNAPPED) } if fishing_st.fishing_ring_t <= 0.0 { fishing_lost(base_st, fishing_st, FISHING_LINE_SNAPPED) }
return return
} }
fishing_mark = fishing_mark + sp * fishing_dir * dt fishing_st.fishing_mark = fishing_st.fishing_mark + sp * fishing_st.fishing_dir * dt
if fishing_mark > 1.0 { if fishing_st.fishing_mark > 1.0 {
fishing_mark = 1.0 fishing_st.fishing_mark = 1.0
fishing_dir = -1.0 fishing_st.fishing_dir = -1.0
} }
if fishing_mark < 0.0 { if fishing_st.fishing_mark < 0.0 {
fishing_mark = 0.0 fishing_st.fishing_mark = 0.0
fishing_dir = 1.0 fishing_st.fishing_dir = 1.0
} }
if fishing_t < 0.0 { fishing_lost(FISHING_SLACK) } if fishing_st.fishing_t < 0.0 { fishing_lost(base_st, fishing_st, FISHING_SLACK) }
} }
export function fishing_run(dt: float) -> void { export function fishing_run(base_st: mut BaseState, fishing_st: mut FishingState, dt: float) -> void {
if fishing_st == FISHING_OFF { return } if fishing_st.fishing_st == FISHING_OFF { return }
fishing_t = fishing_t - dt fishing_st.fishing_t = fishing_st.fishing_t - dt
if fishing_st == FISHING_WAIT { if fishing_st.fishing_st == FISHING_WAIT {
fishing_run_wait() fishing_run_wait(base_st, fishing_st)
return return
} }
if fishing_st == FISHING_BITE_ON { if fishing_st.fishing_st == FISHING_BITE_ON {
if fishing_t < 0.0 { fishing_lost(FISHING_MISSED) } if fishing_st.fishing_t < 0.0 { fishing_lost(base_st, fishing_st, FISHING_MISSED) }
return return
} }
fishing_run_reel(dt) fishing_run_reel(base_st, fishing_st, dt)
} }

View file

@ -4,86 +4,88 @@ export const FISHING_WAIT: int = 1
export const FISHING_BITE_ON: int = 2 export const FISHING_BITE_ON: int = 2
export const FISHING_REEL: int = 3 export const FISHING_REEL: int = 3
var fishing_st: int = 0 export state FishingState {
var fishing_t: float = 0.0 # seconds: to the bite, of the bite, of the fight fishing_st: int = 0
var fishing_bx: float = 0.0 # where the line is in the water fishing_t: float = 0.0 # seconds: to the bite, of the bite, of the fight
var fishing_bz: float = 0.0 fishing_bx: float = 0.0 # where the line is in the water
var fishing_sp: int = -1 # what is on it fishing_bz: float = 0.0
var fishing_len: int = 0 # cm fishing_sp: int = -1 # what is on it
var fishing_bait_on: bool = false fishing_len: int = 0 # cm
var fishing_lure_on: bool = false fishing_bait_on: bool = false
var fishing_mark: float = 0.0 # 0..1 across the bar fishing_lure_on: bool = false
var fishing_dir: float = 1.0 fishing_mark: float = 0.0 # 0..1 across the bar
var fishing_win_c: float = 0.5 # where the green sits this crossing fishing_dir: float = 1.0
var fishing_ring_t: float = 0.0 # Gentle: seconds left to press fishing_win_c: float = 0.5 # where the green sits this crossing
var fishing_gentle_on: bool = false fishing_ring_t: float = 0.0 # Gentle: seconds left to press
var fishing_won: int = 0 # crossings won fishing_gentle_on: bool = false
var fishing_need_n: int = 3 fishing_won: int = 0 # crossings won
var fishing_run_t: float = 0.0 # seconds left of a run fishing_need_n: int = 3
var fishing_run_left: int = 0 fishing_run_t: float = 0.0 # seconds left of a run
var fishing_run_next: float = 0.0 fishing_run_left: int = 0
var fishing_dice: Rng = null fishing_run_next: float = 0.0
var fishing_fact_q: Queue<FishingFact> = null fishing_dice: Rng = null
var fishing_n: int = 0 # the record: caught, the biggest (cm), a legend landed fishing_fact_q: Queue<FishingFact> = null
var fishing_big: int = 0 fishing_n: int = 0 # the record: caught, the biggest (cm), a legend landed
var fishing_leg: bool = false fishing_big: int = 0
fishing_leg: bool = false
fishing_min_depth: float = 0.15 # a cast needs this much water
fishing_deep: float = 2.5 # a legend's deep-water chance from here down
fishing_seed_n: int = 4242
}
var fishing_min_depth: float = 0.15 # a cast needs this much water
var fishing_deep: float = 2.5 # a legend's deep-water chance from here down
var fishing_seed_n: int = 4242
# a cast needs `min_depth` metres; `deep` is where a legend is likelier; the line's own dice # a cast needs `min_depth` metres; `deep` is where a legend is likelier; the line's own dice
export function fishing_config(min_depth: float, deep: float, seed: int) -> void { export function fishing_config(fishing_st: mut FishingState, min_depth: float, deep: float, seed: int) -> void {
fishing_min_depth = min_depth fishing_st.fishing_min_depth = min_depth
fishing_deep = deep fishing_st.fishing_deep = deep
fishing_seed_n = seed fishing_st.fishing_seed_n = seed
fishing_dice = rng_new(seed) fishing_st.fishing_dice = rng_new(seed)
} }
export function fishing_facts() -> Queue<FishingFact> { export function fishing_facts(base_st: mut BaseState, fishing_st: mut FishingState) -> Queue<FishingFact> {
if fishing_fact_q == null { fishing_fact_q = queue_new("fishing.facts") } if fishing_st.fishing_fact_q == null { fishing_st.fishing_fact_q = queue_new(base_st, "fishing.facts") }
return fishing_fact_q return fishing_st.fishing_fact_q
} }
function fishing_rand() -> float { function fishing_rand(fishing_st: mut FishingState) -> float {
if fishing_dice == null { fishing_dice = rng_new(fishing_seed_n) } if fishing_st.fishing_dice == null { fishing_st.fishing_dice = rng_new(fishing_st.fishing_seed_n) }
return rng_float(fishing_dice) return rng_float(fishing_st.fishing_dice)
} }
function fishing_say(what: int) -> void { function fishing_say(base_st: mut BaseState, fishing_st: mut FishingState, what: int) -> void {
let f = new FishingFact let f = new FishingFact
f.what = what f.what = what
f.species = fishing_sp f.species = fishing_st.fishing_sp
f.cm = fishing_len f.cm = fishing_st.fishing_len
f.kg = fishing_weight_of(fishing_sp, fishing_len) f.kg = fishing_weight_of(fishing_st.fishing_sp, fishing_st.fishing_len)
f.hits = fishing_won f.hits = fishing_st.fishing_won
f.need = fishing_need_n f.need = fishing_st.fishing_need_n
f.bait = fishing_bait_on f.bait = fishing_st.fishing_bait_on
f.lure = fishing_lure_on f.lure = fishing_st.fishing_lure_on
f.x = fishing_bx f.x = fishing_st.fishing_bx
f.z = fishing_bz f.z = fishing_st.fishing_bz
q_push(fishing_facts(), f) q_push(base_st, fishing_facts(base_st, fishing_st), f)
} }
export function fishing_state() -> int { return fishing_st } export function fishing_state(fishing_st: FishingState) -> int { return fishing_st.fishing_st }
export function fishing_active() -> bool { return fishing_st != FISHING_OFF } export function fishing_active(fishing_st: FishingState) -> bool { return fishing_st.fishing_st != FISHING_OFF }
export function fishing_species() -> int { return fishing_sp } export function fishing_species(fishing_st: FishingState) -> int { return fishing_st.fishing_sp }
export function fishing_cm() -> int { return fishing_len } export function fishing_cm(fishing_st: FishingState) -> int { return fishing_st.fishing_len }
export function fishing_line_x() -> float { return fishing_bx } export function fishing_line_x(fishing_st: FishingState) -> float { return fishing_st.fishing_bx }
export function fishing_line_z() -> float { return fishing_bz } export function fishing_line_z(fishing_st: FishingState) -> float { return fishing_st.fishing_bz }
export function fishing_marker() -> float { return fishing_mark } export function fishing_marker(fishing_st: FishingState) -> float { return fishing_st.fishing_mark }
export function fishing_hits() -> int { return fishing_won } export function fishing_hits(fishing_st: FishingState) -> int { return fishing_st.fishing_won }
export function fishing_need() -> int { return fishing_need_n } export function fishing_need(fishing_st: FishingState) -> int { return fishing_st.fishing_need_n }
export function fishing_running() -> bool { return fishing_run_t > 0.0 } export function fishing_running(fishing_st: FishingState) -> bool { return fishing_st.fishing_run_t > 0.0 }
export function fishing_gentle() -> bool { return fishing_gentle_on } export function fishing_gentle(fishing_st: FishingState) -> bool { return fishing_st.fishing_gentle_on }
export function fishing_ring() -> float { return fishing_ring_t } export function fishing_ring(fishing_st: FishingState) -> float { return fishing_st.fishing_ring_t }
export function fishing_baited() -> bool { return fishing_bait_on } export function fishing_baited(fishing_st: FishingState) -> bool { return fishing_st.fishing_bait_on }
export function fishing_lured() -> bool { return fishing_lure_on } export function fishing_lured(fishing_st: FishingState) -> bool { return fishing_st.fishing_lure_on }
export function fishing_caught() -> int { return fishing_n } export function fishing_caught(fishing_st: FishingState) -> int { return fishing_st.fishing_n }
export function fishing_biggest() -> int { return fishing_big } export function fishing_biggest(fishing_st: FishingState) -> int { return fishing_st.fishing_big }
export function fishing_legend_caught() -> bool { return fishing_leg } export function fishing_legend_caught(fishing_st: FishingState) -> bool { return fishing_st.fishing_leg }
# can a line land at (x, z)? # can a line land at (x, z)?
export function fishing_water_at(x: float, z: float) -> bool { export function fishing_water_at(fishing_st: FishingState, x: float, z: float) -> bool {
return FishingWorld.afloat() or FishingWorld.depth(x, z) >= fishing_min_depth return FishingWorld.afloat() or FishingWorld.depth(x, z) >= fishing_st.fishing_min_depth
} }

View file

@ -1,35 +1,35 @@
# system.ludic - fishing as a system: its tick, and the record in its own save section (a line in # system.ludic - fishing as a system: its tick, and the record in its own save section (a line in
# the water is never saved) # the water is never saved)
export function fishing_reset() -> void { export function fishing_reset(base_st: mut BaseState, fishing_st: mut FishingState) -> void {
fishing_stop() fishing_stop(fishing_st)
fishing_n = 0 fishing_st.fishing_n = 0
fishing_big = 0 fishing_st.fishing_big = 0
fishing_leg = false fishing_st.fishing_leg = false
fishing_dice = rng_new(fishing_seed_n) fishing_st.fishing_dice = rng_new(fishing_st.fishing_seed_n)
q_clear(fishing_facts()) q_clear(base_st, fishing_facts(base_st, fishing_st))
} }
# the record outright (staging, a test, the network's word) # the record outright (staging, a test, the network's word)
export function fishing_record_set(caught: int, biggest: int, legend: bool) -> void { export function fishing_record_set(fishing_st: mut FishingState, caught: int, biggest: int, legend: bool) -> void {
fishing_n = caught fishing_st.fishing_n = caught
fishing_big = biggest fishing_st.fishing_big = biggest
fishing_leg = legend fishing_st.fishing_leg = legend
} }
export function fishing_save() -> Val { export function fishing_save(fishing_st: FishingState) -> Val {
let v = Value.object() let v = Value.object()
sv_put_int(v, "caught", fishing_n) sv_put_int(v, "caught", fishing_st.fishing_n)
sv_put_int(v, "biggest", fishing_big) sv_put_int(v, "biggest", fishing_st.fishing_big)
sv_put_bool(v, "legend", fishing_leg) sv_put_bool(v, "legend", fishing_st.fishing_leg)
return v return v
} }
export function fishing_load(v: Val, version: int) -> void { export function fishing_load(base_st: mut BaseState, fishing_st: mut FishingState, v: Val, version: int) -> void {
fishing_reset() fishing_reset(base_st, fishing_st)
fishing_record_set(sv_int(v, "caught", 0), sv_int(v, "biggest", 0), sv_bool(v, "legend", false)) fishing_record_set(fishing_st, sv_int(v, "caught", 0), sv_int(v, "biggest", 0), sv_bool(v, "legend", false))
} }
function fishing_sys_tick(t: Tick) -> void { fishing_run(t.dt) } function fishing_sys_tick(base_st: mut BaseState, fishing_st: mut FishingState, t: Tick) -> void { fishing_run(base_st, fishing_st, t.dt) }
export function fishing_system() -> System { export function fishing_system() -> System {
let s = system_new("fishing", PH_SIMULATE) let s = system_new("fishing", PH_SIMULATE)

View file

@ -10,246 +10,248 @@ program FishingTest {
def FishSpecies brook { weight: 0.25, cm_min: 20, cm_spread: 10 } def FishSpecies brook { weight: 0.25, cm_min: 20, cm_spread: 10 }
def FishSpecies old_maroon { legend: true, cm_min: 70, cm_spread: 25, skill_cm: 0.0, chance: 1.0, chance_deep: 1.0, need: 5, runs: 2, fight: 30.0, speed: 1.3, speed_per_hit: 0.22, closes: 0.12 } def FishSpecies old_maroon { legend: true, cm_min: 70, cm_spread: 25, skill_cm: 0.0, chance: 1.0, chance_deep: 1.0, need: 5, runs: 2, fight: 30.0, speed: 1.3, speed_per_hit: 0.22, closes: 0.12 }
var bait: bool = false state FishingtestState {
var lure: bool = false bait: bool = false
var boat: bool = false lure: bool = false
var level: int = 0 boat: bool = false
var skill: float = 0.0 level: int = 0
skill: float = 0.0
}
# water past x = 10, deepening a metre every ten # water past x = 10, deepening a metre every ten
function depth(x: float, z: float) -> float { return Math.max((x - 10.0) / 10.0, 0.0) } function depth(x: float, z: float) -> float { return Math.max((x - 10.0) / 10.0, 0.0) }
function afloat() -> bool { return boat } function afloat(fishingtest_st: FishingtestState) -> bool { return fishingtest_st.boat }
function has_bait() -> bool { return bait } function has_bait(fishingtest_st: FishingtestState) -> bool { return fishingtest_st.bait }
function has_lure() -> bool { return lure } function has_lure(fishingtest_st: FishingtestState) -> bool { return fishingtest_st.lure }
function fake_skill() -> float { return skill } function fake_skill(fishingtest_st: FishingtestState) -> float { return fishingtest_st.skill }
function fake_level() -> int { return level } function fake_level(fishingtest_st: FishingtestState) -> int { return fishingtest_st.level }
bind FishingWorld { depth: fn depth, afloat: fn afloat, has_bait: fn has_bait, has_lure: fn has_lure, skill: fn fake_skill, level: fn fake_level } bind FishingWorld { depth: fn depth, afloat: fn afloat, has_bait: fn has_bait, has_lure: fn has_lure, skill: fn fake_skill, level: fn fake_level }
function fresh() -> void { function fresh(base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) -> void {
fishing_config(0.15, 2.5, 99) fishing_config(fishing_st, 0.15, 2.5, 99)
fishing_reset() fishing_reset(base_st, fishing_st)
bait = false fishingtest_st.bait = false
lure = false fishingtest_st.lure = false
boat = false fishingtest_st.boat = false
level = FISHING_RELAXED fishingtest_st.level = FISHING_RELAXED
skill = 0.0 fishingtest_st.skill = 0.0
} }
function facts() -> []FishingFact { return q_drain(fishing_facts()) } function facts(base_st: mut BaseState, fishing_st: mut FishingState) -> []FishingFact { return q_drain(base_st, fishing_facts(base_st, fishing_st)) }
function saw(fs: []FishingFact, what: int) -> bool { function saw(fs: []FishingFact, what: int) -> bool {
for i in 0 .. len(fs) { if fs[i].what == what { return true } } for i in 0 .. len(fs) { if fs[i].what == what { return true } }
return false return false
} }
# run until it bites, then strike # run until it bites, then strike
function hook() -> void { function hook(base_st: mut BaseState, fishing_st: mut FishingState) -> void {
fishing_set_wait(0.0) fishing_set_wait(fishing_st, 0.0)
fishing_run(0.02) fishing_run(base_st, fishing_st, 0.02)
fishing_press() fishing_press(base_st, fishing_st)
} }
# hold the marker in the green and press, until it is landed or `n` presses are spent # hold the marker in the green and press, until it is landed or `n` presses are spent
function play(n: int) -> void { function play(base_st: mut BaseState, fishing_st: mut FishingState, n: int) -> void {
for i in 0 .. n { for i in 0 .. n {
if fishing_state() != FISHING_REEL { return } if fishing_state(fishing_st) != FISHING_REEL { return }
var guard = 0 var guard = 0
while (fishing_running() or fishing_marker() <= fishing_window_lo() or fishing_marker() >= fishing_window_hi()) and guard < 4000 { while (fishing_running(fishing_st) or fishing_marker(fishing_st) <= fishing_window_lo(fishing_st) or fishing_marker(fishing_st) >= fishing_window_hi(fishing_st)) and guard < 4000 {
fishing_run(0.005) fishing_run(base_st, fishing_st, 0.005)
guard += 1 guard += 1
} }
if fishing_state() == FISHING_REEL { fishing_press() } if fishing_state(fishing_st) == FISHING_REEL { fishing_press(base_st, fishing_st) }
} }
} }
test "a cast lands only in water, and never twice" { test "a cast lands only in water, and never twice" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) {
fresh() fresh(base_st, fishing_st, fishingtest_st)
expect_eq(fishing_cast(5.0, 0.0), FISHING_NOT_WATER) expect_eq(fishing_cast(base_st, fishing_st, 5.0, 0.0), FISHING_NOT_WATER)
expect(not fishing_active()) expect(not fishing_active(fishing_st))
expect_eq(fishing_cast(20.0, 0.0), FISHING_CAST_OK) expect_eq(fishing_cast(base_st, fishing_st, 20.0, 0.0), FISHING_CAST_OK)
expect_eq(fishing_state(), FISHING_WAIT) expect_eq(fishing_state(fishing_st), FISHING_WAIT)
expect_eq(fishing_cast(20.0, 0.0), FISHING_BUSY) expect_eq(fishing_cast(base_st, fishing_st, 20.0, 0.0), FISHING_BUSY)
boat = true fishingtest_st.boat = true
fishing_stop() fishing_stop(fishing_st)
expect_eq(fishing_cast(0.0, 0.0), FISHING_CAST_OK) expect_eq(fishing_cast(base_st, fishing_st, 0.0, 0.0), FISHING_CAST_OK)
} }
test "bait is used and said, and halves the wait" { test "bait is used and said, and halves the wait" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) {
fresh() fresh(base_st, fishing_st, fishingtest_st)
fishing_cast(20.0, 0.0) fishing_cast(base_st, fishing_st, 20.0, 0.0)
expect(not saw(facts(), FISHING_BAIT_USED)) expect(not saw(facts(base_st, fishing_st), FISHING_BAIT_USED))
var plain = 0 var plain = 0
while fishing_state() == FISHING_WAIT { while fishing_state(fishing_st) == FISHING_WAIT {
fishing_run(0.1) fishing_run(base_st, fishing_st, 0.1)
plain += 1 plain += 1
} }
expect(plain <= 201) expect(plain <= 201)
fresh() fresh(base_st, fishing_st, fishingtest_st)
bait = true fishingtest_st.bait = true
fishing_cast(20.0, 0.0) fishing_cast(base_st, fishing_st, 20.0, 0.0)
let fs = facts() let fs = facts(base_st, fishing_st)
expect(saw(fs, FISHING_BAIT_USED)) expect(saw(fs, FISHING_BAIT_USED))
expect(fs[0].bait) expect(fs[0].bait)
var baited = 0 var baited = 0
while fishing_state() == FISHING_WAIT { while fishing_state(fishing_st) == FISHING_WAIT {
fishing_run(0.1) fishing_run(base_st, fishing_st, 0.1)
baited += 1 baited += 1
} }
expect(baited <= 101) expect(baited <= 101)
} }
test "a bite missed is gone, a bite struck is hooked" { test "a bite missed is gone, a bite struck is hooked" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) {
fresh() fresh(base_st, fishing_st, fishingtest_st)
fishing_cast(20.0, 0.0) fishing_cast(base_st, fishing_st, 20.0, 0.0)
fishing_set_wait(0.0) fishing_set_wait(fishing_st, 0.0)
fishing_run(0.02) fishing_run(base_st, fishing_st, 0.02)
expect_eq(fishing_state(), FISHING_BITE_ON) expect_eq(fishing_state(fishing_st), FISHING_BITE_ON)
expect(fishing_species() >= 0) expect(fishing_species(fishing_st) >= 0)
fishing_run(2.0) fishing_run(base_st, fishing_st, 2.0)
expect(not fishing_active()) expect(not fishing_active(fishing_st))
expect(saw(facts(), FISHING_MISSED)) expect(saw(facts(base_st, fishing_st), FISHING_MISSED))
fishing_cast(20.0, 0.0) fishing_cast(base_st, fishing_st, 20.0, 0.0)
hook() hook(base_st, fishing_st)
expect_eq(fishing_state(), FISHING_REEL) expect_eq(fishing_state(fishing_st), FISHING_REEL)
expect(saw(facts(), FISHING_HOOKED)) expect(saw(facts(base_st, fishing_st), FISHING_HOOKED))
} }
test "everyday fish only without a lure, sized and weighed from the data" { test "everyday fish only without a lure, sized and weighed from the data" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) {
fresh() fresh(base_st, fishing_st, fishingtest_st)
for i in 0 .. 40 { for i in 0 .. 40 {
fishing_cast(20.0, 0.0) fishing_cast(base_st, fishing_st, 20.0, 0.0)
hook() hook(base_st, fishing_st)
let s = fishing_species() let s = fishing_species(fishing_st)
expect(s != FISH_OLD_MAROON) expect(s != FISH_OLD_MAROON)
let k = FishSpecies[s] let k = FishSpecies[s]
expect(fishing_cm() >= k.cm_min and fishing_cm() <= k.cm_min + k.cm_spread) expect(fishing_cm(fishing_st) >= k.cm_min and fishing_cm(fishing_st) <= k.cm_min + k.cm_spread)
fishing_stop() fishing_stop(fishing_st)
} }
expect(fishing_weight_of(FISH_CUTTHROAT, 50) > fishing_weight_of(FISH_CUTTHROAT, 30)) expect(fishing_weight_of(FISH_CUTTHROAT, 50) > fishing_weight_of(FISH_CUTTHROAT, 30))
} }
test "reeled in the green three times, it is landed and counted" { test "reeled in the green three times, it is landed and counted" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) {
fresh() fresh(base_st, fishing_st, fishingtest_st)
fishing_cast(20.0, 0.0) fishing_cast(base_st, fishing_st, 20.0, 0.0)
hook() hook(base_st, fishing_st)
facts() facts(base_st, fishing_st)
play(3) play(base_st, fishing_st, 3)
let fs = facts() let fs = facts(base_st, fishing_st)
expect(saw(fs, FISHING_CAUGHT)) expect(saw(fs, FISHING_CAUGHT))
expect_eq(fishing_caught(), 1) expect_eq(fishing_caught(fishing_st), 1)
expect(fishing_biggest() > 0) expect(fishing_biggest(fishing_st) > 0)
expect(not fishing_active()) expect(not fishing_active(fishing_st))
let last = fs[len(fs) - 1] let last = fs[len(fs) - 1]
expect(last.kg > 0.0) expect(last.kg > 0.0)
expect(last.x == 20.0) expect(last.x == 20.0)
} }
test "a press outside the green throws the hook" { test "a press outside the green throws the hook" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) {
fresh() fresh(base_st, fishing_st, fishingtest_st)
fishing_cast(20.0, 0.0) fishing_cast(base_st, fishing_st, 20.0, 0.0)
hook() hook(base_st, fishing_st)
while fishing_marker() < fishing_window_hi() + 0.01 { fishing_run(0.005) } while fishing_marker(fishing_st) < fishing_window_hi(fishing_st) + 0.01 { fishing_run(base_st, fishing_st, 0.005) }
fishing_press() fishing_press(base_st, fishing_st)
expect(saw(facts(), FISHING_LINE_SNAPPED)) expect(saw(facts(base_st, fishing_st), FISHING_LINE_SNAPPED))
expect(not fishing_active()) expect(not fishing_active(fishing_st))
} }
test "a fight left alone goes slack" { test "a fight left alone goes slack" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) {
fresh() fresh(base_st, fishing_st, fishingtest_st)
fishing_cast(20.0, 0.0) fishing_cast(base_st, fishing_st, 20.0, 0.0)
hook() hook(base_st, fishing_st)
for i in 0 .. 700 { fishing_run(0.02) } for i in 0 .. 700 { fishing_run(base_st, fishing_st, 0.02) }
expect(saw(facts(), FISHING_SLACK)) expect(saw(facts(base_st, fishing_st), FISHING_SLACK))
expect_eq(fishing_caught(), 0) expect_eq(fishing_caught(fishing_st), 0)
} }
test "a legend takes the lure, runs, turns, and is landed at five" { test "a legend takes the lure, runs, turns, and is landed at five" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) {
fresh() fresh(base_st, fishing_st, fishingtest_st)
lure = true fishingtest_st.lure = true
fishing_cast(20.0, 0.0) fishing_cast(base_st, fishing_st, 20.0, 0.0)
hook() hook(base_st, fishing_st)
expect_eq(fishing_species(), FISH_OLD_MAROON) expect_eq(fishing_species(fishing_st), FISH_OLD_MAROON)
expect_eq(fishing_need(), 5) expect_eq(fishing_need(fishing_st), 5)
let w0 = fishing_window() let w0 = fishing_window(fishing_st)
play(2) play(base_st, fishing_st, 2)
expect(fishing_window() < w0) expect(fishing_window(fishing_st) < w0)
for i in 0 .. 400 { for i in 0 .. 400 {
if not fishing_running() { fishing_run(0.02) } if not fishing_running(fishing_st) { fishing_run(base_st, fishing_st, 0.02) }
} }
let fs = facts() let fs = facts(base_st, fishing_st)
expect(saw(fs, FISHING_RUNS)) expect(saw(fs, FISHING_RUNS))
if fishing_running() { if fishing_running(fishing_st) {
let before = fishing_hits() let before = fishing_hits(fishing_st)
fishing_press() fishing_press(base_st, fishing_st)
expect_eq(fishing_hits(), before - 1) expect_eq(fishing_hits(fishing_st), before - 1)
} }
play(40) play(base_st, fishing_st, 40)
expect(fishing_legend_caught()) expect(fishing_legend_caught(fishing_st))
let last = facts() let last = facts(base_st, fishing_st)
expect(last[len(last) - 1].lure) expect(last[len(last) - 1].lure)
} }
test "gentle: one press inside the ring, and the ring closes" { test "gentle: one press inside the ring, and the ring closes" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) {
fresh() fresh(base_st, fishing_st, fishingtest_st)
level = FISHING_GENTLE fishingtest_st.level = FISHING_GENTLE
fishing_cast(20.0, 0.0) fishing_cast(base_st, fishing_st, 20.0, 0.0)
hook() hook(base_st, fishing_st)
expect(fishing_gentle()) expect(fishing_gentle(fishing_st))
expect_eq(fishing_need(), 1) expect_eq(fishing_need(fishing_st), 1)
fishing_press() fishing_press(base_st, fishing_st)
expect_eq(fishing_caught(), 1) expect_eq(fishing_caught(fishing_st), 1)
fishing_cast(20.0, 0.0) fishing_cast(base_st, fishing_st, 20.0, 0.0)
hook() hook(base_st, fishing_st)
fishing_run(2.1) fishing_run(base_st, fishing_st, 2.1)
expect(saw(facts(), FISHING_LINE_SNAPPED)) expect(saw(facts(base_st, fishing_st), FISHING_LINE_SNAPPED))
} }
test "a legend fights on gentle too" { test "a legend fights on gentle too" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) {
fresh() fresh(base_st, fishing_st, fishingtest_st)
level = FISHING_GENTLE fishingtest_st.level = FISHING_GENTLE
lure = true fishingtest_st.lure = true
fishing_cast(20.0, 0.0) fishing_cast(base_st, fishing_st, 20.0, 0.0)
hook() hook(base_st, fishing_st)
expect(not fishing_gentle()) expect(not fishing_gentle(fishing_st))
expect_eq(fishing_need(), 5) expect_eq(fishing_need(fishing_st), 5)
} }
test "hard closes the window for everyday fish, relaxed keeps it centred" { test "hard closes the window for everyday fish, relaxed keeps it centred" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) {
fresh() fresh(base_st, fishing_st, fishingtest_st)
fishing_cast(20.0, 0.0) fishing_cast(base_st, fishing_st, 20.0, 0.0)
hook() hook(base_st, fishing_st)
expect(fishing_window_lo() + fishing_window_hi() > 0.999 and fishing_window_lo() + fishing_window_hi() < 1.001) expect(fishing_window_lo(fishing_st) + fishing_window_hi(fishing_st) > 0.999 and fishing_window_lo(fishing_st) + fishing_window_hi(fishing_st) < 1.001)
fishing_stop() fishing_stop(fishing_st)
level = FISHING_HARD fishingtest_st.level = FISHING_HARD
fishing_cast(20.0, 0.0) fishing_cast(base_st, fishing_st, 20.0, 0.0)
hook() hook(base_st, fishing_st)
let w0 = fishing_window() let w0 = fishing_window(fishing_st)
play(1) play(base_st, fishing_st, 1)
expect(fishing_window() < w0) expect(fishing_window(fishing_st) < w0)
} }
test "skill makes a fish bigger" { test "skill makes a fish bigger" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) {
fresh() fresh(base_st, fishing_st, fishingtest_st)
skill = 1.0 fishingtest_st.skill = 1.0
fishing_cast(20.0, 0.0) fishing_cast(base_st, fishing_st, 20.0, 0.0)
hook() hook(base_st, fishing_st)
let k = FishSpecies[fishing_species()] let k = FishSpecies[fishing_species(fishing_st)]
expect(fishing_cm() >= k.cm_min + 60) expect(fishing_cm(fishing_st) >= k.cm_min + 60)
} }
test "the record is saved, and a line in the water is not" { test "the record is saved, and a line in the water is not" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) {
fresh() fresh(base_st, fishing_st, fishingtest_st)
fishing_record_set(7, 52, true) fishing_record_set(fishing_st, 7, 52, true)
fishing_cast(20.0, 0.0) fishing_cast(base_st, fishing_st, 20.0, 0.0)
let root = save_tree() let root = save_tree()
save_section(root, "fishing", 1, fishing_save()) save_section(root, "fishing", 1, fishing_save(fishing_st))
let text = save_encode(root) let text = save_encode(root)
fishing_reset() fishing_reset(base_st, fishing_st)
let n = load_section(save_decode(text), "fishing") let n = load_section(save_decode(text), "fishing")
fishing_load(n.data, n.version) fishing_load(base_st, fishing_st, n.data, n.version)
expect_eq(fishing_caught(), 7) expect_eq(fishing_caught(fishing_st), 7)
expect_eq(fishing_biggest(), 52) expect_eq(fishing_biggest(fishing_st), 52)
expect(fishing_legend_caught()) expect(fishing_legend_caught(fishing_st))
expect(not fishing_active()) expect(not fishing_active(fishing_st))
} }
} }

View file

@ -4,102 +4,102 @@ export const FISHING_NOT_WATER: int = 1
export const FISHING_BUSY: int = 2 export const FISHING_BUSY: int = 2
# a line into (x, z): bait halves the wait (and is a fact, for the game to take from the pack) # a line into (x, z): bait halves the wait (and is a fact, for the game to take from the pack)
export function fishing_cast(x: float, z: float) -> int { export function fishing_cast(base_st: mut BaseState, fishing_st: mut FishingState, x: float, z: float) -> int {
if fishing_st != FISHING_OFF { return FISHING_BUSY } if fishing_st.fishing_st != FISHING_OFF { return FISHING_BUSY }
fishing_bx = x fishing_st.fishing_bx = x
fishing_bz = z fishing_st.fishing_bz = z
if not fishing_water_at(x, z) { return FISHING_NOT_WATER } if not fishing_water_at(fishing_st, x, z) { return FISHING_NOT_WATER }
fishing_st = FISHING_WAIT fishing_st.fishing_st = FISHING_WAIT
fishing_sp = -1 fishing_st.fishing_sp = -1
fishing_len = 0 fishing_st.fishing_len = 0
fishing_lure_on = FishingWorld.has_lure() fishing_st.fishing_lure_on = FishingWorld.has_lure()
fishing_bait_on = FishingWorld.has_bait() fishing_st.fishing_bait_on = FishingWorld.has_bait()
var wait = 6.0 + fishing_rand() * 14.0 var wait = 6.0 + fishing_rand(fishing_st) * 14.0
if fishing_bait_on { wait = wait * 0.5 } if fishing_st.fishing_bait_on { wait = wait * 0.5 }
fishing_t = wait * FishingWorld.bite_mult() fishing_st.fishing_t = wait * FishingWorld.bite_mult()
if fishing_bait_on { fishing_say(FISHING_BAIT_USED) } if fishing_st.fishing_bait_on { fishing_say(base_st, fishing_st, FISHING_BAIT_USED) }
fishing_say(FISHING_CAST) fishing_say(base_st, fishing_st, FISHING_CAST)
return FISHING_CAST_OK return FISHING_CAST_OK
} }
# the bite comes exactly `secs` after now (staging, footage) # the bite comes exactly `secs` after now (staging, footage)
export function fishing_set_wait(secs: float) -> void { export function fishing_set_wait(fishing_st: mut FishingState, secs: float) -> void {
if fishing_st == FISHING_WAIT { fishing_t = secs } if fishing_st.fishing_st == FISHING_WAIT { fishing_st.fishing_t = secs }
} }
export function fishing_stop() -> void { export function fishing_stop(fishing_st: mut FishingState) -> void {
fishing_st = FISHING_OFF fishing_st.fishing_st = FISHING_OFF
fishing_run_t = 0.0 fishing_st.fishing_run_t = 0.0
} }
function fishing_strike() -> void { function fishing_strike(base_st: mut BaseState, fishing_st: mut FishingState) -> void {
let k = FishSpecies[fishing_sp] let k = FishSpecies[fishing_st.fishing_sp]
fishing_st = FISHING_REEL fishing_st.fishing_st = FISHING_REEL
fishing_won = 0 fishing_st.fishing_won = 0
fishing_mark = 0.0 fishing_st.fishing_mark = 0.0
fishing_dir = 1.0 fishing_st.fishing_dir = 1.0
fishing_t = k.fight fishing_st.fishing_t = k.fight
fishing_need_n = k.need fishing_st.fishing_need_n = k.need
fishing_run_t = 0.0 fishing_st.fishing_run_t = 0.0
fishing_run_left = k.runs fishing_st.fishing_run_left = k.runs
fishing_run_next = 3.0 + fishing_rand() * 3.0 fishing_st.fishing_run_next = 3.0 + fishing_rand(fishing_st) * 3.0
fishing_len = fishing_size(fishing_sp) fishing_st.fishing_len = fishing_size(fishing_st, fishing_st.fishing_sp)
fishing_gentle_on = FishingWorld.level() == FISHING_GENTLE and not k.legend fishing_st.fishing_gentle_on = FishingWorld.level() == FISHING_GENTLE and not k.legend
fishing_ring_t = 0.0 fishing_st.fishing_ring_t = 0.0
if fishing_gentle_on { if fishing_st.fishing_gentle_on {
fishing_need_n = 1 fishing_st.fishing_need_n = 1
fishing_ring_t = 2.0 fishing_st.fishing_ring_t = 2.0
} }
fishing_new_window() fishing_new_window(fishing_st)
fishing_say(FISHING_HOOKED) fishing_say(base_st, fishing_st, FISHING_HOOKED)
} }
function fishing_land() -> void { function fishing_land(base_st: mut BaseState, fishing_st: mut FishingState) -> void {
fishing_n += 1 fishing_st.fishing_n += 1
if fishing_len > fishing_big { fishing_big = fishing_len } if fishing_st.fishing_len > fishing_st.fishing_big { fishing_st.fishing_big = fishing_st.fishing_len }
if FishSpecies[fishing_sp].legend { fishing_leg = true } if FishSpecies[fishing_st.fishing_sp].legend { fishing_st.fishing_leg = true }
fishing_say(FISHING_CAUGHT) fishing_say(base_st, fishing_st, FISHING_CAUGHT)
fishing_stop() fishing_stop(fishing_st)
} }
function fishing_lost(what: int) -> void { function fishing_lost(base_st: mut BaseState, fishing_st: mut FishingState, what: int) -> void {
fishing_say(what) fishing_say(base_st, fishing_st, what)
fishing_stop() fishing_stop(fishing_st)
} }
function fishing_reel_press() -> void { function fishing_reel_press(base_st: mut BaseState, fishing_st: mut FishingState) -> void {
if fishing_gentle_on { if fishing_st.fishing_gentle_on {
if fishing_ring_t > 0.0 { fishing_land() } else { fishing_lost(FISHING_LINE_SNAPPED) } if fishing_st.fishing_ring_t > 0.0 { fishing_land(base_st, fishing_st) } else { fishing_lost(base_st, fishing_st, FISHING_LINE_SNAPPED) }
return return
} }
if fishing_run_t > 0.0 { if fishing_st.fishing_run_t > 0.0 {
fishing_won = Math.max(fishing_won - 1, 0) fishing_st.fishing_won = Math.max(fishing_st.fishing_won - 1, 0)
fishing_say(FISHING_CHECKED) fishing_say(base_st, fishing_st, FISHING_CHECKED)
return return
} }
if fishing_in_window() { if fishing_in_window(fishing_st) {
fishing_won += 1 fishing_st.fishing_won += 1
fishing_new_window() fishing_new_window(fishing_st)
if fishing_won >= fishing_need_n { fishing_land() } else { fishing_say(FISHING_GOOD) } if fishing_st.fishing_won >= fishing_st.fishing_need_n { fishing_land(base_st, fishing_st) } else { fishing_say(base_st, fishing_st, FISHING_GOOD) }
return return
} }
if FishSpecies[fishing_sp].legend and fishing_won > 0 { if FishSpecies[fishing_st.fishing_sp].legend and fishing_st.fishing_won > 0 {
fishing_won -= 1 fishing_st.fishing_won -= 1
fishing_say(FISHING_TURNED) fishing_say(base_st, fishing_st, FISHING_TURNED)
return return
} }
fishing_lost(FISHING_LINE_SNAPPED) fishing_lost(base_st, fishing_st, FISHING_LINE_SNAPPED)
} }
# the button: waiting it reels in, on a bite it strikes, on a fish it reels # the button: waiting it reels in, on a bite it strikes, on a fish it reels
export function fishing_press() -> void { export function fishing_press(base_st: mut BaseState, fishing_st: mut FishingState) -> void {
if fishing_st == FISHING_WAIT { if fishing_st.fishing_st == FISHING_WAIT {
fishing_lost(FISHING_REELED_IN) fishing_lost(base_st, fishing_st, FISHING_REELED_IN)
return return
} }
if fishing_st == FISHING_BITE_ON { if fishing_st.fishing_st == FISHING_BITE_ON {
fishing_strike() fishing_strike(base_st, fishing_st)
return return
} }
if fishing_st == FISHING_REEL { fishing_reel_press() } if fishing_st.fishing_st == FISHING_REEL { fishing_reel_press(base_st, fishing_st) }
} }

View file

@ -1,25 +1,25 @@
# buy.ludic - a tier bought with money once the standing is there; the kind's item follows # buy.ludic - a tier bought with money once the standing is there; the kind's item follows
export function gear_can_buy(k: int) -> bool { export function gear_can_buy(gear_st: mut GearState, k: int) -> bool {
if not gear_ok(k) or gear_best(k) or GearWorld.hidden(k) { return false } if not gear_ok(k) or gear_best(gear_st, k) or GearWorld.hidden(k) { return false }
return GearPurse.rep() >= gear_next_rep(k) and GearPurse.money() >= gear_next_cost(k) return GearPurse.rep() >= gear_next_rep(gear_st, k) and GearPurse.money() >= gear_next_cost(gear_st, k)
} }
export function gear_buy(k: int) -> bool { export function gear_buy(base_st: mut BaseState, gear_st: mut GearState, k: int) -> bool {
if not gear_can_buy(k) { return false } if not gear_can_buy(gear_st, k) { return false }
let cost = gear_next_cost(k) let cost = gear_next_cost(gear_st, k)
if not GearPurse.spend(cost) { return false } if not GearPurse.spend(cost) { return false }
gear__lv[k] += 1 gear_st.gear__lv[k] += 1
let it = GearKinds[k].item let it = GearKinds[k].item
if it >= 0 and GearPack.count(it) == 0 { GearPack.add(it, 1) } if it >= 0 and GearPack.count(it) == 0 { GearPack.add(it, 1) }
gear__fact(GEAR_BOUGHT, k, -1, cost) gear__fact(base_st, gear_st, GEAR_BOUGHT, k, -1, cost)
return true return true
} }
# a kind whose item is already carried counts as its first tier # a kind whose item is already carried counts as its first tier
export function gear_sync() -> void { export function gear_sync(gear_st: mut GearState) -> void {
gear__ensure() gear__ensure(gear_st)
for k in 0 .. GEAR_COUNT { for k in 0 .. GEAR_COUNT {
let it = GearKinds[k].item let it = GearKinds[k].item
if it >= 0 and gear__lv[k] == 0 and GearPack.count(it) > 0 { gear__lv[k] = 1 } if it >= 0 and gear_st.gear__lv[k] == 0 and GearPack.count(it) > 0 { gear_st.gear__lv[k] = 1 }
} }
} }

View file

@ -1,12 +1,12 @@
# hand.ludic - what is held, and the charges: a held one runs only while it is in the hand, and # hand.ludic - what is held, and the charges: a held one runs only while it is in the hand, and
# each burns down by the game hours while it runs # each burns down by the game hours while it runs
export function gear_hand() -> int { return gear__hand } export function gear_hand(gear_st: GearState) -> int { return gear_st.gear__hand }
# a new thing in the hand (-1 empty): whatever the old one was running goes out # a new thing in the hand (-1 empty): whatever the old one was running goes out
export function gear_set_hand(it: int) -> void { export function gear_set_hand(gear_st: mut GearState, it: int) -> void {
gear__ensure() gear__ensure(gear_st)
gear__hand = it gear_st.gear__hand = it
for c in 0 .. CHARGE_COUNT { if GearCharges[c].hand { gear__on[c] = false } } for c in 0 .. CHARGE_COUNT { if GearCharges[c].hand { gear_st.gear__on[c] = false } }
} }
# the charge an item carries, -1 none # the charge an item carries, -1 none
@ -15,58 +15,58 @@ export function gear_charge_of(it: int) -> int {
return -1 return -1
} }
export function gear_charge(c: int) -> float { export function gear_charge(gear_st: mut GearState, c: int) -> float {
gear__ensure() gear__ensure(gear_st)
return gear__left[c] return gear_st.gear__left[c]
} }
export function gear_charge_set(c: int, v: float) -> void { export function gear_charge_set(gear_st: mut GearState, c: int, v: float) -> void {
gear__ensure() gear__ensure(gear_st)
gear__left[c] = Math.clamp(v, 0.0, GearCharges[c].full) gear_st.gear__left[c] = Math.clamp(v, 0.0, GearCharges[c].full)
} }
export function gear_charge_fill(c: int) -> void { gear_charge_set(c, GearCharges[c].full) } export function gear_charge_fill(gear_st: mut GearState, c: int) -> void { gear_charge_set(gear_st, c, GearCharges[c].full) }
export function gear_running(c: int) -> bool { export function gear_running(gear_st: mut GearState, c: int) -> bool {
gear__ensure() gear__ensure(gear_st)
return gear__on[c] return gear_st.gear__on[c]
} }
# on or off; false when it has nothing left, or is held and not in the hand # on or off; false when it has nothing left, or is held and not in the hand
export function gear_run(c: int, on: bool) -> bool { export function gear_run(gear_st: mut GearState, c: int, on: bool) -> bool {
gear__ensure() gear__ensure(gear_st)
if not on { if not on {
gear__on[c] = false gear_st.gear__on[c] = false
return true return true
} }
if gear__left[c] < 1.0 { return false } if gear_st.gear__left[c] < 1.0 { return false }
if GearCharges[c].hand and GearCharges[c].item != gear__hand { return false } if GearCharges[c].hand and GearCharges[c].item != gear_st.gear__hand { return false }
gear__on[c] = true gear_st.gear__on[c] = true
return true return true
} }
# the hand's own charge is running (a torch lit, a flashlight on) # the hand's own charge is running (a torch lit, a flashlight on)
export function gear_lit() -> bool { export function gear_lit(gear_st: mut GearState) -> bool {
if gear__hand < 0 { return false } if gear_st.gear__hand < 0 { return false }
let c = gear_charge_of(gear__hand) let c = gear_charge_of(gear_st.gear__hand)
return c >= 0 and GearCharges[c].hand and gear_running(c) return c >= 0 and GearCharges[c].hand and gear_running(gear_st, c)
} }
function gear__rate(c: int) -> float { function gear__rate(gear_st: mut GearState, c: int) -> float {
let d = GearCharges[c] let d = GearCharges[c]
if d.rate >= 0 { return gear_value(d.rate) } if d.rate >= 0 { return gear_value(gear_st, d.rate) }
return d.per_hour return d.per_hour
} }
# the game hours of a tick burn every running charge; one that runs out stops and says so # the game hours of a tick burn every running charge; one that runs out stops and says so
function gear__burn(hours: float) -> void { function gear__burn(base_st: mut BaseState, gear_st: mut GearState, hours: float) -> void {
gear__ensure() gear__ensure(gear_st)
for c in 0 .. CHARGE_COUNT { for c in 0 .. CHARGE_COUNT {
if not gear__on[c] { continue } if not gear_st.gear__on[c] { continue }
gear__left[c] = gear__left[c] - hours * gear__rate(c) gear_st.gear__left[c] = gear_st.gear__left[c] - hours * gear__rate(gear_st, c)
if gear__left[c] > 0.0 { continue } if gear_st.gear__left[c] > 0.0 { continue }
gear__left[c] = 0.0 gear_st.gear__left[c] = 0.0
gear__on[c] = false gear_st.gear__on[c] = false
gear__fact(GEAR_RAN_OUT, -1, c, 0) gear__fact(base_st, gear_st, GEAR_RAN_OUT, -1, c, 0)
} }
} }

View file

@ -1,37 +1,39 @@
# state.ludic - the level of each kind, the hand, each charge's units and whether it runs, the facts # state.ludic - the level of each kind, the hand, each charge's units and whether it runs, the facts
var gear__lv: []int = null export state GearState {
var gear__hand: int = -1 gear__lv: []int = null
var gear__left: []float = null gear__hand: int = -1
var gear__on: []bool = null gear__left: []float = null
var gear__q: Queue<GearFact> = null gear__on: []bool = null
gear__q: Queue<GearFact> = null
}
function gear__ensure() -> void { function gear__ensure(gear_st: mut GearState) -> void {
if gear__lv != null { return } if gear_st.gear__lv != null { return }
gear__lv = new []int gear_st.gear__lv = new []int
for k in 0 .. GEAR_COUNT { push(gear__lv, GearKinds[k].start) } for k in 0 .. GEAR_COUNT { push(gear_st.gear__lv, GearKinds[k].start) }
gear__left = new []float gear_st.gear__left = new []float
gear__on = new []bool gear_st.gear__on = new []bool
for c in 0 .. CHARGE_COUNT { for c in 0 .. CHARGE_COUNT {
push(gear__left, GearCharges[c].full) push(gear_st.gear__left, GearCharges[c].full)
push(gear__on, false) push(gear_st.gear__on, false)
} }
} }
export function gear_facts() -> Queue<GearFact> { export function gear_facts(base_st: mut BaseState, gear_st: mut GearState) -> Queue<GearFact> {
if gear__q == null { gear__q = queue_new("gear.facts") } if gear_st.gear__q == null { gear_st.gear__q = queue_new(base_st, "gear.facts") }
return gear__q return gear_st.gear__q
} }
function gear__fact(what: int, k: int, c: int, cost: int) -> void { function gear__fact(base_st: mut BaseState, gear_st: mut GearState, what: int, k: int, c: int, cost: int) -> void {
let f = new GearFact let f = new GearFact
f.what = what f.what = what
f.cost = cost f.cost = cost
f.kind = k f.kind = k
f.charge = c f.charge = c
if k >= 0 { if k >= 0 {
f.level = gear__lv[k] f.level = gear_st.gear__lv[k]
f.item = GearKinds[k].item f.item = GearKinds[k].item
} }
if c >= 0 { f.item = GearCharges[c].item } if c >= 0 { f.item = GearCharges[c].item }
q_push(gear_facts(), f) q_push(base_st, gear_facts(base_st, gear_st), f)
} }

View file

@ -1,43 +1,43 @@
# system.ludic - gear as a system: the burn each tick, and the levels and charges saved by key (the # system.ludic - gear as a system: the burn each tick, and the levels and charges saved by key (the
# hand is not saved: a trip opens with empty hands) # hand is not saved: a trip opens with empty hands)
export function gear_reset() -> void { export function gear_reset(base_st: mut BaseState, gear_st: mut GearState) -> void {
gear__ensure() gear__ensure(gear_st)
for k in 0 .. GEAR_COUNT { gear__lv[k] = GearKinds[k].start } for k in 0 .. GEAR_COUNT { gear_st.gear__lv[k] = GearKinds[k].start }
for c in 0 .. CHARGE_COUNT { for c in 0 .. CHARGE_COUNT {
gear__left[c] = GearCharges[c].full gear_st.gear__left[c] = GearCharges[c].full
gear__on[c] = false gear_st.gear__on[c] = false
} }
gear__hand = -1 gear_st.gear__hand = -1
q_clear(gear_facts()) q_clear(base_st, gear_facts(base_st, gear_st))
} }
export function gear_save() -> Val { export function gear_save(gear_st: mut GearState) -> Val {
gear__ensure() gear__ensure(gear_st)
let v = Value.object() let v = Value.object()
let lv = Value.object() let lv = Value.object()
for k in 0 .. GEAR_COUNT { sv_put_int(lv, GearKinds[k].key, gear__lv[k]) } for k in 0 .. GEAR_COUNT { sv_put_int(lv, GearKinds[k].key, gear_st.gear__lv[k]) }
Value.put(v, "levels", lv) Value.put(v, "levels", lv)
let ch = Value.object() let ch = Value.object()
for c in 0 .. CHARGE_COUNT { sv_put_float(ch, GearCharges[c].key, gear__left[c]) } for c in 0 .. CHARGE_COUNT { sv_put_float(ch, GearCharges[c].key, gear_st.gear__left[c]) }
Value.put(v, "charges", ch) Value.put(v, "charges", ch)
return v return v
} }
# a kind the file does not name starts where a new trip does; then what the pack carries counts # a kind the file does not name starts where a new trip does; then what the pack carries counts
export function gear_load(v: Val, version: int) -> void { export function gear_load(base_st: mut BaseState, gear_st: mut GearState, v: Val, version: int) -> void {
gear_reset() gear_reset(base_st, gear_st)
if Value.has(v, "levels") != 0 { if Value.has(v, "levels") != 0 {
let lv = Value.get(v, "levels") let lv = Value.get(v, "levels")
for k in 0 .. GEAR_COUNT { gear__lv[k] = sv_int(lv, GearKinds[k].key, GearKinds[k].start) } for k in 0 .. GEAR_COUNT { gear_st.gear__lv[k] = sv_int(lv, GearKinds[k].key, GearKinds[k].start) }
} }
if Value.has(v, "charges") != 0 { if Value.has(v, "charges") != 0 {
let ch = Value.get(v, "charges") let ch = Value.get(v, "charges")
for c in 0 .. CHARGE_COUNT { gear_charge_set(c, sv_float(ch, GearCharges[c].key, GearCharges[c].full)) } for c in 0 .. CHARGE_COUNT { gear_charge_set(gear_st, c, sv_float(ch, GearCharges[c].key, GearCharges[c].full)) }
} }
gear_sync() gear_sync(gear_st)
} }
function gear__tick(t: Tick) -> void { gear__burn(t.hours) } function gear__tick(base_st: mut BaseState, gear_st: mut GearState, t: Tick) -> void { gear__burn(base_st, gear_st, t.hours) }
export function gear_system() -> System { export function gear_system() -> System {
let s = system_new("gear", PH_SIMULATE) let s = system_new("gear", PH_SIMULATE)

View file

@ -17,147 +17,149 @@ program GearTest {
def GearCharges lamp { item: IT_LAMP, full: 100.0, rate: GV_DRAIN } def GearCharges lamp { item: IT_LAMP, full: 100.0, rate: GV_DRAIN }
def GearCharges radio { item: 9, full: 100.0, per_hour: 5.0, hand: false } def GearCharges radio { item: 9, full: 100.0, per_hour: 5.0, hand: false }
var money: int = 0 state GeartestState {
var rep: int = 0 money: int = 0
var pack: []int = null rep: int = 0
var hide_pack: bool = false pack: []int = null
hide_pack: bool = false
}
function purse_money() -> int { return money } function purse_money(geartest_st: GeartestState) -> int { return geartest_st.money }
function purse_spend(n: int) -> bool { function purse_spend(geartest_st: mut GeartestState, n: int) -> bool {
if n > money { return false } if n > geartest_st.money { return false }
money -= n geartest_st.money -= n
return true return true
} }
function purse_rep() -> int { return rep } function purse_rep(geartest_st: GeartestState) -> int { return geartest_st.rep }
function pack_count(it: int) -> int { return pack[it] } function pack_count(geartest_st: GeartestState, it: int) -> int { return geartest_st.pack[it] }
function pack_add(it: int, n: int) -> int { function pack_add(geartest_st: mut GeartestState, it: int, n: int) -> int {
pack[it] = pack[it] + n geartest_st.pack[it] = geartest_st.pack[it] + n
return n return n
} }
function hidden(k: int) -> bool { return hide_pack and k == GEAR_PACK } function hidden(geartest_st: GeartestState, k: int) -> bool { return geartest_st.hide_pack and k == GEAR_PACK }
bind GearPurse { money: fn purse_money, spend: fn purse_spend, rep: fn purse_rep } bind GearPurse { money: fn purse_money, spend: fn purse_spend, rep: fn purse_rep }
bind GearPack { count: fn pack_count, add: fn pack_add } bind GearPack { count: fn pack_count, add: fn pack_add }
bind GearWorld { hidden: fn hidden } bind GearWorld { hidden: fn hidden }
function fresh() -> void { function fresh(base_st: mut BaseState, gear_st: mut GearState, geartest_st: mut GeartestState) -> void {
money = 0 geartest_st.money = 0
rep = 0 geartest_st.rep = 0
hide_pack = false geartest_st.hide_pack = false
pack = new []int geartest_st.pack = new []int
for i in 0 .. 12 { push(pack, 0) } for i in 0 .. 12 { push(geartest_st.pack, 0) }
gear_reset() gear_reset(base_st, gear_st)
} }
function ran_out() -> int { function ran_out(base_st: mut BaseState, gear_st: mut GearState) -> int {
let fs = q_drain(gear_facts()) let fs = q_drain(base_st, gear_facts(base_st, gear_st))
var n = -1 var n = -1
for i in 0 .. len(fs) { if fs[i].what == GEAR_RAN_OUT { n = fs[i].charge } } for i in 0 .. len(fs) { if fs[i].what == GEAR_RAN_OUT { n = fs[i].charge } }
return n return n
} }
test "a new trip starts where the kinds say" { test "a new trip starts where the kinds say" (base_st: mut BaseState, gear_st: mut GearState, geartest_st: mut GeartestState) {
fresh() fresh(base_st, gear_st, geartest_st)
expect_eq(gear_level(GEAR_ROD), 0) expect_eq(gear_level(gear_st, GEAR_ROD), 0)
expect_eq(gear_level(GEAR_CAMERA), 1) expect_eq(gear_level(gear_st, GEAR_CAMERA), 1)
expect(gear_next_name(GEAR_CAMERA) == "Zoom") expect(gear_next_name(gear_st, GEAR_CAMERA) == "Zoom")
expect_eq(gear_next_cost(GEAR_ROD), 30) expect_eq(gear_next_cost(gear_st, GEAR_ROD), 30)
} }
test "bought with money once the standing is there, and the item follows" { test "bought with money once the standing is there, and the item follows" (base_st: mut BaseState, gear_st: mut GearState, geartest_st: mut GeartestState) {
fresh() fresh(base_st, gear_st, geartest_st)
expect(not gear_buy(GEAR_ROD)) expect(not gear_buy(base_st, gear_st, GEAR_ROD))
money = 200 geartest_st.money = 200
expect(gear_buy(GEAR_ROD)) expect(gear_buy(base_st, gear_st, GEAR_ROD))
expect_eq(money, 170) expect_eq(geartest_st.money, 170)
expect_eq(pack[IT_ROD], 1) expect_eq(geartest_st.pack[IT_ROD], 1)
expect(not gear_can_buy(GEAR_ROD)) expect(not gear_can_buy(gear_st, GEAR_ROD))
rep = 10 geartest_st.rep = 10
expect(gear_buy(GEAR_ROD)) expect(gear_buy(base_st, gear_st, GEAR_ROD))
expect_eq(pack[IT_ROD], 1) expect_eq(geartest_st.pack[IT_ROD], 1)
let fs = q_drain(gear_facts()) let fs = q_drain(base_st, gear_facts(base_st, gear_st))
expect_eq(len(fs), 2) expect_eq(len(fs), 2)
expect_eq(fs[1].what, GEAR_BOUGHT) expect_eq(fs[1].what, GEAR_BOUGHT)
expect_eq(fs[1].level, 2) expect_eq(fs[1].level, 2)
expect_eq(fs[1].cost, 100) expect_eq(fs[1].cost, 100)
} }
test "the best tier is the end, and a hidden kind is not for sale" { test "the best tier is the end, and a hidden kind is not for sale" (base_st: mut BaseState, gear_st: mut GearState, geartest_st: mut GeartestState) {
fresh() fresh(base_st, gear_st, geartest_st)
money = 1000 geartest_st.money = 1000
rep = 50 geartest_st.rep = 50
for i in 0 .. 5 { gear_buy(GEAR_PACK) } for i in 0 .. 5 { gear_buy(base_st, gear_st, GEAR_PACK) }
expect(gear_best(GEAR_PACK)) expect(gear_best(gear_st, GEAR_PACK))
expect_eq(gear_level(GEAR_PACK), 3) expect_eq(gear_level(gear_st, GEAR_PACK), 3)
fresh() fresh(base_st, gear_st, geartest_st)
money = 100 geartest_st.money = 100
hide_pack = true geartest_st.hide_pack = true
expect(not gear_buy(GEAR_PACK)) expect(not gear_buy(base_st, gear_st, GEAR_PACK))
expect_eq(len(gear_kinds_at(-1)), 2) expect_eq(len(gear_kinds_at(-1)), 2)
} }
test "a tier decides a value and a mult, and the stack's room follows" { test "a tier decides a value and a mult, and the stack's room follows" (base_st: mut BaseState, gear_st: mut GearState, geartest_st: mut GeartestState) {
fresh() fresh(base_st, gear_st, geartest_st)
expect_eq(gear_value(GV_BITE), 1.0) expect_eq(gear_value(gear_st, GV_BITE), 1.0)
gear_set_level(GEAR_ROD, 3) gear_set_level(gear_st, GEAR_ROD, 3)
expect_eq(gear_value(GV_BITE), 0.55) expect_eq(gear_value(gear_st, GV_BITE), 0.55)
expect_eq(gear_mult(GEAR_PACK), 1.0) expect_eq(gear_mult(gear_st, GEAR_PACK), 1.0)
gear_set_level(GEAR_PACK, 2) gear_set_level(gear_st, GEAR_PACK, 2)
expect_eq(gear_mult(GEAR_PACK), 1.7) expect_eq(gear_mult(gear_st, GEAR_PACK), 1.7)
expect_eq(gear_stack_limit(10, gear_mult(GEAR_PACK)), 17) expect_eq(gear_stack_limit(10, gear_mult(gear_st, GEAR_PACK)), 17)
expect_eq(gear_stack_limit(1, 3.0), 1) expect_eq(gear_stack_limit(1, 3.0), 1)
expect_eq(gear_stack_limit(4, 0.1), 1) expect_eq(gear_stack_limit(4, 0.1), 1)
expect_eq(gear_mult(GEAR_ROD), 1.0) expect_eq(gear_mult(gear_st, GEAR_ROD), 1.0)
} }
test "a carried item counts as the first tier" { test "a carried item counts as the first tier" (base_st: mut BaseState, gear_st: mut GearState, geartest_st: mut GeartestState) {
fresh() fresh(base_st, gear_st, geartest_st)
pack[IT_ROD] = 1 geartest_st.pack[IT_ROD] = 1
gear_sync() gear_sync(gear_st)
expect_eq(gear_level(GEAR_ROD), 1) expect_eq(gear_level(gear_st, GEAR_ROD), 1)
} }
test "a held charge runs only in the hand, burns by the hour and runs out" { test "a held charge runs only in the hand, burns by the hour and runs out" (base_st: mut BaseState, gear_st: mut GearState, geartest_st: mut GeartestState) {
fresh() fresh(base_st, gear_st, geartest_st)
let t = CHARGE_TORCH let t = CHARGE_TORCH
expect(not gear_run(t, true)) expect(not gear_run(gear_st, t, true))
gear_set_hand(IT_TORCH) gear_set_hand(gear_st, IT_TORCH)
expect(gear_run(t, true)) expect(gear_run(gear_st, t, true))
expect(gear_lit()) expect(gear_lit(gear_st))
gear_system().tick(tick_new(0.1, 1, 0.5)) gear_system().tick(tick_new(0.1, 1, 0.5))
expect_eq(gear_charge(t), 30.0) expect_eq(gear_charge(gear_st, t), 30.0)
gear_system().tick(tick_new(0.1, 2, 0.6)) gear_system().tick(tick_new(0.1, 2, 0.6))
expect_eq(gear_charge(t), 0.0) expect_eq(gear_charge(gear_st, t), 0.0)
expect(not gear_lit()) expect(not gear_lit(gear_st))
expect_eq(ran_out(), t) expect_eq(ran_out(base_st, gear_st), t)
expect(not gear_run(t, true)) expect(not gear_run(gear_st, t, true))
} }
test "a charge's rate can be its tier's, and a new hand puts the light out" { test "a charge's rate can be its tier's, and a new hand puts the light out" (base_st: mut BaseState, gear_st: mut GearState, geartest_st: mut GeartestState) {
fresh() fresh(base_st, gear_st, geartest_st)
gear_set_hand(IT_LAMP) gear_set_hand(gear_st, IT_LAMP)
gear_run(CHARGE_LAMP, true) gear_run(gear_st, CHARGE_LAMP, true)
gear_system().tick(tick_new(0.1, 1, 1.0)) gear_system().tick(tick_new(0.1, 1, 1.0))
expect_eq(gear_charge(CHARGE_LAMP), 86.0) expect_eq(gear_charge(gear_st, CHARGE_LAMP), 86.0)
gear_set_level(GEAR_CAMERA, 2) gear_set_level(gear_st, GEAR_CAMERA, 2)
gear_system().tick(tick_new(0.1, 2, 1.0)) gear_system().tick(tick_new(0.1, 2, 1.0))
expect_eq(gear_charge(CHARGE_LAMP), 78.0) expect_eq(gear_charge(gear_st, CHARGE_LAMP), 78.0)
gear_run(CHARGE_RADIO, true) gear_run(gear_st, CHARGE_RADIO, true)
gear_set_hand(-1) gear_set_hand(gear_st, -1)
expect(not gear_running(CHARGE_LAMP)) expect(not gear_running(gear_st, CHARGE_LAMP))
expect(gear_running(CHARGE_RADIO)) expect(gear_running(gear_st, CHARGE_RADIO))
} }
test "levels and charges are saved by key" { test "levels and charges are saved by key" (base_st: mut BaseState, gear_st: mut GearState, geartest_st: mut GeartestState) {
fresh() fresh(base_st, gear_st, geartest_st)
gear_set_level(GEAR_ROD, 2) gear_set_level(gear_st, GEAR_ROD, 2)
gear_charge_set(CHARGE_TORCH, 12.5) gear_charge_set(gear_st, CHARGE_TORCH, 12.5)
let v = gear_save() let v = gear_save(gear_st)
fresh() fresh(base_st, gear_st, geartest_st)
gear_load(v, 1) gear_load(base_st, gear_st, v, 1)
expect_eq(gear_level(GEAR_ROD), 2) expect_eq(gear_level(gear_st, GEAR_ROD), 2)
expect_eq(gear_level(GEAR_CAMERA), 1) expect_eq(gear_level(gear_st, GEAR_CAMERA), 1)
expect_eq(gear_charge(CHARGE_TORCH), 12.5) expect_eq(gear_charge(gear_st, CHARGE_TORCH), 12.5)
expect_eq(gear_charge(CHARGE_LAMP), 100.0) expect_eq(gear_charge(gear_st, CHARGE_LAMP), 100.0)
} }
} }

View file

@ -1,20 +1,20 @@
# tiers.ludic - what is owned and what a tier is worth # tiers.ludic - what is owned and what a tier is worth
export function gear_ok(k: int) -> bool { return k >= 0 and k < GEAR_COUNT } export function gear_ok(k: int) -> bool { return k >= 0 and k < GEAR_COUNT }
export function gear_level(k: int) -> int { export function gear_level(gear_st: mut GearState, k: int) -> int {
gear__ensure() gear__ensure(gear_st)
return gear__lv[k] return gear_st.gear__lv[k]
} }
# outright: staging, a test, a camera handed over with the trip # outright: staging, a test, a camera handed over with the trip
export function gear_set_level(k: int, lv: int) -> void { export function gear_set_level(gear_st: mut GearState, k: int, lv: int) -> void {
gear__ensure() gear__ensure(gear_st)
gear__lv[k] = lv gear_st.gear__lv[k] = lv
} }
export function gear_owned(k: int) -> bool { return gear_level(k) > 0 } export function gear_owned(gear_st: mut GearState, k: int) -> bool { return gear_level(gear_st, k) > 0 }
export function gear_best(k: int) -> bool { return gear_level(k) >= GearKinds[k].tiers } export function gear_best(gear_st: mut GearState, k: int) -> bool { return gear_level(gear_st, k) >= GearKinds[k].tiers }
# a tier's name, "" for none # a tier's name, "" for none
export function gear_level_name(k: int, lv: int) -> string { export function gear_level_name(k: int, lv: int) -> string {
@ -23,16 +23,16 @@ export function gear_level_name(k: int, lv: int) -> string {
} }
# the tier on offer next: its name, cost and the standing it wants # the tier on offer next: its name, cost and the standing it wants
export function gear_next_name(k: int) -> string { return gear_level_name(k, gear_level(k) + 1) } export function gear_next_name(gear_st: mut GearState, k: int) -> string { return gear_level_name(k, gear_level(gear_st, k) + 1) }
export function gear_next_cost(k: int) -> int { export function gear_next_cost(gear_st: mut GearState, k: int) -> int {
if gear_best(k) { return 0 } if gear_best(gear_st, k) { return 0 }
return GearKinds[k].cost[gear_level(k)] return GearKinds[k].cost[gear_level(gear_st, k)]
} }
export function gear_next_rep(k: int) -> int { export function gear_next_rep(gear_st: mut GearState, k: int) -> int {
if gear_best(k) { return 0 } if gear_best(gear_st, k) { return 0 }
return GearKinds[k].rep[gear_level(k)] return GearKinds[k].rep[gear_level(gear_st, k)]
} }
function gear__pick(xs: []float, lv: int, fallback: float) -> float { function gear__pick(xs: []float, lv: int, fallback: float) -> float {
@ -43,12 +43,12 @@ function gear__pick(xs: []float, lv: int, fallback: float) -> float {
} }
# the kind's own number at its level (1.0 when it has none) # the kind's own number at its level (1.0 when it has none)
export function gear_mult(k: int) -> float { return gear__pick(GearKinds[k].mult, gear_level(k), 1.0) } export function gear_mult(gear_st: mut GearState, k: int) -> float { return gear__pick(GearKinds[k].mult, gear_level(gear_st, k), 1.0) }
# a value its kind's tier decides # a value its kind's tier decides
export function gear_value(v: int) -> float { export function gear_value(gear_st: mut GearState, v: int) -> float {
let d = GearValues[v] let d = GearValues[v]
return gear__pick(d.at, gear_level(d.gear), 0.0) return gear__pick(d.at, gear_level(gear_st, d.gear), 0.0)
} }
# the room for a thing whose base stack is `base`, scaled: a single thing stays single, and a # the room for a thing whose base stack is `base`, scaled: a single thing stays single, and a

View file

@ -1,45 +1,45 @@
# card.ludic - a card read off the rail: the key opens the picked chip and puts it down, the arrows # card.ludic - a card read off the rail: the key opens the picked chip and puts it down, the arrows
# walk the rail with the card up, and "I know this" mutes the card being read # walk the rail with the card up, and "I know this" mutes the card being read
export function hints_card() -> int { return hn_card } export function hints_card(hints_st: HintsState) -> int { return hints_st.hn_card }
# the key: the picked card opened, or the open one put down # the key: the picked card opened, or the open one put down
export function hints_key() -> void { export function hints_key(base_st: mut BaseState, hints_st: mut HintsState) -> void {
if hn_card >= 0 { if hints_st.hn_card >= 0 {
hints_close() hints_close(base_st, hints_st)
return return
} }
hints_open(hn_pick) hints_open(base_st, hints_st, hints_st.hn_pick)
} }
# the k-th chip on the rail opened as a card # the k-th chip on the rail opened as a card
export function hints_open(k: int) -> void { export function hints_open(base_st: mut BaseState, hints_st: mut HintsState, k: int) -> void {
if k < 0 or k >= hn_n { return } if k < 0 or k >= hints_st.hn_n { return }
hn_pick = k hints_st.hn_pick = k
hn_card = hn_top[k] hints_st.hn_card = hints_st.hn_top[k]
hn_fact(HINTS_F_OPENED, hn_card) hn_fact(base_st, hints_st, HINTS_F_OPENED, hints_st.hn_card)
} }
export function hints_close() -> void { export function hints_close(base_st: mut BaseState, hints_st: mut HintsState) -> void {
if hn_card < 0 { return } if hints_st.hn_card < 0 { return }
let c = hn_card let c = hints_st.hn_card
hn_card = -1 hints_st.hn_card = -1
hn_fact(HINTS_F_CLOSED, c) hn_fact(base_st, hints_st, HINTS_F_CLOSED, c)
} }
# the arrows, with a card up: the next chip along (d = 1) or the one before, round the ends # the arrows, with a card up: the next chip along (d = 1) or the one before, round the ends
export function hints_move(d: int) -> void { export function hints_move(base_st: mut BaseState, hints_st: mut HintsState, d: int) -> void {
if hn_card < 0 or hn_n <= 1 { return } if hints_st.hn_card < 0 or hints_st.hn_n <= 1 { return }
hints_open((hn_pick + hn_n + d) % hn_n) hints_open(base_st, hints_st, (hints_st.hn_pick + hints_st.hn_n + d) % hints_st.hn_n)
} }
# "I know this - do not show it again": the card off the rail until the mutes are given back # "I know this - do not show it again": the card off the rail until the mutes are given back
export function hints_mute_card() -> void { export function hints_mute_card(base_st: mut BaseState, hints_st: mut HintsState) -> void {
if hn_card < 0 { return } if hints_st.hn_card < 0 { return }
let c = hn_card let c = hints_st.hn_card
hints_mute(c, true) hints_mute(hints_st, c, true)
hn_card = -1 hints_st.hn_card = -1
hn_t = 0.0 hints_st.hn_t = 0.0
hn_fact(HINTS_F_MUTED, c) hn_fact(base_st, hints_st, HINTS_F_MUTED, c)
} }
# a step's words and its picture (n 1..3): the glyph's name, or the item (-1 when it is a glyph) # a step's words and its picture (n 1..3): the glyph's name, or the item (-1 when it is a glyph)

View file

@ -1,62 +1,60 @@
# mute.ludic - the cards a player has said they know, kept BY KEY: a card added, removed or moved in # mute.ludic - the cards a player has said they know, kept BY KEY: a card added, removed or moved in
# the registry leaves every other mute where it was, and a key this build does not know is carried # the registry leaves every other mute where it was, and a key this build does not know is carried
var hn_mute: []bool = null
var hn_foreign: []string = null # muted keys with no card in this build, written back as read
function hn_mute_ensure() -> void { function hn_mute_ensure(hints_st: mut HintsState) -> void {
if hn_mute != null { return } if hints_st.hn_mute != null { return }
hn_mute = new []bool hints_st.hn_mute = new []bool
for i in 0 .. HINT_COUNT { push(hn_mute, false) } for i in 0 .. HINT_COUNT { push(hints_st.hn_mute, false) }
hn_foreign = new []string hints_st.hn_foreign = new []string
} }
export function hints_muted(id: int) -> bool { export function hints_muted(hints_st: mut HintsState, id: int) -> bool {
hn_mute_ensure() hn_mute_ensure(hints_st)
if id < 0 or id >= HINT_COUNT { return false } if id < 0 or id >= HINT_COUNT { return false }
return hn_mute[id] return hints_st.hn_mute[id]
} }
export function hints_mute(id: int, on: bool) -> void { export function hints_mute(hints_st: mut HintsState, id: int, on: bool) -> void {
hn_mute_ensure() hn_mute_ensure(hints_st)
if id < 0 or id >= HINT_COUNT { return } if id < 0 or id >= HINT_COUNT { return }
hn_mute[id] = on hints_st.hn_mute[id] = on
} }
export function hints_mute_count() -> int { export function hints_mute_count(hints_st: mut HintsState) -> int {
hn_mute_ensure() hn_mute_ensure(hints_st)
var n = 0 var n = 0
for i in 0 .. HINT_COUNT { if hn_mute[i] { n += 1 } } for i in 0 .. HINT_COUNT { if hints_st.hn_mute[i] { n += 1 } }
return n return n
} }
# every muted card given back, from the next pass # every muted card given back, from the next pass
export function hints_unmute_all() -> void { export function hints_unmute_all(base_st: mut BaseState, hints_st: mut HintsState) -> void {
hn_mute_ensure() hn_mute_ensure(hints_st)
for i in 0 .. HINT_COUNT { hn_mute[i] = false } for i in 0 .. HINT_COUNT { hints_st.hn_mute[i] = false }
hn_foreign = new []string hints_st.hn_foreign = new []string
hn_t = 0.0 hints_st.hn_t = 0.0
hn_fact(HINTS_F_UNMUTED, -1) hn_fact(base_st, hints_st, HINTS_F_UNMUTED, -1)
} }
# the mutes as a list of keys, for wherever the game keeps the player's own choices # the mutes as a list of keys, for wherever the game keeps the player's own choices
export function hints_mutes_save() -> Val { export function hints_mutes_save(hints_st: mut HintsState) -> Val {
hn_mute_ensure() hn_mute_ensure(hints_st)
let l = Value.list() let l = Value.list()
for i in 0 .. HINT_COUNT { if hn_mute[i] { Value.add(l, Value.str(Hints[i].key)) } } for i in 0 .. HINT_COUNT { if hints_st.hn_mute[i] { Value.add(l, Value.str(Hints[i].key)) } }
for i in 0 .. len(hn_foreign) { Value.add(l, Value.str(hn_foreign[i])) } for i in 0 .. len(hints_st.hn_foreign) { Value.add(l, Value.str(hints_st.hn_foreign[i])) }
return l return l
} }
# a list of keys read back; anything that is not a list is no mutes # a list of keys read back; anything that is not a list is no mutes
export function hints_mutes_load(l: Val) -> void { export function hints_mutes_load(hints_st: mut HintsState, l: Val) -> void {
hn_mute = null hints_st.hn_mute = null
hn_mute_ensure() hn_mute_ensure(hints_st)
if l == null or Value.kind(l) != 5 { return } if l == null or Value.kind(l) != 5 { return }
for i in 0 .. Value.count(l) { for i in 0 .. Value.count(l) {
let e = Value.at(l, i) let e = Value.at(l, i)
if Value.kind(e) != 4 { continue } if Value.kind(e) != 4 { continue }
let k = Value.as_str(e) let k = Value.as_str(e)
let id = hints_find(k) let id = hints_find(k)
if id >= 0 { hn_mute[id] = true } else { push(hn_foreign, k) } if id >= 0 { hints_st.hn_mute[id] = true } else { push(hints_st.hn_foreign, k) }
} }
} }

View file

@ -1,102 +1,106 @@
# rail.ludic - the rail: once a pass (a second by default) the most urgent true cards, up to a few, # rail.ludic - the rail: once a pass (a second by default) the most urgent true cards, up to a few,
# muted ones left out; in a tie the registry's order decides # muted ones left out; in a tie the registry's order decides
var hn_shown: int = 3 # how many the rail holds export state HintsState {
var hn_every: float = 1.0 # seconds between passes hn_mute: []bool = null
var hn_top: []int = null # the cards on the rail this pass hn_foreign: []string = null # muted keys with no card in this build, written back as read
var hn_n: int = 0 hn_shown: int = 3 # how many the rail holds
var hn_t: float = 0.0 # seconds to the next pass hn_every: float = 1.0 # seconds between passes
var hn_pick: int = 0 # which chip the key would open hn_top: []int = null # the cards on the rail this pass
var hn_card: int = -1 # the card being read, or -1 hn_n: int = 0
var hn_facts: Queue<HintsFact> = null hn_t: float = 0.0 # seconds to the next pass
hn_pick: int = 0 # which chip the key would open
export function hints_facts() -> Queue<HintsFact> { hn_card: int = -1 # the card being read, or -1
if hn_facts == null { hn_facts = queue_new("hints.facts") } hn_facts: Queue<HintsFact> = null
return hn_facts
} }
function hn_fact(what: int, card: int) -> void { export function hints_facts(base_st: mut BaseState, hints_st: mut HintsState) -> Queue<HintsFact> {
if hints_st.hn_facts == null { hints_st.hn_facts = queue_new(base_st, "hints.facts") }
return hints_st.hn_facts
}
function hn_fact(base_st: mut BaseState, hints_st: mut HintsState, what: int, card: int) -> void {
let f = new HintsFact let f = new HintsFact
f.what = what f.what = what
f.card = card f.card = card
q_push(hints_facts(), f) q_push(base_st, hints_facts(base_st, hints_st), f)
} }
# how many the rail holds and how often it is sorted # how many the rail holds and how often it is sorted
export function hints_config(shown: int, every: float) -> void { export function hints_config(hints_st: mut HintsState, shown: int, every: float) -> void {
hn_shown = shown hints_st.hn_shown = shown
hn_every = every hints_st.hn_every = every
hn_top = null hints_st.hn_top = null
} }
function hn_ensure() -> void { function hn_ensure(hints_st: mut HintsState) -> void {
if hn_top == null or len(hn_top) != hn_shown { if hints_st.hn_top == null or len(hints_st.hn_top) != hints_st.hn_shown {
hn_top = new []int hints_st.hn_top = new []int
for i in 0 .. hn_shown { push(hn_top, -1) } for i in 0 .. hints_st.hn_shown { push(hints_st.hn_top, -1) }
} }
hn_mute_ensure() hn_mute_ensure(hints_st)
} }
# the next pass comes on the next tick rather than in a second # the next pass comes on the next tick rather than in a second
export function hints_refresh() -> void { hn_t = 0.0 } export function hints_refresh(hints_st: mut HintsState) -> void { hints_st.hn_t = 0.0 }
export function hints_tick(dt: float) -> void { export function hints_tick(hints_st: mut HintsState, dt: float) -> void {
hn_ensure() hn_ensure(hints_st)
hn_t = hn_t - dt hints_st.hn_t = hints_st.hn_t - dt
if hn_t > 0.0 { return } if hints_st.hn_t > 0.0 { return }
hn_t = hn_every hints_st.hn_t = hints_st.hn_every
hints_sort() hints_sort(hints_st)
} }
# one pass now: the rail re-sorted, the pick kept in range, and the card following its subject # one pass now: the rail re-sorted, the pick kept in range, and the card following its subject
# along the rail - or put down when what it was about stopped being true # along the rail - or put down when what it was about stopped being true
export function hints_sort() -> void { export function hints_sort(hints_st: mut HintsState) -> void {
hn_ensure() hn_ensure(hints_st)
hn_n = 0 hints_st.hn_n = 0
let on = HintsWorld.showing() let on = HintsWorld.showing()
for u in 0 .. 4 { for u in 0 .. 4 {
let want = HINTS_URGENT - u let want = HINTS_URGENT - u
for id in 0 .. HINT_COUNT { for id in 0 .. HINT_COUNT {
if not on or hn_n >= hn_shown { break } if not on or hints_st.hn_n >= hints_st.hn_shown { break }
if Hints[id].urgency != want or hints_muted(id) { continue } if Hints[id].urgency != want or hints_muted(hints_st, id) { continue }
if not HintsWorld.true_now(id) { continue } if not HintsWorld.true_now(id) { continue }
hn_top[hn_n] = id hints_st.hn_top[hints_st.hn_n] = id
hn_n += 1 hints_st.hn_n += 1
} }
} }
if hn_pick >= hn_n { hn_pick = 0 } if hints_st.hn_pick >= hints_st.hn_n { hints_st.hn_pick = 0 }
if hn_card < 0 { return } if hints_st.hn_card < 0 { return }
if not on or hints_muted(hn_card) or not HintsWorld.true_now(hn_card) { if not on or hints_muted(hints_st, hints_st.hn_card) or not HintsWorld.true_now(hints_st.hn_card) {
hn_card = -1 hints_st.hn_card = -1
return return
} }
for k in 0 .. hn_n { if hn_top[k] == hn_card { hn_pick = k } } for k in 0 .. hints_st.hn_n { if hints_st.hn_top[k] == hints_st.hn_card { hints_st.hn_pick = k } }
} }
# the cards on the rail, most urgent first (empty while the rail is not showing) # the cards on the rail, most urgent first (empty while the rail is not showing)
export function hints_rail() -> []int { export function hints_rail(hints_st: mut HintsState) -> []int {
hn_ensure() hn_ensure(hints_st)
let out = new []int let out = new []int
if not HintsWorld.showing() { return out } if not HintsWorld.showing() { return out }
for k in 0 .. hn_n { push(out, hn_top[k]) } for k in 0 .. hints_st.hn_n { push(out, hints_st.hn_top[k]) }
return out return out
} }
export function hints_count() -> int { return hn_n } export function hints_count(hints_st: HintsState) -> int { return hints_st.hn_n }
export function hints_at(k: int) -> int { export function hints_at(hints_st: HintsState, k: int) -> int {
if k < 0 or k >= hn_n { return -1 } if k < 0 or k >= hints_st.hn_n { return -1 }
return hn_top[k] return hints_st.hn_top[k]
} }
export function hints_pick() -> int { return hn_pick } export function hints_pick(hints_st: HintsState) -> int { return hints_st.hn_pick }
export function hints_hot(k: int) -> bool { return k == hn_pick } export function hints_hot(hints_st: HintsState, k: int) -> bool { return k == hints_st.hn_pick }
export function hints_reset() -> void { export function hints_reset(base_st: mut BaseState, hints_st: mut HintsState) -> void {
hn_top = null hints_st.hn_top = null
hn_n = 0 hints_st.hn_n = 0
hn_t = 0.0 hints_st.hn_t = 0.0
hn_pick = 0 hints_st.hn_pick = 0
hn_card = -1 hints_st.hn_card = -1
q_clear(hints_facts()) q_clear(base_st, hints_facts(base_st, hints_st))
} }

View file

@ -1,6 +1,6 @@
# system.ludic - the rail as a system in PH_PRESENT: it only reads the world, and a trip saves # system.ludic - the rail as a system in PH_PRESENT: it only reads the world, and a trip saves
# nothing of it (the mutes are the player's, kept wherever the game keeps its settings) # nothing of it (the mutes are the player's, kept wherever the game keeps its settings)
function hn_tick(t: Tick) -> void { hints_tick(t.dt) } function hn_tick(hints_st: mut HintsState, t: Tick) -> void { hints_tick(hints_st, t.dt) }
export function hints_system() -> System { export function hints_system() -> System {
let s = system_new("hints", PH_PRESENT) let s = system_new("hints", PH_PRESENT)

View file

@ -11,24 +11,26 @@ program HintsTest {
def Hints night { urgency: HINTS_TIP, line: "Night soon" } def Hints night { urgency: HINTS_TIP, line: "Night soon" }
def Hints from "cards.lres" def Hints from "cards.lres"
var on: []bool = null state HintstestState {
var showing: bool = true on: []bool = null
showing: bool = true
function fake_true(i: int) -> bool { return on[i] }
function fake_showing() -> bool { return showing }
bind HintsWorld { true_now: fn fake_true, showing: fn fake_showing }
function fresh() -> void {
on = new []bool
for i in 0 .. HINT_COUNT { push(on, false) }
showing = true
hints_config(3, 1.0)
hints_reset()
hints_mutes_load(Value.list())
} }
function count(what: int) -> int { function fake_true(hintstest_st: HintstestState, i: int) -> bool { return hintstest_st.on[i] }
let fs = q_drain(hints_facts()) function fake_showing(hintstest_st: HintstestState) -> bool { return hintstest_st.showing }
bind HintsWorld { true_now: fn fake_true, showing: fn fake_showing }
function fresh(base_st: mut BaseState, hints_st: mut HintsState, hintstest_st: mut HintstestState) -> void {
hintstest_st.on = new []bool
for i in 0 .. HINT_COUNT { push(hintstest_st.on, false) }
hintstest_st.showing = true
hints_config(hints_st, 3, 1.0)
hints_reset(base_st, hints_st)
hints_mutes_load(hints_st, Value.list())
}
function count(base_st: mut BaseState, hints_st: mut HintsState, what: int) -> int {
let fs = q_drain(base_st, hints_facts(base_st, hints_st))
var n = 0 var n = 0
for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } } for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } }
return n return n
@ -44,119 +46,119 @@ program HintsTest {
expect(hints_step_text(HINT_THIRSTY, 1) == "Drink at a shore") expect(hints_step_text(HINT_THIRSTY, 1) == "Drink at a shore")
} }
test "the most urgent true cards, capped, ties in registry order" { test "the most urgent true cards, capped, ties in registry order" (base_st: mut BaseState, hints_st: mut HintsState, hintstest_st: mut HintstestState) {
fresh() fresh(base_st, hints_st, hintstest_st)
for i in 0 .. HINT_COUNT { on[i] = true } for i in 0 .. HINT_COUNT { hintstest_st.on[i] = true }
hints_tick(1.0) hints_tick(hints_st, 1.0)
expect_eq(hints_count(), 3) expect_eq(hints_count(hints_st), 3)
expect_eq(hints_at(0), HINT_PARCHED) expect_eq(hints_at(hints_st, 0), HINT_PARCHED)
expect_eq(hints_at(1), HINT_BEAR) expect_eq(hints_at(hints_st, 1), HINT_BEAR)
expect_eq(hints_at(2), HINT_COLD) expect_eq(hints_at(hints_st, 2), HINT_COLD)
on[HINT_PARCHED] = false hintstest_st.on[HINT_PARCHED] = false
on[HINT_BEAR] = false hintstest_st.on[HINT_BEAR] = false
hints_tick(0.5) hints_tick(hints_st, 0.5)
expect_eq(hints_at(0), HINT_PARCHED) # not a second yet: the rail holds expect_eq(hints_at(hints_st, 0), HINT_PARCHED) # not a second yet: the rail holds
hints_tick(0.6) hints_tick(hints_st, 0.6)
expect_eq(hints_at(0), HINT_COLD) expect_eq(hints_at(hints_st, 0), HINT_COLD)
expect_eq(hints_at(1), HINT_THIRSTY) expect_eq(hints_at(hints_st, 1), HINT_THIRSTY)
expect_eq(hints_at(2), HINT_FIRE) expect_eq(hints_at(hints_st, 2), HINT_FIRE)
} }
test "nothing on the rail while it is not showing" { test "nothing on the rail while it is not showing" (base_st: mut BaseState, hints_st: mut HintsState, hintstest_st: mut HintstestState) {
fresh() fresh(base_st, hints_st, hintstest_st)
on[HINT_COLD] = true hintstest_st.on[HINT_COLD] = true
showing = false hintstest_st.showing = false
hints_sort() hints_sort(hints_st)
expect_eq(len(hints_rail()), 0) expect_eq(len(hints_rail(hints_st)), 0)
expect_eq(hints_count(), 0) expect_eq(hints_count(hints_st), 0)
showing = true hintstest_st.showing = true
hints_sort() hints_sort(hints_st)
expect_eq(len(hints_rail()), 1) expect_eq(len(hints_rail(hints_st)), 1)
} }
test "the key opens the picked chip, the arrows walk round, the key puts it down" { test "the key opens the picked chip, the arrows walk round, the key puts it down" (base_st: mut BaseState, hints_st: mut HintsState, hintstest_st: mut HintstestState) {
fresh() fresh(base_st, hints_st, hintstest_st)
on[HINT_COLD] = true hintstest_st.on[HINT_COLD] = true
on[HINT_THIRSTY] = true hintstest_st.on[HINT_THIRSTY] = true
on[HINT_RAIN] = true hintstest_st.on[HINT_RAIN] = true
hints_sort() hints_sort(hints_st)
hints_key() hints_key(base_st, hints_st)
expect_eq(hints_card(), HINT_COLD) expect_eq(hints_card(hints_st), HINT_COLD)
hints_move(1) hints_move(base_st, hints_st, 1)
expect_eq(hints_card(), HINT_THIRSTY) expect_eq(hints_card(hints_st), HINT_THIRSTY)
expect(hints_hot(1)) expect(hints_hot(hints_st, 1))
hints_move(-1) hints_move(base_st, hints_st, -1)
hints_move(-1) hints_move(base_st, hints_st, -1)
expect_eq(hints_card(), HINT_RAIN) expect_eq(hints_card(hints_st), HINT_RAIN)
hints_key() hints_key(base_st, hints_st)
expect_eq(hints_card(), -1) expect_eq(hints_card(hints_st), -1)
expect_eq(count(HINTS_F_OPENED), 4) expect_eq(count(base_st, hints_st, HINTS_F_OPENED), 4)
} }
test "the card follows its subject as the rail re-sorts, and goes when it is no longer true" { test "the card follows its subject as the rail re-sorts, and goes when it is no longer true" (base_st: mut BaseState, hints_st: mut HintsState, hintstest_st: mut HintstestState) {
fresh() fresh(base_st, hints_st, hintstest_st)
on[HINT_THIRSTY] = true hintstest_st.on[HINT_THIRSTY] = true
on[HINT_RAIN] = true hintstest_st.on[HINT_RAIN] = true
hints_sort() hints_sort(hints_st)
hints_open(1) hints_open(base_st, hints_st, 1)
expect_eq(hints_card(), HINT_RAIN) expect_eq(hints_card(hints_st), HINT_RAIN)
on[HINT_COLD] = true hintstest_st.on[HINT_COLD] = true
hints_sort() hints_sort(hints_st)
expect_eq(hints_card(), HINT_RAIN) expect_eq(hints_card(hints_st), HINT_RAIN)
expect_eq(hints_pick(), 2) expect_eq(hints_pick(hints_st), 2)
on[HINT_RAIN] = false hintstest_st.on[HINT_RAIN] = false
hints_sort() hints_sort(hints_st)
expect_eq(hints_card(), -1) expect_eq(hints_card(hints_st), -1)
expect_eq(count(HINTS_F_CLOSED), 0) # it went; nobody put it down expect_eq(count(base_st, hints_st, HINTS_F_CLOSED), 0) # it went; nobody put it down
} }
test "I know this: muted off the rail, counted, given back" { test "I know this: muted off the rail, counted, given back" (base_st: mut BaseState, hints_st: mut HintsState, hintstest_st: mut HintstestState) {
fresh() fresh(base_st, hints_st, hintstest_st)
on[HINT_COLD] = true hintstest_st.on[HINT_COLD] = true
on[HINT_THIRSTY] = true hintstest_st.on[HINT_THIRSTY] = true
hints_sort() hints_sort(hints_st)
hints_key() hints_key(base_st, hints_st)
hints_mute_card() hints_mute_card(base_st, hints_st)
expect_eq(hints_card(), -1) expect_eq(hints_card(hints_st), -1)
expect(hints_muted(HINT_COLD)) expect(hints_muted(hints_st, HINT_COLD))
expect_eq(hints_mute_count(), 1) expect_eq(hints_mute_count(hints_st), 1)
expect_eq(count(HINTS_F_MUTED), 1) expect_eq(count(base_st, hints_st, HINTS_F_MUTED), 1)
hints_tick(0.0) # the next pass comes at once hints_tick(hints_st, 0.0) # the next pass comes at once
expect_eq(hints_at(0), HINT_THIRSTY) expect_eq(hints_at(hints_st, 0), HINT_THIRSTY)
expect_eq(hints_count(), 1) expect_eq(hints_count(hints_st), 1)
hints_unmute_all() hints_unmute_all(base_st, hints_st)
hints_tick(0.0) hints_tick(hints_st, 0.0)
expect_eq(hints_at(0), HINT_COLD) expect_eq(hints_at(hints_st, 0), HINT_COLD)
expect_eq(count(HINTS_F_UNMUTED), 1) expect_eq(count(base_st, hints_st, HINTS_F_UNMUTED), 1)
} }
test "mutes are kept by key, and a key this build lacks is carried through" { test "mutes are kept by key, and a key this build lacks is carried through" (base_st: mut BaseState, hints_st: mut HintsState, hintstest_st: mut HintstestState) {
fresh() fresh(base_st, hints_st, hintstest_st)
hints_mute(HINT_NIGHT, true) hints_mute(hints_st, HINT_NIGHT, true)
hints_mute(HINT_BEAR, true) hints_mute(hints_st, HINT_BEAR, true)
let l = hints_mutes_save() let l = hints_mutes_save(hints_st)
expect_eq(Value.count(l), 2) expect_eq(Value.count(l), 2)
Value.add(l, Value.str("gone_card")) Value.add(l, Value.str("gone_card"))
Value.add(l, Value.int(3)) # not a key: ignored Value.add(l, Value.int(3)) # not a key: ignored
hints_mutes_load(l) hints_mutes_load(hints_st, l)
expect(hints_muted(HINT_NIGHT)) expect(hints_muted(hints_st, HINT_NIGHT))
expect(hints_muted(HINT_BEAR)) expect(hints_muted(hints_st, HINT_BEAR))
expect(not hints_muted(HINT_RAIN)) expect(not hints_muted(hints_st, HINT_RAIN))
expect_eq(hints_mute_count(), 2) expect_eq(hints_mute_count(hints_st), 2)
let back = hints_mutes_save() let back = hints_mutes_save(hints_st)
expect_eq(Value.count(back), 3) expect_eq(Value.count(back), 3)
expect(Value.as_str(Value.at(back, 2)) == "gone_card") expect(Value.as_str(Value.at(back, 2)) == "gone_card")
hints_mutes_load(null) hints_mutes_load(hints_st, null)
expect_eq(hints_mute_count(), 0) expect_eq(hints_mute_count(hints_st), 0)
} }
test "as a system it ticks the rail" { test "as a system it ticks the rail" (base_st: mut BaseState, hints_st: mut HintsState, hintstest_st: mut HintstestState) {
fresh() fresh(base_st, hints_st, hintstest_st)
core_clear() core_clear(base_st)
core_add(hints_system()) core_add(base_st, hints_system())
core_reset_all() core_reset_all(base_st)
on[HINT_FIRE] = true hintstest_st.on[HINT_FIRE] = true
core_tick_all(tick_new(1.0, 1, 0.0)) core_tick_all(base_st, tick_new(1.0, 1, 0.0))
expect_eq(hints_at(0), HINT_FIRE) expect_eq(hints_at(hints_st, 0), HINT_FIRE)
} }
} }

View file

@ -1,18 +1,18 @@
# langs.ludic - where languages come from, later ones replacing earlier ones with the same code: # langs.ludic - where languages come from, later ones replacing earlier ones with the same code:
# <lang_dir>/languages.txt (one code per line, # a comment) and every .po in the mod folder # <lang_dir>/languages.txt (one code per line, # a comment) and every .po in the mod folder
export function i18n_init() -> void { export function i18n_init(i18n_st: mut I18nState) -> void {
codes = new []string i18n_st.codes = new []string
names = new []string i18n_st.names = new []string
paths = new []string i18n_st.paths = new []string
fonts = new []string i18n_st.fonts = new []string
lang_add("en", "English", "", "") lang_add(i18n_st, "en", "English", "", "")
let idx = Fs.read_text(lang_dir + "/languages.txt") let idx = Fs.read_text(i18n_st.lang_dir + "/languages.txt")
if idx != null { if idx != null {
let ls = lines_of(idx) let ls = lines_of(idx)
for i in 0 .. len(ls) { for i in 0 .. len(ls) {
let ln = trim(ls[i]) let ln = trim(ls[i])
if len(ln) == 0 or ln[0] == 35 { continue } if len(ln) == 0 or ln[0] == 35 { continue }
probe(ln, `{lang_dir}/{ln}.po`, lang_dir) probe(i18n_st, ln, `{i18n_st.lang_dir}/{ln}.po`, i18n_st.lang_dir)
} }
} }
let md = i18n_mod_dir() let md = i18n_mod_dir()
@ -23,60 +23,60 @@ export function i18n_init() -> void {
for i in 0 .. len(files) { for i in 0 .. len(files) {
let nm = files[i] let nm = files[i]
let n = len(nm) let n = len(nm)
if n > 3 and i18n_ends(nm, ".po") { probe(nm[0..n - 3], md + "/" + nm, md) } if n > 3 and i18n_ends(nm, ".po") { probe(i18n_st, nm[0..n - 3], md + "/" + nm, md) }
} }
} }
# where a player's own .po files go; "" when the game has none # where a player's own .po files go; "" when the game has none
export function i18n_mod_dir() -> string { return I18nWorld.mod_dir() } export function i18n_mod_dir() -> string { return I18nWorld.mod_dir() }
function lang_add(code: string, name: string, path: string, font: string) -> void { function lang_add(i18n_st: mut I18nState, code: string, name: string, path: string, font: string) -> void {
for i in 0 .. len(codes) { for i in 0 .. len(i18n_st.codes) {
if codes[i] == code { if i18n_st.codes[i] == code {
names[i] = name i18n_st.names[i] = name
paths[i] = path i18n_st.paths[i] = path
fonts[i] = font i18n_st.fonts[i] = font
return return
} }
} }
push(codes, code) push(i18n_st.codes, code)
push(names, name) push(i18n_st.names, name)
push(paths, path) push(i18n_st.paths, path)
push(fonts, font) push(i18n_st.fonts, font)
} }
function probe(code: string, path: string, dir: string) -> void { function probe(i18n_st: mut I18nState, code: string, path: string, dir: string) -> void {
let text = Fs.read_text(path) let text = Fs.read_text(path)
if text == null { return } if text == null { return }
var name = i18n_header(text, "X-Language-Name") var name = i18n_header(text, "X-Language-Name")
if len(name) == 0 { name = code } if len(name) == 0 { name = code }
var font = i18n_header(text, "X-Font") var font = i18n_header(text, "X-Font")
if len(font) > 0 { font = dir + "/" + font } if len(font) > 0 { font = dir + "/" + font }
lang_add(code, name, path, font) lang_add(i18n_st, code, name, path, font)
} }
export function i18n_count() -> int { export function i18n_count(i18n_st: I18nState) -> int {
if codes == null { return 1 } if i18n_st.codes == null { return 1 }
return len(codes) return len(i18n_st.codes)
} }
export function i18n_index(code: string) -> int { export function i18n_index(i18n_st: I18nState, code: string) -> int {
if codes != null { for i in 0 .. len(codes) { if codes[i] == code { return i } } } if i18n_st.codes != null { for i in 0 .. len(i18n_st.codes) { if i18n_st.codes[i] == code { return i } } }
return 0 return 0
} }
export function i18n_code_at(i: int) -> string { export function i18n_code_at(i18n_st: I18nState, i: int) -> string {
if codes == null or i < 0 or i >= len(codes) { return "en" } if i18n_st.codes == null or i < 0 or i >= len(i18n_st.codes) { return "en" }
return codes[i] return i18n_st.codes[i]
} }
export function i18n_name_at(i: int) -> string { export function i18n_name_at(i18n_st: I18nState, i: int) -> string {
if names == null or i < 0 or i >= len(names) { return "English" } if i18n_st.names == null or i < 0 or i >= len(i18n_st.names) { return "English" }
return names[i] return i18n_st.names[i]
} }
# the font atlas a language names with X-Font, beside its .po; "" is the default # the font atlas a language names with X-Font, beside its .po; "" is the default
export function i18n_font_at(i: int) -> string { export function i18n_font_at(i18n_st: I18nState, i: int) -> string {
if fonts == null or i < 0 or i >= len(fonts) { return "" } if i18n_st.fonts == null or i < 0 or i >= len(i18n_st.fonts) { return "" }
return fonts[i] return i18n_st.fonts[i]
} }

View file

@ -2,13 +2,13 @@
# English, and the text that filled each hole translated in turn # English, and the text that filled each hole translated in turn
# does pattern p produce s? The holes land in caps, in hole order # does pattern p produce s? The holes land in caps, in hole order
function try_pattern(p: int, s: string) -> bool { function try_pattern(i18n_st: mut I18nState, p: int, s: string) -> bool {
let n = len(s) let n = len(s)
let l0 = pat_l0[p] let l0 = i18n_st.pat_l0[p]
let nl = pat_nl[p] let nl = i18n_st.pat_nl[p]
let first = lits[l0] let first = i18n_st.lits[l0]
let f_n = len(first) let f_n = len(first)
let last = lits[l0 + nl - 1] let last = i18n_st.lits[l0 + nl - 1]
let l_n = len(last) let l_n = len(last)
if nl - 1 > CAPS { return false } if nl - 1 > CAPS { return false }
if n < f_n + l_n + nl - 1 { return false } # every hole takes at least a byte if n < f_n + l_n + nl - 1 { return false } # every hole takes at least a byte
@ -20,76 +20,76 @@ function try_pattern(p: int, s: string) -> bool {
var stop = lim var stop = lim
var adv = 0 var adv = 0
if g < nl - 2 { if g < nl - 2 {
let mid = lits[l0 + g + 1] let mid = i18n_st.lits[l0 + g + 1]
stop = i18n_find(s, lim, pos + 1, mid, len(mid)) stop = i18n_find(s, lim, pos + 1, mid, len(mid))
if stop < 0 { return false } if stop < 0 { return false }
adv = len(mid) adv = len(mid)
} }
if stop <= pos { return false } if stop <= pos { return false }
caps[g] = s[pos..stop] i18n_st.caps[g] = s[pos..stop]
pos = stop + adv pos = stop + adv
} }
return true return true
} }
# the best of a chain that produces s, or `best` as it was # the best of a chain that produces s, or `best` as it was
function best_of(head: int, next: words, s: string, best: int) -> int { function best_of(i18n_st: mut I18nState, head: int, next: words, s: string, best: int) -> int {
var b = best var b = best
var c = head var c = head
while c > 0 { while c > 0 {
let p = c - 1 let p = c - 1
var sc = -1 var sc = -1
if b >= 0 { sc = pat_sc[b] } if b >= 0 { sc = i18n_st.pat_sc[b] }
if pat_sc[p] > sc and try_pattern(p, s) { b = p } if i18n_st.pat_sc[p] > sc and try_pattern(i18n_st, p, s) { b = p }
c = next[p] c = next[p]
} }
return b return b
} }
function pattern(s: string, depth: int) -> string { function pattern(i18n_st: mut I18nState, s: string, depth: int) -> string {
hit = false i18n_st.hit = false
if pat_n == 0 { return s } if i18n_st.pat_n == 0 { return s }
let n = len(s) let n = len(s)
var best = -1 var best = -1
if n >= 4 { if n >= 4 {
best = best_of(Dict.get_or(pfx, s[0..4], 0), nx_p, s, best) best = best_of(i18n_st, Dict.get_or(i18n_st.pfx, s[0..4], 0), i18n_st.nx_p, s, best)
best = best_of(Dict.get_or(sfx, s[n - 4..n], 0), nx_s, s, best) best = best_of(i18n_st, Dict.get_or(i18n_st.sfx, s[n - 4..n], 0), i18n_st.nx_s, s, best)
} }
if n < 80 { best = best_of(gen_head, nx_g, s, best) } if n < 80 { best = best_of(i18n_st, i18n_st.gen_head, i18n_st.nx_g, s, best) }
if best < 0 { return s } if best < 0 { return s }
try_pattern(best, s) try_pattern(i18n_st, best, s)
let l0 = pat_l0[best] let l0 = i18n_st.pat_l0[best]
let nl = pat_nl[best] let nl = i18n_st.pat_nl[best]
# copied out first: translating a hole may match another pattern and reuse caps # copied out first: translating a hole may match another pattern and reuse caps
let raw = new []string let raw = new []string
for g in 0 .. nl - 1 { push(raw, caps[g]) } for g in 0 .. nl - 1 { push(raw, i18n_st.caps[g]) }
let cs = new []string let cs = new []string
for g in 0 .. nl - 1 { push(cs, piece(raw[g], depth + 1)) } for g in 0 .. nl - 1 { push(cs, piece(i18n_st, raw[g], depth + 1)) }
let r = fill_holes(pat_out[best], l0, nl, cs) let r = fill_holes(i18n_st, i18n_st.pat_out[best], l0, nl, cs)
# a pattern that gives the English straight back has translated nothing, and must not stop # a pattern that gives the English straight back has translated nothing, and must not stop
# the sentence-by-sentence pass # the sentence-by-sentence pass
if r == s { return s } if r == s { return s }
hit = true i18n_st.hit = true
return r return r
} }
# the text that filled a hole: known as it stands, known without the English article the game # the text that filled a hole: known as it stands, known without the English article the game
# glued on ("a deer", "the Dark King"), or itself a pattern # glued on ("a deer", "the Dark King"), or itself a pattern
function piece(c: string, depth: int) -> string { function piece(i18n_st: mut I18nState, c: string, depth: int) -> string {
let e = Dict.get_or(exact, c, -1) let e = Dict.get_or(i18n_st.exact, c, -1)
if e >= 0 { return out[e] } if e >= 0 { return i18n_st.out[e] }
let n = len(c) let n = len(c)
var bare = -1 var bare = -1
if n > 4 and (i18n_starts(c, "the ") or i18n_starts(c, "The ")) { bare = 4 } if n > 4 and (i18n_starts(c, "the ") or i18n_starts(c, "The ")) { bare = 4 }
else if n > 3 and (i18n_starts(c, "an ") or i18n_starts(c, "An ")) { bare = 3 } else if n > 3 and (i18n_starts(c, "an ") or i18n_starts(c, "An ")) { bare = 3 }
else if n > 2 and (i18n_starts(c, "a ") or i18n_starts(c, "A ")) { bare = 2 } else if n > 2 and (i18n_starts(c, "a ") or i18n_starts(c, "A ")) { bare = 2 }
if bare > 0 { if bare > 0 {
let e2 = Dict.get_or(exact, c[bare..n], -1) let e2 = Dict.get_or(i18n_st.exact, c[bare..n], -1)
if e2 >= 0 { return out[e2] } if e2 >= 0 { return i18n_st.out[e2] }
} }
if depth < 3 { if depth < 3 {
let r = pattern(c, depth) let r = pattern(i18n_st, c, depth)
if hit { return r } if i18n_st.hit { return r }
} }
return c return c
} }

View file

@ -15,10 +15,10 @@ function holes(s: string) -> int {
return h return h
} }
function add_pattern(id: string, tr: string) -> void { function add_pattern(i18n_st: mut I18nState, id: string, tr: string) -> void {
let p = pat_n let p = i18n_st.pat_n
pat_n += 1 i18n_st.pat_n += 1
let l0 = len(lits) let l0 = len(i18n_st.lits)
let n = len(id) let n = len(id)
var seg = 0 var seg = 0
var i = 0 var i = 0
@ -27,9 +27,9 @@ function add_pattern(id: string, tr: string) -> void {
while i < n { while i < n {
let num = hole_at(id, i) let num = hole_at(id, i)
if num >= 0 { if num >= 0 {
push(lits, id[seg..i]) push(i18n_st.lits, id[seg..i])
score += i - seg score += i - seg
pat_ph[l0 + nl] = num i18n_st.pat_ph[l0 + nl] = num
nl += 1 nl += 1
i = hole_end(id, i) i = hole_end(id, i)
seg = i seg = i
@ -37,33 +37,33 @@ function add_pattern(id: string, tr: string) -> void {
} }
i += 1 i += 1
} }
push(lits, id[seg..n]) push(i18n_st.lits, id[seg..n])
score += n - seg score += n - seg
nl += 1 nl += 1
pat_l0[p] = l0 i18n_st.pat_l0[p] = l0
pat_nl[p] = nl i18n_st.pat_nl[p] = nl
pat_sc[p] = score i18n_st.pat_sc[p] = score
push(pat_out, tr) push(i18n_st.pat_out, tr)
chain(p, lits[l0], lits[l0 + nl - 1]) chain(i18n_st, p, i18n_st.lits[l0], i18n_st.lits[l0 + nl - 1])
} }
function chain(p: int, first: string, last: string) -> void { function chain(i18n_st: mut I18nState, p: int, first: string, last: string) -> void {
if len(first) >= 4 { if len(first) >= 4 {
let key: string = first[0..4] let key: string = first[0..4]
nx_p[p] = Dict.get_or(pfx, key, 0) i18n_st.nx_p[p] = Dict.get_or(i18n_st.pfx, key, 0)
Dict.set(pfx, key, p + 1) Dict.set(i18n_st.pfx, key, p + 1)
} else if len(last) >= 4 { } else if len(last) >= 4 {
let key: string = last[len(last) - 4..len(last)] let key: string = last[len(last) - 4..len(last)]
nx_s[p] = Dict.get_or(sfx, key, 0) i18n_st.nx_s[p] = Dict.get_or(i18n_st.sfx, key, 0)
Dict.set(sfx, key, p + 1) Dict.set(i18n_st.sfx, key, p + 1)
} else { } else {
nx_g[p] = gen_head i18n_st.nx_g[p] = i18n_st.gen_head
gen_head = p + 1 i18n_st.gen_head = p + 1
} }
} }
# a translation with its holes filled from caps, by number: a translation may reorder or drop them # a translation with its holes filled from caps, by number: a translation may reorder or drop them
function fill_holes(t: string, l0: int, nl: int, cs: []string) -> string { function fill_holes(i18n_st: I18nState, t: string, l0: int, nl: int, cs: []string) -> string {
let n = len(t) let n = len(t)
var r = "" var r = ""
var seg = 0 var seg = 0
@ -72,7 +72,7 @@ function fill_holes(t: string, l0: int, nl: int, cs: []string) -> string {
let num = hole_at(t, i) let num = hole_at(t, i)
if num >= 0 { if num >= 0 {
r = r + t[seg..i] r = r + t[seg..i]
for g in 0 .. nl - 1 { if pat_ph[l0 + g] == num { r = r + cs[g] } } for g in 0 .. nl - 1 { if i18n_st.pat_ph[l0 + g] == num { r = r + cs[g] } }
i = hole_end(t, i) i = hole_end(t, i)
seg = i seg = i
continue continue

View file

@ -15,7 +15,7 @@ function plural_rule_of(h: string) -> int {
return PL_NOT_ONE return PL_NOT_ONE
} }
export function i18n_plural_rule() -> int { return plural_rule } export function i18n_plural_rule(i18n_st: I18nState) -> int { return i18n_st.plural_rule }
# which msgstr[k] a count takes under a rule # which msgstr[k] a count takes under a rule
export function i18n_plural_form(rule: int, n: int) -> int { export function i18n_plural_form(rule: int, n: int) -> int {
@ -33,23 +33,23 @@ export function i18n_plural_form(rule: int, n: int) -> int {
return 0 return 0
} }
function plural_add(e: PoEntry) -> void { function plural_add(i18n_st: mut I18nState, e: PoEntry) -> void {
if len(e.forms) == 0 or blank(e.forms[0]) { return } if len(e.forms) == 0 or blank(e.forms[0]) { return }
Dict.set(plural, e.id, len(forms) / 3) Dict.set(i18n_st.plural, e.id, len(i18n_st.forms) / 3)
for k in 0 .. 3 { for k in 0 .. 3 {
if k < len(e.forms) { push(forms, e.forms[k]) } else { push(forms, e.forms[len(e.forms) - 1]) } if k < len(e.forms) { push(i18n_st.forms, e.forms[k]) } else { push(i18n_st.forms, e.forms[len(e.forms) - 1]) }
} }
} }
# `one` or `many` by the count, in the language in use, with every {1} the count: Ln("{1} trout", # `one` or `many` by the count, in the language in use, with every {1} the count: Ln("{1} trout",
# "{1} trout", 3). English (or a line the file lacks) is one for 1 and many otherwise # "{1} trout", 3). English (or a line the file lacks) is one for 1 and many otherwise
export function Ln(one: string, many: string, n: int) -> string { export function Ln(i18n_st: I18nState, one: string, many: string, n: int) -> string {
var t = many var t = many
if n == 1 { t = one } if n == 1 { t = one }
if on { if i18n_st.on {
let k = Dict.get_or(plural, one, -1) let k = Dict.get_or(i18n_st.plural, one, -1)
if k >= 0 { if k >= 0 {
let f = forms[k * 3 + i18n_plural_form(plural_rule, n)] let f = i18n_st.forms[k * 3 + i18n_plural_form(i18n_st.plural_rule, n)]
if not blank(f) { t = f } if not blank(f) { t = f }
} }
} }

View file

@ -12,13 +12,44 @@ function i18n__no_code() -> string { return "" }
function i18n__no() -> bool { return false } function i18n__no() -> bool { return false }
function i18n__no_font(dir: string) -> bool { return false } function i18n__no_font(dir: string) -> bool { return false }
var lang_dir: string = "assets/lang" export state I18nState {
var font_default: string = "assets/kit/font" lang_dir: string = "assets/lang"
font_default: string = "assets/kit/font"
on: bool = false # a language other than English is drawing
cur: string = "en"
codes: []string = null # every language found, "en" first
names: []string = null
paths: []string = null
fonts: []string = null # a font atlas directory, "" for the game's own
font_now: string = ""
exact: Dict = null # msgid -> index into out
out: []string = null
lits: []string = null # every pattern's literal pieces, one run per pattern
pat_out: []string = null
pat_l0: words = null # where a pattern's pieces start in lits
pat_nl: words = null # how many pieces (holes + 1)
pat_sc: words = null # bytes of English it pins down: more is a better match
pat_ph: words = null # by piece: the hole number that follows it
pat_n: int = 0
pfx: Dict = null # a pattern's first four bytes -> its chain head + 1
sfx: Dict = null # else its last four
gen_head: int = 0 # else the short list, tried on short strings only
nx_p: words = null
nx_s: words = null
nx_g: words = null
cache: Dict = null # English as drawn -> index into cache_out
cache_out: []string = null
caps: []string = null
hit: bool = false
plural: Dict = null # a plural msgid (the singular) -> index into forms / 3
forms: []string = null # three forms per entry, "" where the file has fewer
plural_rule: int = 0 # PL_*
}
# where the shipped languages are (a directory with languages.txt and <code>.po), and the atlas a # where the shipped languages are (a directory with languages.txt and <code>.po), and the atlas a
# language with no X-Font draws in # language with no X-Font draws in
export function i18n_config(dir: string, default_font: string) -> void { export function i18n_config(i18n_st: mut I18nState, dir: string, default_font: string) -> void {
lang_dir = dir i18n_st.lang_dir = dir
font_default = default_font i18n_st.font_default = default_font
codes = null i18n_st.codes = null
} }

View file

@ -1,47 +1,18 @@
# state.ludic - the languages found, the one in use, and the loaded file's lookups # state.ludic - the languages found, the one in use, and the loaded file's lookups
var on: bool = false # a language other than English is drawing
var cur: string = "en"
var codes: []string = null # every language found, "en" first
var names: []string = null
var paths: []string = null
var fonts: []string = null # a font atlas directory, "" for the game's own
var font_now: string = ""
var exact: Dict = null # msgid -> index into out
var out: []string = null
var lits: []string = null # every pattern's literal pieces, one run per pattern
var pat_out: []string = null
var pat_l0: words = null # where a pattern's pieces start in lits
var pat_nl: words = null # how many pieces (holes + 1)
var pat_sc: words = null # bytes of English it pins down: more is a better match
var pat_ph: words = null # by piece: the hole number that follows it
var pat_n: int = 0
var pfx: Dict = null # a pattern's first four bytes -> its chain head + 1
var sfx: Dict = null # else its last four
var gen_head: int = 0 # else the short list, tried on short strings only
var nx_p: words = null
var nx_s: words = null
var nx_g: words = null
var cache: Dict = null # English as drawn -> index into cache_out
var cache_out: []string = null
var caps: []string = null
var hit: bool = false
var plural: Dict = null # a plural msgid (the singular) -> index into forms / 3
var forms: []string = null # three forms per entry, "" where the file has fewer
var plural_rule: int = 0 # PL_*
const CACHE_MAX: int = 4000 const CACHE_MAX: int = 4000
const CAPS: int = 16 const CAPS: int = 16
export function i18n_on() -> bool { return on } export function i18n_on(i18n_st: I18nState) -> bool { return i18n_st.on }
export function i18n_current() -> string { return cur } export function i18n_current(i18n_st: I18nState) -> string { return i18n_st.cur }
export function i18n_font_now() -> string { return font_now } export function i18n_font_now(i18n_st: I18nState) -> string { return i18n_st.font_now }
export function i18n_exact_count() -> int { export function i18n_exact_count(i18n_st: I18nState) -> int {
if out == null { return 0 } if i18n_st.out == null { return 0 }
return len(out) return len(i18n_st.out)
} }
export function i18n_pattern_count() -> int { return pat_n } export function i18n_pattern_count(i18n_st: I18nState) -> int { return i18n_st.pat_n }
# the overlay's own font, as the overlay was opened with it # the overlay's own font, as the overlay was opened with it
export function i18n_font_base(path: string) -> void { font_now = path } export function i18n_font_base(i18n_st: mut I18nState, path: string) -> void { i18n_st.font_now = path }

View file

@ -4,18 +4,20 @@
import "ludic.i18n" import "ludic.i18n"
program I18nTest { program I18nTest {
numbers float numbers float
var forced_code: string = "" state I18ntestState {
var fonts_set: []string = null forced_code: string = ""
var font_ok: bool = true fonts_set: []string = null
font_ok: bool = true
}
function dir() -> string { return Os.temp_dir() + "/ludic-i18n-test" } function dir() -> string { return Os.temp_dir() + "/ludic-i18n-test" }
function mods() -> string { return dir() + "/mods" } function mods() -> string { return dir() + "/mods" }
function t_mods() -> string { return mods() } function t_mods() -> string { return mods() }
function t_forced() -> string { return forced_code } function t_forced(i18ntest_st: I18ntestState) -> string { return i18ntest_st.forced_code }
function t_ready() -> bool { return true } function t_ready() -> bool { return true }
function t_font(d: string) -> bool { function t_font(i18ntest_st: mut I18ntestState, d: string) -> bool {
push(fonts_set, d) push(i18ntest_st.fonts_set, d)
return font_ok return i18ntest_st.font_ok
} }
bind I18nWorld { mod_dir: fn t_mods, forced: fn t_forced, font_ready: fn t_ready, set_font: fn t_font } bind I18nWorld { mod_dir: fn t_mods, forced: fn t_forced, font_ready: fn t_ready, set_font: fn t_font }
@ -36,116 +38,116 @@ program I18nTest {
return s return s
} }
function fresh() -> void { function fresh(i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) -> void {
forced_code = "" i18ntest_st.forced_code = ""
fonts_set = new []string i18ntest_st.fonts_set = new []string
font_ok = true i18ntest_st.font_ok = true
Fs.mkdir(dir()) Fs.mkdir(dir())
Fs.mkdir(mods()) Fs.mkdir(mods())
Fs.write_text(dir() + "/languages.txt", "# shipped\ntt\nnope\n") Fs.write_text(dir() + "/languages.txt", "# shipped\ntt\nnope\n")
Fs.write_text(dir() + "/tt.po", tt()) Fs.write_text(dir() + "/tt.po", tt())
Fs.remove(mods() + "/zz.po") Fs.remove(mods() + "/zz.po")
i18n_config(dir(), "font_default") i18n_config(i18n_st, dir(), "font_default")
i18n_init() i18n_init(i18n_st)
} }
test "the shipped list and English are found, a missing file is not" { test "the shipped list and English are found, a missing file is not" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
fresh() fresh(i18n_st, i18ntest_st)
expect_eq(i18n_count(), 2) expect_eq(i18n_count(i18n_st), 2)
expect(i18n_code_at(0) == "en") expect(i18n_code_at(i18n_st, 0) == "en")
expect_eq(i18n_index("tt"), 1) expect_eq(i18n_index(i18n_st, "tt"), 1)
expect_eq(i18n_index("nope"), 0) expect_eq(i18n_index(i18n_st, "nope"), 0)
expect(i18n_name_at(1) == "Test Tongue") expect(i18n_name_at(i18n_st, 1) == "Test Tongue")
expect(i18n_font_at(1) == dir() + "/font_tt") expect(i18n_font_at(i18n_st, 1) == dir() + "/font_tt")
expect(L("Settings") == "Settings") expect(L(i18n_st, "Settings") == "Settings")
} }
test "exact lines, patterns with translated holes, and English where unknown" { test "exact lines, patterns with translated holes, and English where unknown" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
fresh() fresh(i18n_st, i18ntest_st)
i18n_use("tt") i18n_use(i18n_st, "tt")
expect(i18n_on()) expect(i18n_on(i18n_st))
expect(i18n_current() == "tt") expect(i18n_current(i18n_st) == "tt")
expect(L("Settings") == "Zettings") expect(L(i18n_st, "Settings") == "Zettings")
expect(L("Day 12") == "12. Gun") expect(L(i18n_st, "Day 12") == "12. Gun")
expect(L("+2 Plant fiber") == "Lif +2") expect(L(i18n_st, "+2 Plant fiber") == "Lif +2")
expect(L("You saw a deer.") == "geyik gordun.") expect(L(i18n_st, "You saw a deer.") == "geyik gordun.")
expect(L("zzqx words nobody wrote") == "zzqx words nobody wrote") expect(L(i18n_st, "zzqx words nobody wrote") == "zzqx words nobody wrote")
expect(L("Multiline") == "Coksatir\n") expect(L(i18n_st, "Multiline") == "Coksatir\n")
expect_eq(i18n_exact_count(), 6) expect_eq(i18n_exact_count(i18n_st), 6)
expect_eq(i18n_pattern_count(), 3) expect_eq(i18n_pattern_count(i18n_st), 3)
} }
test "a paragraph a sentence at a time, and a layout's padding kept" { test "a paragraph a sentence at a time, and a layout's padding kept" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
fresh() fresh(i18n_st, i18ntest_st)
i18n_use("tt") i18n_use(i18n_st, "tt")
expect(L("You need to eat. zzqx words nobody wrote.") == "Yemek lazim. zzqx words nobody wrote.") expect(L(i18n_st, "You need to eat. zzqx words nobody wrote.") == "Yemek lazim. zzqx words nobody wrote.")
expect(L("zzqx. zzqy.") == "zzqx. zzqy.") expect(L(i18n_st, "zzqx. zzqy.") == "zzqx. zzqy.")
expect(L(" owned") == " senin") expect(L(i18n_st, " owned") == " senin")
expect(L(" zzqx") == " zzqx") expect(L(i18n_st, " zzqx") == " zzqx")
} }
test "plurals by the file's rule, and English by one and many" { test "plurals by the file's rule, and English by one and many" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
fresh() fresh(i18n_st, i18ntest_st)
expect(Ln("{1} trout", "{1} trouts", 1) == "1 trout") expect(Ln(i18n_st, "{1} trout", "{1} trouts", 1) == "1 trout")
expect(Ln("{1} trout", "{1} trouts", 3) == "3 trouts") expect(Ln(i18n_st, "{1} trout", "{1} trouts", 3) == "3 trouts")
i18n_use("tt") i18n_use(i18n_st, "tt")
expect_eq(i18n_plural_rule(), PL_SLAVIC) expect_eq(i18n_plural_rule(i18n_st), PL_SLAVIC)
expect(Ln("{1} trout", "{1} trouts", 21) == "21 alabalik") expect(Ln(i18n_st, "{1} trout", "{1} trouts", 21) == "21 alabalik")
expect(Ln("{1} trout", "{1} trouts", 5) == "5 alabaliklar") expect(Ln(i18n_st, "{1} trout", "{1} trouts", 5) == "5 alabaliklar")
expect(Ln("{1} fish", "{1} fishes", 2) == "2 fishes") expect(Ln(i18n_st, "{1} fish", "{1} fishes", 2) == "2 fishes")
expect_eq(i18n_plural_form(PL_NOT_ONE, 0), 1) expect_eq(i18n_plural_form(PL_NOT_ONE, 0), 1)
expect_eq(i18n_plural_form(PL_ABOVE_ONE, 0), 0) expect_eq(i18n_plural_form(PL_ABOVE_ONE, 0), 0)
expect_eq(i18n_plural_form(PL_SLAVIC, 3), 1) expect_eq(i18n_plural_form(PL_SLAVIC, 3), 1)
expect_eq(i18n_plural_form(PL_ONE_FORM, 7), 0) expect_eq(i18n_plural_form(PL_ONE_FORM, 7), 0)
} }
test "the font follows the language, and falls back when it will not load" { test "the font follows the language, and falls back when it will not load" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
fresh() fresh(i18n_st, i18ntest_st)
i18n_font_base("font_default") i18n_font_base(i18n_st, "font_default")
i18n_use("tt") i18n_use(i18n_st, "tt")
expect_eq(len(fonts_set), 1) expect_eq(len(i18ntest_st.fonts_set), 1)
expect(i18n_font_now() == dir() + "/font_tt") expect(i18n_font_now(i18n_st) == dir() + "/font_tt")
i18n_use("en") i18n_use(i18n_st, "en")
expect(i18n_font_now() == "font_default") expect(i18n_font_now(i18n_st) == "font_default")
font_ok = false i18ntest_st.font_ok = false
i18n_use("tt") i18n_use(i18n_st, "tt")
expect(i18n_font_now() == "font_default") expect(i18n_font_now(i18n_st) == "font_default")
} }
test "a mod in the player's folder is found, replaces a shipped code, and goes when removed" { test "a mod in the player's folder is found, replaces a shipped code, and goes when removed" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
fresh() fresh(i18n_st, i18ntest_st)
Fs.write_text(mods() + "/zz.po", header("Mod Tongue", "") + po_line("Settings", "Mettings")) Fs.write_text(mods() + "/zz.po", header("Mod Tongue", "") + po_line("Settings", "Mettings"))
Fs.write_text(mods() + "/tt.po", header("Tongue Two", "") + po_line("Settings", "Z2")) Fs.write_text(mods() + "/tt.po", header("Tongue Two", "") + po_line("Settings", "Z2"))
i18n_init() i18n_init(i18n_st)
expect_eq(i18n_count(), 3) expect_eq(i18n_count(i18n_st), 3)
expect(i18n_name_at(i18n_index("zz")) == "Mod Tongue") expect(i18n_name_at(i18n_st, i18n_index(i18n_st, "zz")) == "Mod Tongue")
i18n_use("zz") i18n_use(i18n_st, "zz")
expect(L("Settings") == "Mettings") expect(L(i18n_st, "Settings") == "Mettings")
expect(L("Day 3") == "Day 3") expect(L(i18n_st, "Day 3") == "Day 3")
i18n_use("tt") i18n_use(i18n_st, "tt")
expect(L("Settings") == "Z2") expect(L(i18n_st, "Settings") == "Z2")
Fs.remove(mods() + "/zz.po") Fs.remove(mods() + "/zz.po")
Fs.remove(mods() + "/tt.po") Fs.remove(mods() + "/tt.po")
i18n_init() i18n_init(i18n_st)
expect_eq(i18n_index("zz"), 0) expect_eq(i18n_index(i18n_st, "zz"), 0)
i18n_use("en") i18n_use(i18n_st, "en")
expect(not i18n_on()) expect(not i18n_on(i18n_st))
expect(L("Settings") == "Settings") expect(L(i18n_st, "Settings") == "Settings")
} }
test "a forced code wins over the one asked for" { test "a forced code wins over the one asked for" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
fresh() fresh(i18n_st, i18ntest_st)
forced_code = "tt" i18ntest_st.forced_code = "tt"
i18n_use("en") i18n_use(i18n_st, "en")
expect(i18n_current() == "tt") expect(i18n_current(i18n_st) == "tt")
} }
test "a header field, and a .po loaded from text" { test "a header field, and a .po loaded from text" (i18n_st: mut I18nState) {
expect(i18n_header(tt(), "X-Language-Name") == "Test Tongue") expect(i18n_header(tt(), "X-Language-Name") == "Test Tongue")
expect(i18n_header(tt(), "X-Nothing") == "") expect(i18n_header(tt(), "X-Nothing") == "")
i18n_load_text(po_line("Quit", "Cik")) i18n_load_text(i18n_st, po_line("Quit", "Cik"))
expect(L("Quit") == "Cik") expect(L(i18n_st, "Quit") == "Cik")
i18n_off() i18n_off(i18n_st)
expect(L("Quit") == "Quit") expect(L(i18n_st, "Quit") == "Quit")
} }
} }

View file

@ -1,26 +1,26 @@
# translate.ludic - what the game draws, in the language in use: exact, else a pattern, else the # translate.ludic - what the game draws, in the language in use: exact, else a pattern, else the
# padding a layout put round it taken off, else a paragraph a sentence at a time; English otherwise # padding a layout put round it taken off, else a paragraph a sentence at a time; English otherwise
export function L(s: string) -> string { export function L(i18n_st: mut I18nState, s: string) -> string {
if not on { return s } if not i18n_st.on { return s }
let ci = Dict.get_or(cache, s, -1) let ci = Dict.get_or(i18n_st.cache, s, -1)
if ci >= 0 { return cache_out[ci] } if ci >= 0 { return i18n_st.cache_out[ci] }
let r = whole(s) let r = whole(i18n_st, s)
if Dict.size(cache) >= CACHE_MAX { if Dict.size(i18n_st.cache) >= CACHE_MAX {
Dict.clear(cache) Dict.clear(i18n_st.cache)
cache_out = new []string i18n_st.cache_out = new []string
} }
Dict.set(cache, s, len(cache_out)) Dict.set(i18n_st.cache, s, len(i18n_st.cache_out))
push(cache_out, r) push(i18n_st.cache_out, r)
return r return r
} }
function whole(s: string) -> string { function whole(i18n_st: mut I18nState, s: string) -> string {
let n = len(s) let n = len(s)
if n == 0 { return s } if n == 0 { return s }
let e = Dict.get_or(exact, s, -1) let e = Dict.get_or(i18n_st.exact, s, -1)
if e >= 0 { return out[e] } if e >= 0 { return i18n_st.out[e] }
let r = pattern(s, 0) let r = pattern(i18n_st, s, 0)
if hit { return r } if i18n_st.hit { return r }
# the padding a layout put round a word: " owned" # the padding a layout put round a word: " owned"
var a = 0 var a = 0
var b = n var b = n
@ -29,15 +29,15 @@ function whole(s: string) -> string {
if a > 0 or b < n { if a > 0 or b < n {
if b <= a { return s } if b <= a { return s }
let core: string = s[a..b] let core: string = s[a..b]
let t = whole(core) let t = whole(i18n_st, core)
if t == core { return s } if t == core { return s }
return s[0..a] + t + s[b..n] return s[0..a] + t + s[b..n]
} }
return sentences(s) return sentences(i18n_st, s)
} }
# a paragraph built from sentences, one at a time; English stays where a sentence is unknown # a paragraph built from sentences, one at a time; English stays where a sentence is unknown
function sentences(s: string) -> string { function sentences(i18n_st: mut I18nState, s: string) -> string {
let n = len(s) let n = len(s)
var r = "" var r = ""
var start = 0 var start = 0
@ -47,8 +47,8 @@ function sentences(s: string) -> string {
while i < n - 1 { while i < n - 1 {
let c = s[i] let c = s[i]
if (c == 46 or c == 33 or c == 63) and s[i + 1] == 32 { if (c == 46 or c == 33 or c == 63) and s[i + 1] == 32 {
let t = sentence(s[start..i + 1]) let t = sentence(i18n_st, s[start..i + 1])
if hit { any = true } if i18n_st.hit { any = true }
r = r + t + " " r = r + t + " "
start = i + 2 start = i + 2
pieces += 1 pieces += 1
@ -58,18 +58,18 @@ function sentences(s: string) -> string {
i += 1 i += 1
} }
if pieces == 0 { return s } if pieces == 0 { return s }
let t = sentence(s[start..n]) let t = sentence(i18n_st, s[start..n])
if hit { any = true } if i18n_st.hit { any = true }
if not any { return s } if not any { return s }
return r + t return r + t
} }
function sentence(p: string) -> string { function sentence(i18n_st: mut I18nState, p: string) -> string {
hit = false i18n_st.hit = false
let e = Dict.get_or(exact, p, -1) let e = Dict.get_or(i18n_st.exact, p, -1)
if e >= 0 { if e >= 0 {
hit = true i18n_st.hit = true
return out[e] return i18n_st.out[e]
} }
return pattern(p, 0) return pattern(i18n_st, p, 0)
} }

View file

@ -1,43 +1,43 @@
# use.ludic - choosing a language: its file loaded into the lookups, and the font atlas it names # use.ludic - choosing a language: its file loaded into the lookups, and the font atlas it names
export function i18n_use(code: string) -> void { export function i18n_use(i18n_st: mut I18nState, code: string) -> void {
if codes == null { i18n_init() } if i18n_st.codes == null { i18n_init(i18n_st) }
on = false i18n_st.on = false
cur = "en" i18n_st.cur = "en"
var k = i18n_index(code) var k = i18n_index(i18n_st, code)
let forced = I18nWorld.forced() let forced = I18nWorld.forced()
if len(forced) > 0 { k = i18n_index(forced) } if len(forced) > 0 { k = i18n_index(i18n_st, forced) }
var font = "" var font = ""
if k > 0 { if k > 0 {
let text = Fs.read_text(paths[k]) let text = Fs.read_text(i18n_st.paths[k])
if text != null { if text != null {
i18n_load_text(text) i18n_load_text(i18n_st, text)
on = true i18n_st.on = true
cur = codes[k] i18n_st.cur = i18n_st.codes[k]
font = fonts[k] font = i18n_st.fonts[k]
} }
} }
# the atlas follows the language, and is only reloaded when it actually changes # the atlas follows the language, and is only reloaded when it actually changes
if len(font) == 0 { font = font_default } if len(font) == 0 { font = i18n_st.font_default }
if I18nWorld.font_ready() and not (font == font_now) { if I18nWorld.font_ready() and not (font == i18n_st.font_now) {
if I18nWorld.set_font(font) { font_now = font } else if not (font_now == font_default) { if I18nWorld.set_font(font) { i18n_st.font_now = font } else if not (i18n_st.font_now == i18n_st.font_default) {
I18nWorld.set_font(font_default) I18nWorld.set_font(i18n_st.font_default)
font_now = font_default i18n_st.font_now = i18n_st.font_default
} }
} }
} }
# a .po's text as the lookups, whatever the language is called (a test, a file from elsewhere) # a .po's text as the lookups, whatever the language is called (a test, a file from elsewhere)
export function i18n_load_text(text: string) -> void { export function i18n_load_text(i18n_st: mut I18nState, text: string) -> void {
let es = i18n_parse(text) let es = i18n_parse(text)
exact = Dict.new() i18n_st.exact = Dict.new()
out = new []string i18n_st.out = new []string
cache = Dict.new() i18n_st.cache = Dict.new()
cache_out = new []string i18n_st.cache_out = new []string
caps = new []string i18n_st.caps = new []string
for c in 0 .. CAPS { push(caps, "") } for c in 0 .. CAPS { push(i18n_st.caps, "") }
plural = Dict.new() i18n_st.plural = Dict.new()
forms = new []string i18n_st.forms = new []string
plural_rule = plural_rule_of(i18n_header(text, "Plural-Forms")) i18n_st.plural_rule = plural_rule_of(i18n_header(text, "Plural-Forms"))
var np = 0 var np = 0
var nlit = 0 var nlit = 0
for i in 0 .. len(es) { for i in 0 .. len(es) {
@ -48,33 +48,33 @@ export function i18n_load_text(text: string) -> void {
nlit += h + 1 nlit += h + 1
} }
} }
pat_out = new []string i18n_st.pat_out = new []string
lits = new []string i18n_st.lits = new []string
pat_l0 = words(np + 1) i18n_st.pat_l0 = words(np + 1)
pat_nl = words(np + 1) i18n_st.pat_nl = words(np + 1)
pat_sc = words(np + 1) i18n_st.pat_sc = words(np + 1)
pat_ph = words(nlit + 1) i18n_st.pat_ph = words(nlit + 1)
nx_p = words(np + 1) i18n_st.nx_p = words(np + 1)
nx_s = words(np + 1) i18n_st.nx_s = words(np + 1)
nx_g = words(np + 1) i18n_st.nx_g = words(np + 1)
pfx = Dict.new() i18n_st.pfx = Dict.new()
sfx = Dict.new() i18n_st.sfx = Dict.new()
gen_head = 0 i18n_st.gen_head = 0
pat_n = 0 i18n_st.pat_n = 0
for i in 0 .. len(es) { for i in 0 .. len(es) {
let e = es[i] let e = es[i]
if len(e.plural) > 0 { plural_add(e) } else if not (blank(e.id) or blank(e.str)) { if len(e.plural) > 0 { plural_add(i18n_st, e) } else if not (blank(e.id) or blank(e.str)) {
if holes(e.id) > 0 { add_pattern(e.id, e.str) } else { if holes(e.id) > 0 { add_pattern(i18n_st, e.id, e.str) } else {
Dict.set(exact, e.id, len(out)) Dict.set(i18n_st.exact, e.id, len(i18n_st.out))
push(out, e.str) push(i18n_st.out, e.str)
} }
} }
} }
on = true i18n_st.on = true
} }
# English again: nothing loaded, nothing translated # English again: nothing loaded, nothing translated
export function i18n_off() -> void { export function i18n_off(i18n_st: mut I18nState) -> void {
on = false i18n_st.on = false
cur = "en" i18n_st.cur = "en"
} }

View file

@ -12,31 +12,33 @@ export property ItemChanged {
after: int = 0 after: int = 0
} }
var iv_keys: []string = null export state InventoryState {
var iv_counts: []int = null iv_keys: []string = null
var iv_changes: Queue<ItemChanged> = null iv_counts: []int = null
iv_changes: Queue<ItemChanged> = null
}
# the kinds, by the name each is saved under; the pack starts empty # the kinds, by the name each is saved under; the pack starts empty
export function inv_config(keys: []string) -> void { export function inv_config(inventory_st: mut InventoryState, keys: []string) -> void {
iv_keys = keys inventory_st.iv_keys = keys
inv_clear() inv_clear(inventory_st)
} }
export function inv_changes() -> Queue<ItemChanged> { export function inv_changes(base_st: mut BaseState, inventory_st: mut InventoryState) -> Queue<ItemChanged> {
if iv_changes == null { iv_changes = queue_new("inventory.changes") } if inventory_st.iv_changes == null { inventory_st.iv_changes = queue_new(base_st, "inventory.changes") }
return iv_changes return inventory_st.iv_changes
} }
function iv_valid(it: int) -> bool { return iv_counts != null and it >= 0 and it < len(iv_counts) } function iv_valid(inventory_st: InventoryState, it: int) -> bool { return inventory_st.iv_counts != null and it >= 0 and it < len(inventory_st.iv_counts) }
export function inv_kinds() -> int { export function inv_kinds(inventory_st: InventoryState) -> int {
if iv_keys == null { return 0 } if inventory_st.iv_keys == null { return 0 }
return len(iv_keys) return len(inventory_st.iv_keys)
} }
export function inv_count(it: int) -> int { export function inv_count(inventory_st: InventoryState, it: int) -> int {
if not iv_valid(it) { return 0 } if not iv_valid(inventory_st, it) { return 0 }
return iv_counts[it] return inventory_st.iv_counts[it]
} }
export function inv_limit(it: int) -> int { export function inv_limit(it: int) -> int {
@ -44,16 +46,16 @@ export function inv_limit(it: int) -> int {
} }
# how many more fit # how many more fit
export function inv_room(it: int) -> int { export function inv_room(inventory_st: InventoryState, it: int) -> int {
if not iv_valid(it) { return 0 } if not iv_valid(inventory_st, it) { return 0 }
return Math.max(inv_limit(it) - iv_counts[it], 0) return Math.max(inv_limit(it) - inventory_st.iv_counts[it], 0)
} }
export function inv_has(it: int, n: int) -> bool { return inv_count(it) >= n } export function inv_has(inventory_st: InventoryState, it: int, n: int) -> bool { return inv_count(inventory_st, it) >= n }
export function inv_total() -> int { export function inv_total(inventory_st: InventoryState) -> int {
var t = 0 var t = 0
if iv_counts == null { return 0 } if inventory_st.iv_counts == null { return 0 }
for i in 0 .. len(iv_counts) { t += iv_counts[i] } for i in 0 .. len(inventory_st.iv_counts) { t += inventory_st.iv_counts[i] }
return t return t
} }

View file

@ -1,14 +1,14 @@
# system.ludic - the pack as a system: reset and its own save section, each count under its # system.ludic - the pack as a system: reset and its own save section, each count under its
# kind's name, so kinds can be added or reordered without a migration # kind's name, so kinds can be added or reordered without a migration
export function inv_reset() -> void { export function inv_reset(base_st: mut BaseState, inventory_st: mut InventoryState) -> void {
inv_clear() inv_clear(inventory_st)
q_clear(inv_changes()) q_clear(base_st, inv_changes(base_st, inventory_st))
} }
export function inv_save() -> Val { export function inv_save(inventory_st: InventoryState) -> Val {
let counts = Value.object() let counts = Value.object()
for i in 0 .. inv_kinds() { for i in 0 .. inv_kinds(inventory_st) {
if iv_counts[i] != 0 { sv_put_int(counts, iv_keys[i], iv_counts[i]) } if inventory_st.iv_counts[i] != 0 { sv_put_int(counts, inventory_st.iv_keys[i], inventory_st.iv_counts[i]) }
} }
let v = Value.object() let v = Value.object()
Value.put(v, "counts", counts) Value.put(v, "counts", counts)
@ -16,11 +16,11 @@ export function inv_save() -> Val {
} }
# a name this build does not know is dropped # a name this build does not know is dropped
export function inv_load(v: Val, version: int) -> void { export function inv_load(inventory_st: mut InventoryState, v: Val, version: int) -> void {
inv_clear() inv_clear(inventory_st)
if Value.has(v, "counts") == 0 { return } if Value.has(v, "counts") == 0 { return }
let counts = Value.get(v, "counts") let counts = Value.get(v, "counts")
for i in 0 .. inv_kinds() { iv_counts[i] = Math.max(sv_int(counts, iv_keys[i], 0), 0) } for i in 0 .. inv_kinds(inventory_st) { inventory_st.iv_counts[i] = Math.max(sv_int(counts, inventory_st.iv_keys[i], 0), 0) }
} }
export function inv_system() -> System { export function inv_system() -> System {

View file

@ -23,74 +23,74 @@ program InventoryTest {
bind PackRules { stack_limit: fn limit } bind PackRules { stack_limit: fn limit }
function fresh() -> void { function fresh(base_st: mut BaseState, inventory_st: mut InventoryState) -> void {
inv_config(names("wood", "rod", "fish")) inv_config(inventory_st, names("wood", "rod", "fish"))
inv_reset() inv_reset(base_st, inventory_st)
} }
function changes() -> []ItemChanged { return q_drain(inv_changes()) } function changes(base_st: mut BaseState, inventory_st: mut InventoryState) -> []ItemChanged { return q_drain(base_st, inv_changes(base_st, inventory_st)) }
function waiting() -> int { return q_len(inv_changes()) } function waiting(base_st: mut BaseState, inventory_st: mut InventoryState) -> int { return q_len(inv_changes(base_st, inventory_st)) }
test "add puts in what fits and says how many" { test "add puts in what fits and says how many" (base_st: mut BaseState, inventory_st: mut InventoryState) {
fresh() fresh(base_st, inventory_st)
expect_eq(inv_add(WOOD, 7), 7) expect_eq(inv_add(base_st, inventory_st, WOOD, 7), 7)
expect_eq(inv_add(WOOD, 7), 3) expect_eq(inv_add(base_st, inventory_st, WOOD, 7), 3)
expect_eq(inv_count(WOOD), 10) expect_eq(inv_count(inventory_st, WOOD), 10)
expect_eq(inv_room(WOOD), 0) expect_eq(inv_room(inventory_st, WOOD), 0)
expect_eq(inv_add(ROD, 2), 1) expect_eq(inv_add(base_st, inventory_st, ROD, 2), 1)
expect_eq(inv_total(), 11) expect_eq(inv_total(inventory_st), 11)
} }
test "take is all or none" { test "take is all or none" (base_st: mut BaseState, inventory_st: mut InventoryState) {
fresh() fresh(base_st, inventory_st)
inv_add(FISH, 3) inv_add(base_st, inventory_st, FISH, 3)
expect(not inv_take(FISH, 4)) expect(not inv_take(base_st, inventory_st, FISH, 4))
expect_eq(inv_count(FISH), 3) expect_eq(inv_count(inventory_st, FISH), 3)
expect(inv_take(FISH, 3)) expect(inv_take(base_st, inventory_st, FISH, 3))
expect(not inv_has(FISH, 1)) expect(not inv_has(inventory_st, FISH, 1))
} }
test "set goes past the room, never below zero" { test "set goes past the room, never below zero" (base_st: mut BaseState, inventory_st: mut InventoryState) {
fresh() fresh(base_st, inventory_st)
inv_set(ROD, 3) inv_set(base_st, inventory_st, ROD, 3)
expect_eq(inv_count(ROD), 3) expect_eq(inv_count(inventory_st, ROD), 3)
expect_eq(inv_room(ROD), 0) expect_eq(inv_room(inventory_st, ROD), 0)
inv_set(ROD, -2) inv_set(base_st, inventory_st, ROD, -2)
expect_eq(inv_count(ROD), 0) expect_eq(inv_count(inventory_st, ROD), 0)
} }
test "every change is a fact, and no change is not" { test "every change is a fact, and no change is not" (base_st: mut BaseState, inventory_st: mut InventoryState) {
fresh() fresh(base_st, inventory_st)
inv_add(WOOD, 2) inv_add(base_st, inventory_st, WOOD, 2)
inv_take(WOOD, 1) inv_take(base_st, inventory_st, WOOD, 1)
inv_set(WOOD, 1) inv_set(base_st, inventory_st, WOOD, 1)
let cs = changes() let cs = changes(base_st, inventory_st)
expect_eq(len(cs), 2) expect_eq(len(cs), 2)
expect_eq(cs[0].after, 2) expect_eq(cs[0].after, 2)
expect_eq(cs[1].before, 2) expect_eq(cs[1].before, 2)
expect_eq(cs[1].after, 1) expect_eq(cs[1].after, 1)
expect_eq(waiting(), 0) expect_eq(waiting(base_st, inventory_st), 0)
} }
test "an item out of range is nothing" { test "an item out of range is nothing" (base_st: mut BaseState, inventory_st: mut InventoryState) {
fresh() fresh(base_st, inventory_st)
expect_eq(inv_add(9, 1), 0) expect_eq(inv_add(base_st, inventory_st, 9, 1), 0)
expect_eq(inv_count(-1), 0) expect_eq(inv_count(inventory_st, -1), 0)
expect(not inv_take(9, 0)) expect(not inv_take(base_st, inventory_st, 9, 0))
} }
test "the save is by name, so a reordered table still reads it" { test "the save is by name, so a reordered table still reads it" (base_st: mut BaseState, inventory_st: mut InventoryState) {
fresh() fresh(base_st, inventory_st)
inv_add(WOOD, 4) inv_add(base_st, inventory_st, WOOD, 4)
inv_add(FISH, 2) inv_add(base_st, inventory_st, FISH, 2)
let root = save_tree() let root = save_tree()
save_section(root, "inventory", 1, inv_save()) save_section(root, "inventory", 1, inv_save(inventory_st))
let text = save_encode(root) let text = save_encode(root)
inv_config(names("fish", "bait", "wood")) inv_config(inventory_st, names("fish", "bait", "wood"))
let n = load_section(save_decode(text), "inventory") let n = load_section(save_decode(text), "inventory")
inv_load(n.data, n.version) inv_load(inventory_st, n.data, n.version)
expect_eq(inv_count(0), 2) expect_eq(inv_count(inventory_st, 0), 2)
expect_eq(inv_count(1), 0) expect_eq(inv_count(inventory_st, 1), 0)
expect_eq(inv_count(2), 4) expect_eq(inv_count(inventory_st, 2), 4)
} }
} }

View file

@ -1,38 +1,38 @@
# verbs.ludic - the only way a count changes, and each change is said once # verbs.ludic - the only way a count changes, and each change is said once
function iv_change(it: int, after: int) -> void { function iv_change(base_st: mut BaseState, inventory_st: mut InventoryState, it: int, after: int) -> void {
let before = iv_counts[it] let before = inventory_st.iv_counts[it]
if before == after { return } if before == after { return }
iv_counts[it] = after inventory_st.iv_counts[it] = after
let c = new ItemChanged let c = new ItemChanged
c.item = it c.item = it
c.before = before c.before = before
c.after = after c.after = after
q_push(inv_changes(), c) q_push(base_st, inv_changes(base_st, inventory_st), c)
} }
# as many of n as there is room for; returns how many went in # as many of n as there is room for; returns how many went in
export function inv_add(it: int, n: int) -> int { export function inv_add(base_st: mut BaseState, inventory_st: mut InventoryState, it: int, n: int) -> int {
if not iv_valid(it) or n <= 0 { return 0 } if not iv_valid(inventory_st, it) or n <= 0 { return 0 }
let k = Math.min(n, inv_room(it)) let k = Math.min(n, inv_room(inventory_st, it))
if k > 0 { iv_change(it, iv_counts[it] + k) } if k > 0 { iv_change(base_st, inventory_st, it, inventory_st.iv_counts[it] + k) }
return k return k
} }
# all n or none # all n or none
export function inv_take(it: int, n: int) -> bool { export function inv_take(base_st: mut BaseState, inventory_st: mut InventoryState, it: int, n: int) -> bool {
if not iv_valid(it) or iv_counts[it] < n { return false } if not iv_valid(inventory_st, it) or inventory_st.iv_counts[it] < n { return false }
if n > 0 { iv_change(it, iv_counts[it] - n) } if n > 0 { iv_change(base_st, inventory_st, it, inventory_st.iv_counts[it] - n) }
return true return true
} }
# a count outright, room or not (a starting kit, a correction, the network's word); never below 0 # a count outright, room or not (a starting kit, a correction, the network's word); never below 0
export function inv_set(it: int, n: int) -> void { export function inv_set(base_st: mut BaseState, inventory_st: mut InventoryState, it: int, n: int) -> void {
if not iv_valid(it) { return } if not iv_valid(inventory_st, it) { return }
iv_change(it, Math.max(n, 0)) iv_change(base_st, inventory_st, it, Math.max(n, 0))
} }
# everything gone, silently: a new trip, or before a load # everything gone, silently: a new trip, or before a load
export function inv_clear() -> void { export function inv_clear(inventory_st: mut InventoryState) -> void {
iv_counts = new []int inventory_st.iv_counts = new []int
for i in 0 .. inv_kinds() { push(iv_counts, 0) } for i in 0 .. inv_kinds(inventory_st) { push(inventory_st.iv_counts, 0) }
} }

View file

@ -2,88 +2,88 @@
# the jobs' own dice seeded by the day, so a board never moves anyone else's rolls # the jobs' own dice seeded by the day, so a board never moves anyone else's rolls
export function jobs_board_ok(b: int) -> bool { return b >= 0 and b < BOARD_COUNT } export function jobs_board_ok(b: int) -> bool { return b >= 0 and b < BOARD_COUNT }
function jobs__at(b: int, s: int) -> int { function jobs__at(jobs_st: mut JobsState, b: int, s: int) -> int {
jobs__ensure() jobs__ensure(jobs_st)
return jobs__off[b] + s return jobs_st.jobs__off[b] + s
} }
# a whole number in [lo, hi] from the jobs' dice: what JobsWorld.roll draws with # a whole number in [lo, hi] from the jobs' dice: what JobsWorld.roll draws with
export function jobs_roll(lo: int, hi: int) -> int { export function jobs_roll(jobs_st: mut JobsState, lo: int, hi: int) -> int {
jobs__ensure() jobs__ensure(jobs_st)
return rng_between(jobs__dice, lo, hi) return rng_between(jobs_st.jobs__dice, lo, hi)
} }
# inside JobsWorld.roll: what goes up in the slot being filled # inside JobsWorld.roll: what goes up in the slot being filled
export function jobs_post(b: int, s: int, kind: int, param: int, need: int, money: int, rep: int) -> void { export function jobs_post(jobs_st: mut JobsState, b: int, s: int, kind: int, param: int, need: int, money: int, rep: int) -> void {
let k = jobs__at(b, s) let k = jobs__at(jobs_st, b, s)
jobs__pk[k] = kind jobs_st.jobs__pk[k] = kind
jobs__pp[k] = param jobs_st.jobs__pp[k] = param
jobs__pn[k] = need jobs_st.jobs__pn[k] = need
jobs__pm[k] = money jobs_st.jobs__pm[k] = money
jobs__pr[k] = rep jobs_st.jobs__pr[k] = rep
jobs__ph[k] = 0 jobs_st.jobs__ph[k] = 0
jobs__ps[k] = JOBS_ACTIVE jobs_st.jobs__ps[k] = JOBS_ACTIVE
jobs__pd[k] = JobsWorld.day() jobs_st.jobs__pd[k] = JobsWorld.day()
let e = JobsWorld.evidence(kind, param) let e = JobsWorld.evidence(kind, param)
jobs__pb[k] = 0 jobs_st.jobs__pb[k] = 0
if e > 0 { jobs__pb[k] = e } if e > 0 { jobs_st.jobs__pb[k] = e }
} }
function jobs__clear(k: int) -> void { function jobs__clear(jobs_st: mut JobsState, k: int) -> void {
jobs__pk[k] = -1 jobs_st.jobs__pk[k] = -1
jobs__pp[k] = -1 jobs_st.jobs__pp[k] = -1
jobs__pn[k] = 0 jobs_st.jobs__pn[k] = 0
jobs__ph[k] = 0 jobs_st.jobs__ph[k] = 0
jobs__ps[k] = JOBS_OFFERED jobs_st.jobs__ps[k] = JOBS_OFFERED
} }
# take a slot down and ask the game for what replaces it # take a slot down and ask the game for what replaces it
function jobs__repost(b: int, s: int) -> void { function jobs__repost(base_st: mut BaseState, jobs_st: mut JobsState, b: int, s: int) -> void {
let k = jobs__at(b, s) let k = jobs__at(jobs_st, b, s)
let day = JobsWorld.day() let day = JobsWorld.day()
rng_seed(jobs__dice, jobs__seed + day * 7 + b * 7919 + s * 104729 + jobs__serial[b] * 31) rng_seed(jobs_st.jobs__dice, jobs_st.jobs__seed + day * 7 + b * 7919 + s * 104729 + jobs_st.jobs__serial[b] * 31)
jobs__serial[b] += 1 jobs_st.jobs__serial[b] += 1
jobs__clear(k) jobs__clear(jobs_st, k)
JobsWorld.roll(b, s) JobsWorld.roll(b, s)
if jobs__pk[k] >= 0 { jobs__fact(JOBS_F_OFFERED, -1, b, s, 0, jobs__pn[k]) } if jobs_st.jobs__pk[k] >= 0 { jobs__fact(base_st, jobs_st, JOBS_F_OFFERED, -1, b, s, 0, jobs_st.jobs__pn[k]) }
} }
# a new day, on the boards this machine keeps: an empty or stale post is replaced, and one # a new day, on the boards this machine keeps: an empty or stale post is replaced, and one
# finished on an earlier day # finished on an earlier day
export function jobs_morning() -> void { export function jobs_morning(base_st: mut BaseState, jobs_st: mut JobsState) -> void {
jobs__ensure() jobs__ensure(jobs_st)
let day = JobsWorld.day() let day = JobsWorld.day()
for b in 0 .. BOARD_COUNT { for b in 0 .. BOARD_COUNT {
let bd = JobBoards[b] let bd = JobBoards[b]
if not JobsWorld.owns(bd.scope) { continue } if not JobsWorld.owns(bd.scope) { continue }
for s in 0 .. bd.slots { for s in 0 .. bd.slots {
let k = jobs__off[b] + s let k = jobs_st.jobs__off[b] + s
let finished = jobs__ps[k] == JOBS_DONE and day > jobs__pd[k] let finished = jobs_st.jobs__ps[k] == JOBS_DONE and day > jobs_st.jobs__pd[k]
if jobs__pk[k] < 0 or finished or day - jobs__pd[k] >= bd.keep { jobs__repost(b, s) } if jobs_st.jobs__pk[k] < 0 or finished or day - jobs_st.jobs__pd[k] >= bd.keep { jobs__repost(base_st, jobs_st, b, s) }
} }
} }
} }
function jobs__post_moved(b: int, s: int, have: int) -> void { function jobs__post_moved(base_st: mut BaseState, jobs_st: mut JobsState, b: int, s: int, have: int) -> void {
let k = jobs__off[b] + s let k = jobs_st.jobs__off[b] + s
let was = jobs__ph[k] let was = jobs_st.jobs__ph[k]
jobs__ph[k] = have jobs_st.jobs__ph[k] = have
if jobs__ph[k] > jobs__pn[k] { jobs__ph[k] = jobs__pn[k] } if jobs_st.jobs__ph[k] > jobs_st.jobs__pn[k] { jobs_st.jobs__ph[k] = jobs_st.jobs__pn[k] }
if jobs__ph[k] > was { jobs__fact(JOBS_F_PROGRESSED, -1, b, s, jobs__ph[k], jobs__pn[k]) } if jobs_st.jobs__ph[k] > was { jobs__fact(base_st, jobs_st, JOBS_F_PROGRESSED, -1, b, s, jobs_st.jobs__ph[k], jobs_st.jobs__pn[k]) }
if jobs__ps[k] != JOBS_ACTIVE or jobs__ph[k] < jobs__pn[k] { return } if jobs_st.jobs__ps[k] != JOBS_ACTIVE or jobs_st.jobs__ph[k] < jobs_st.jobs__pn[k] { return }
jobs__ps[k] = JOBS_READY jobs_st.jobs__ps[k] = JOBS_READY
jobs__fact(JOBS_F_COMPLETED, -1, b, s, jobs__ph[k], jobs__pn[k]) jobs__fact(base_st, jobs_st, JOBS_F_COMPLETED, -1, b, s, jobs_st.jobs__ph[k], jobs_st.jobs__pn[k])
if JobBoards[b].auto { jobs_post_hand_in(b, s) } if JobBoards[b].auto { jobs_post_hand_in(base_st, jobs_st, b, s) }
} }
export function jobs_post_hand_in(b: int, s: int) -> bool { export function jobs_post_hand_in(base_st: mut BaseState, jobs_st: mut JobsState, b: int, s: int) -> bool {
if not jobs_board_ok(b) or s < 0 or s >= JobBoards[b].slots { return false } if not jobs_board_ok(b) or s < 0 or s >= JobBoards[b].slots { return false }
let k = jobs__at(b, s) let k = jobs__at(jobs_st, b, s)
if jobs__ps[k] != JOBS_READY { return false } if jobs_st.jobs__ps[k] != JOBS_READY { return false }
jobs__ps[k] = JOBS_DONE jobs_st.jobs__ps[k] = JOBS_DONE
jobs__total[b] += 1 jobs_st.jobs__total[b] += 1
JobsPay.reward(JobBoards[b].scope, jobs__pm[k], jobs__pr[k]) JobsPay.reward(JobBoards[b].scope, jobs_st.jobs__pm[k], jobs_st.jobs__pr[k])
jobs__fact(JOBS_F_HANDED_IN, -1, b, s, jobs__ph[k], jobs__pn[k]) jobs__fact(base_st, jobs_st, JOBS_F_HANDED_IN, -1, b, s, jobs_st.jobs__ph[k], jobs_st.jobs__pn[k])
if JobBoards[b].refill { jobs__repost(b, s) } if JobBoards[b].refill { jobs__repost(base_st, jobs_st, b, s) }
return true return true
} }

View file

@ -6,69 +6,69 @@ function jobs__matches(want_kind: int, want_param: int, kind: int, param: int) -
} }
# `n` of (kind, param) happened here: every job and post of a scope this machine keeps # `n` of (kind, param) happened here: every job and post of a scope this machine keeps
export function jobs_count(kind: int, param: int, n: int) -> void { jobs__count(-1, kind, param, n) } export function jobs_count(base_st: mut BaseState, jobs_st: mut JobsState, kind: int, param: int, n: int) -> void { jobs__count(base_st, jobs_st, -1, kind, param, n) }
# the same for one scope alone, kept or not: another member's deed arriving at the host # the same for one scope alone, kept or not: another member's deed arriving at the host
export function jobs_count_in(scope: int, kind: int, param: int, n: int) -> void { jobs__count(scope, kind, param, n) } export function jobs_count_in(base_st: mut BaseState, jobs_st: mut JobsState, scope: int, kind: int, param: int, n: int) -> void { jobs__count(base_st, jobs_st, scope, kind, param, n) }
function jobs__takes(only: int, scope: int) -> bool { function jobs__takes(only: int, scope: int) -> bool {
if only >= 0 { return scope == only } if only >= 0 { return scope == only }
return JobsWorld.owns(scope) return JobsWorld.owns(scope)
} }
function jobs__count(only: int, kind: int, param: int, n: int) -> void { function jobs__count(base_st: mut BaseState, jobs_st: mut JobsState, only: int, kind: int, param: int, n: int) -> void {
jobs__ensure() jobs__ensure(jobs_st)
for i in 0 .. JOB_COUNT { for i in 0 .. JOB_COUNT {
let d = Jobs[i] let d = Jobs[i]
if jobs__st[i] != JOBS_ACTIVE or not jobs__takes(only, d.scope) { continue } if jobs_st.jobs__st[i] != JOBS_ACTIVE or not jobs__takes(only, d.scope) { continue }
if not jobs__matches(d.kind, d.param, kind, param) { continue } if not jobs__matches(d.kind, d.param, kind, param) { continue }
let was = jobs__hv[i] let was = jobs_st.jobs__hv[i]
jobs__hv[i] = was + n jobs_st.jobs__hv[i] = was + n
jobs__moved(i, was) jobs__moved(base_st, jobs_st, i, was)
} }
for b in 0 .. BOARD_COUNT { for b in 0 .. BOARD_COUNT {
if not jobs__takes(only, JobBoards[b].scope) { continue } if not jobs__takes(only, JobBoards[b].scope) { continue }
for s in 0 .. JobBoards[b].slots { for s in 0 .. JobBoards[b].slots {
let k = jobs__off[b] + s let k = jobs_st.jobs__off[b] + s
if jobs__ps[k] != JOBS_ACTIVE or not jobs__matches(jobs__pk[k], jobs__pp[k], kind, param) { continue } if jobs_st.jobs__ps[k] != JOBS_ACTIVE or not jobs__matches(jobs_st.jobs__pk[k], jobs_st.jobs__pp[k], kind, param) { continue }
jobs__post_moved(b, s, jobs__ph[k] + n) jobs__post_moved(base_st, jobs_st, b, s, jobs_st.jobs__ph[k] + n)
} }
} }
} }
function jobs__recheck_one(i: int) -> void { function jobs__recheck_one(base_st: mut BaseState, jobs_st: mut JobsState, i: int) -> void {
let d = Jobs[i] let d = Jobs[i]
if jobs__st[i] != JOBS_ACTIVE or d.kind < 0 or not JobsWorld.owns(d.scope) { return } if jobs_st.jobs__st[i] != JOBS_ACTIVE or d.kind < 0 or not JobsWorld.owns(d.scope) { return }
let e = JobsWorld.evidence(d.kind, d.param) let e = JobsWorld.evidence(d.kind, d.param)
if e <= jobs__hv[i] { return } if e <= jobs_st.jobs__hv[i] { return }
let was = jobs__hv[i] let was = jobs_st.jobs__hv[i]
jobs__hv[i] = e jobs_st.jobs__hv[i] = e
jobs__moved(i, was) jobs__moved(base_st, jobs_st, i, was)
} }
# every job under way against what the state already proves: work done before it was taken counts # every job under way against what the state already proves: work done before it was taken counts
# for a written job; a post counts only what came after it went up # for a written job; a post counts only what came after it went up
export function jobs_recheck() -> void { export function jobs_recheck(base_st: mut BaseState, jobs_st: mut JobsState) -> void {
jobs__ensure() jobs__ensure(jobs_st)
for i in 0 .. JOB_COUNT { jobs__recheck_one(i) } for i in 0 .. JOB_COUNT { jobs__recheck_one(base_st, jobs_st, i) }
for b in 0 .. BOARD_COUNT { for b in 0 .. BOARD_COUNT {
if not JobsWorld.owns(JobBoards[b].scope) { continue } if not JobsWorld.owns(JobBoards[b].scope) { continue }
for s in 0 .. JobBoards[b].slots { for s in 0 .. JobBoards[b].slots {
let k = jobs__off[b] + s let k = jobs_st.jobs__off[b] + s
if jobs__ps[k] != JOBS_ACTIVE or jobs__pk[k] < 0 { continue } if jobs_st.jobs__ps[k] != JOBS_ACTIVE or jobs_st.jobs__pk[k] < 0 { continue }
let e = JobsWorld.evidence(jobs__pk[k], jobs__pp[k]) let e = JobsWorld.evidence(jobs_st.jobs__pk[k], jobs_st.jobs__pp[k])
if e >= 0 and e - jobs__pb[k] > jobs__ph[k] { jobs__post_moved(b, s, e - jobs__pb[k]) } if e >= 0 and e - jobs_st.jobs__pb[k] > jobs_st.jobs__ph[k] { jobs__post_moved(base_st, jobs_st, b, s, e - jobs_st.jobs__pb[k]) }
} }
} }
} }
# offers newly open are said once; the first look after a reset says none of them # offers newly open are said once; the first look after a reset says none of them
function jobs__offers() -> void { function jobs__offers(base_st: mut BaseState, jobs_st: mut JobsState) -> void {
jobs__ensure() jobs__ensure(jobs_st)
for i in 0 .. JOB_COUNT { for i in 0 .. JOB_COUNT {
let now = jobs_offered(i) let now = jobs_offered(jobs_st, i)
if now and not jobs__seen[i] and jobs__primed { jobs__fact(JOBS_F_OFFERED, i, -1, -1, 0, Jobs[i].need) } if now and not jobs_st.jobs__seen[i] and jobs_st.jobs__primed { jobs__fact(base_st, jobs_st, JOBS_F_OFFERED, i, -1, -1, 0, Jobs[i].need) }
jobs__seen[i] = now jobs_st.jobs__seen[i] = now
} }
jobs__primed = true jobs_st.jobs__primed = true
} }

View file

@ -1,96 +1,96 @@
# jobs.ludic - the written jobs: offered by standing and story, taken, readied, handed in and paid # jobs.ludic - the written jobs: offered by standing and story, taken, readied, handed in and paid
export function jobs_ok(i: int) -> bool { return i >= 0 and i < JOB_COUNT } export function jobs_ok(i: int) -> bool { return i >= 0 and i < JOB_COUNT }
export function jobs_state(i: int) -> int { export function jobs_state(jobs_st: mut JobsState, i: int) -> int {
jobs__ensure() jobs__ensure(jobs_st)
return jobs__st[i] return jobs_st.jobs__st[i]
} }
export function jobs_have(i: int) -> int { export function jobs_have(jobs_st: mut JobsState, i: int) -> int {
jobs__ensure() jobs__ensure(jobs_st)
return jobs__hv[i] return jobs_st.jobs__hv[i]
} }
export function jobs_ready_now(i: int) -> bool { return jobs_state(i) == JOBS_READY } export function jobs_ready_now(jobs_st: mut JobsState, i: int) -> bool { return jobs_state(jobs_st, i) == JOBS_READY }
# taken and not yet paid # taken and not yet paid
export function jobs_under_way(i: int) -> bool { export function jobs_under_way(jobs_st: mut JobsState, i: int) -> bool {
let s = jobs_state(i) let s = jobs_state(jobs_st, i)
return s == JOBS_ACTIVE or s == JOBS_READY return s == JOBS_ACTIVE or s == JOBS_READY
} }
# on offer: not taken, the scope's standing and the story far enough, and the game allowing it # on offer: not taken, the scope's standing and the story far enough, and the game allowing it
export function jobs_offered(i: int) -> bool { export function jobs_offered(jobs_st: mut JobsState, i: int) -> bool {
let d = Jobs[i] let d = Jobs[i]
if jobs_state(i) != JOBS_OFFERED { return false } if jobs_state(jobs_st, i) != JOBS_OFFERED { return false }
return JobsWorld.rep(d.scope) >= d.minrep and JobsWorld.stage() >= d.minstage and JobsWorld.open(i) return JobsWorld.rep(d.scope) >= d.minrep and JobsWorld.stage() >= d.minstage and JobsWorld.open(i)
} }
export function jobs_take(i: int) -> bool { export function jobs_take(base_st: mut BaseState, jobs_st: mut JobsState, i: int) -> bool {
if not jobs_ok(i) or jobs_state(i) != JOBS_OFFERED { return false } if not jobs_ok(i) or jobs_state(jobs_st, i) != JOBS_OFFERED { return false }
jobs__st[i] = JOBS_ACTIVE jobs_st.jobs__st[i] = JOBS_ACTIVE
jobs__hv[i] = 0 jobs_st.jobs__hv[i] = 0
jobs__recheck_one(i) jobs__recheck_one(base_st, jobs_st, i)
return true return true
} }
# a count has moved: a job with a need is ready when it is met # a count has moved: a job with a need is ready when it is met
function jobs__moved(i: int, was: int) -> void { function jobs__moved(base_st: mut BaseState, jobs_st: mut JobsState, i: int, was: int) -> void {
let d = Jobs[i] let d = Jobs[i]
if d.need > 0 and jobs__hv[i] > d.need { jobs__hv[i] = d.need } if d.need > 0 and jobs_st.jobs__hv[i] > d.need { jobs_st.jobs__hv[i] = d.need }
if jobs__hv[i] > was { jobs__fact(JOBS_F_PROGRESSED, i, -1, -1, jobs__hv[i], d.need) } if jobs_st.jobs__hv[i] > was { jobs__fact(base_st, jobs_st, JOBS_F_PROGRESSED, i, -1, -1, jobs_st.jobs__hv[i], d.need) }
if jobs__st[i] == JOBS_ACTIVE and d.need > 0 and jobs__hv[i] >= d.need { jobs_ready(i) } if jobs_st.jobs__st[i] == JOBS_ACTIVE and d.need > 0 and jobs_st.jobs__hv[i] >= d.need { jobs_ready(base_st, jobs_st, i) }
} }
# the game's own play: a step counter, a mask of targets met # the game's own play: a step counter, a mask of targets met
export function jobs_set_have(i: int, v: int) -> void { export function jobs_set_have(base_st: mut BaseState, jobs_st: mut JobsState, i: int, v: int) -> void {
let was = jobs_have(i) let was = jobs_have(jobs_st, i)
jobs__hv[i] = v jobs_st.jobs__hv[i] = v
jobs__moved(i, was) jobs__moved(base_st, jobs_st, i, was)
} }
export function jobs_add_have(i: int, n: int) -> void { jobs_set_have(i, jobs_have(i) + n) } export function jobs_add_have(base_st: mut BaseState, jobs_st: mut JobsState, i: int, n: int) -> void { jobs_set_have(base_st, jobs_st, i, jobs_have(jobs_st, i) + n) }
export function jobs_mark(i: int, bit: int) -> void { jobs_set_have(i, jobs_have(i) | (1 << bit)) } export function jobs_mark(base_st: mut BaseState, jobs_st: mut JobsState, i: int, bit: int) -> void { jobs_set_have(base_st, jobs_st, i, jobs_have(jobs_st, i) | (1 << bit)) }
# everything in hand # everything in hand
export function jobs_ready(i: int) -> void { export function jobs_ready(base_st: mut BaseState, jobs_st: mut JobsState, i: int) -> void {
if jobs_state(i) != JOBS_ACTIVE { return } if jobs_state(jobs_st, i) != JOBS_ACTIVE { return }
jobs__st[i] = JOBS_READY jobs_st.jobs__st[i] = JOBS_READY
jobs__fact(JOBS_F_COMPLETED, i, -1, -1, jobs__hv[i], Jobs[i].need) jobs__fact(base_st, jobs_st, JOBS_F_COMPLETED, i, -1, -1, jobs_st.jobs__hv[i], Jobs[i].need)
} }
export function jobs_hand_in(i: int) -> bool { export function jobs_hand_in(base_st: mut BaseState, jobs_st: mut JobsState, i: int) -> bool {
if not jobs_ok(i) or jobs_state(i) != JOBS_READY { return false } if not jobs_ok(i) or jobs_state(jobs_st, i) != JOBS_READY { return false }
let d = Jobs[i] let d = Jobs[i]
jobs__st[i] = JOBS_DONE jobs_st.jobs__st[i] = JOBS_DONE
if d.group >= 0 and d.group < JOBS_GROUPS { jobs__done_n[d.group] += 1 } if d.group >= 0 and d.group < JOBS_GROUPS { jobs_st.jobs__done_n[d.group] += 1 }
JobsPay.reward(d.scope, d.money, d.rep) JobsPay.reward(d.scope, d.money, d.rep)
jobs__fact(JOBS_F_HANDED_IN, i, -1, -1, jobs__hv[i], d.need) jobs__fact(base_st, jobs_st, JOBS_F_HANDED_IN, i, -1, -1, jobs_st.jobs__hv[i], d.need)
return true return true
} }
# finished and paid in one go, whatever the count says # finished and paid in one go, whatever the count says
export function jobs_complete(i: int) -> bool { export function jobs_complete(base_st: mut BaseState, jobs_st: mut JobsState, i: int) -> bool {
if not jobs_ok(i) or not jobs_under_way(i) { return false } if not jobs_ok(i) or not jobs_under_way(jobs_st, i) { return false }
jobs_ready(i) jobs_ready(base_st, jobs_st, i)
return jobs_hand_in(i) return jobs_hand_in(base_st, jobs_st, i)
} }
# the state outright: the host's word for a party job, a test # the state outright: the host's word for a party job, a test
export function jobs_set(i: int, state: int, have: int) -> void { export function jobs_set(jobs_st: mut JobsState, i: int, state: int, have: int) -> void {
jobs__ensure() jobs__ensure(jobs_st)
jobs__st[i] = state jobs_st.jobs__st[i] = state
jobs__hv[i] = have jobs_st.jobs__hv[i] = have
} }
export function jobs_done(group: int) -> int { export function jobs_done(jobs_st: mut JobsState, group: int) -> int {
jobs__ensure() jobs__ensure(jobs_st)
if group < 0 or group >= JOBS_GROUPS { return 0 } if group < 0 or group >= JOBS_GROUPS { return 0 }
return jobs__done_n[group] return jobs_st.jobs__done_n[group]
} }
export function jobs_set_done(group: int, n: int) -> void { export function jobs_set_done(jobs_st: mut JobsState, group: int, n: int) -> void {
jobs__ensure() jobs__ensure(jobs_st)
if group >= 0 and group < JOBS_GROUPS { jobs__done_n[group] = n } if group >= 0 and group < JOBS_GROUPS { jobs_st.jobs__done_n[group] = n }
} }

View file

@ -1,36 +1,36 @@
# lists.ludic - the written jobs as a screen lists them: a vendor's, the taken, the ones to show # lists.ludic - the written jobs as a screen lists them: a vendor's, the taken, the ones to show
# a vendor's offers and the ones it can take back, in a group (-1 every vendor) # a vendor's offers and the ones it can take back, in a group (-1 every vendor)
export function jobs_at(group: int, vendor: int) -> []int { export function jobs_at(jobs_st: mut JobsState, group: int, vendor: int) -> []int {
let out = new []int let out = new []int
for i in 0 .. JOB_COUNT { for i in 0 .. JOB_COUNT {
let d = Jobs[i] let d = Jobs[i]
if d.group != group or (vendor >= 0 and d.vendor != vendor) { continue } if d.group != group or (vendor >= 0 and d.vendor != vendor) { continue }
let s = jobs_state(i) let s = jobs_state(jobs_st, i)
if s == JOBS_DONE or (s == JOBS_OFFERED and not jobs_offered(i)) { continue } if s == JOBS_DONE or (s == JOBS_OFFERED and not jobs_offered(jobs_st, i)) { continue }
push(out, i) push(out, i)
} }
return out return out
} }
# every job of a group taken at some point, done ones too # every job of a group taken at some point, done ones too
export function jobs_taken(group: int) -> []int { export function jobs_taken(jobs_st: mut JobsState, group: int) -> []int {
let out = new []int let out = new []int
for i in 0 .. JOB_COUNT { if Jobs[i].group == group and jobs_state(i) != JOBS_OFFERED { push(out, i) } } for i in 0 .. JOB_COUNT { if Jobs[i].group == group and jobs_state(jobs_st, i) != JOBS_OFFERED { push(out, i) } }
return out return out
} }
# the first `most` of a group under way # the first `most` of a group under way
export function jobs_shown(group: int, most: int) -> []int { export function jobs_shown(jobs_st: mut JobsState, group: int, most: int) -> []int {
let out = new []int let out = new []int
for i in 0 .. JOB_COUNT { for i in 0 .. JOB_COUNT {
if len(out) >= most { break } if len(out) >= most { break }
if Jobs[i].group == group and jobs_under_way(i) { push(out, i) } if Jobs[i].group == group and jobs_under_way(jobs_st, i) { push(out, i) }
} }
return out return out
} }
# the first under way and not yet met, or -1 # the first under way and not yet met, or -1
export function jobs_first_open(group: int) -> int { export function jobs_first_open(jobs_st: mut JobsState, group: int) -> int {
for i in 0 .. JOB_COUNT { if Jobs[i].group == group and jobs_state(i) == JOBS_ACTIVE { return i } } for i in 0 .. JOB_COUNT { if Jobs[i].group == group and jobs_state(jobs_st, i) == JOBS_ACTIVE { return i } }
return -1 return -1
} }

View file

@ -1,12 +1,12 @@
# posts.ludic - what is on a board, slot by slot, and a board as the host says it # posts.ludic - what is on a board, slot by slot, and a board as the host says it
export function jobs_post_kind(b: int, s: int) -> int { return jobs__pk[jobs__at(b, s)] } export function jobs_post_kind(jobs_st: mut JobsState, b: int, s: int) -> int { return jobs_st.jobs__pk[jobs__at(jobs_st, b, s)] }
export function jobs_post_param(b: int, s: int) -> int { return jobs__pp[jobs__at(b, s)] } export function jobs_post_param(jobs_st: mut JobsState, b: int, s: int) -> int { return jobs_st.jobs__pp[jobs__at(jobs_st, b, s)] }
export function jobs_post_need(b: int, s: int) -> int { return jobs__pn[jobs__at(b, s)] } export function jobs_post_need(jobs_st: mut JobsState, b: int, s: int) -> int { return jobs_st.jobs__pn[jobs__at(jobs_st, b, s)] }
export function jobs_post_have(b: int, s: int) -> int { return jobs__ph[jobs__at(b, s)] } export function jobs_post_have(jobs_st: mut JobsState, b: int, s: int) -> int { return jobs_st.jobs__ph[jobs__at(jobs_st, b, s)] }
export function jobs_post_money(b: int, s: int) -> int { return jobs__pm[jobs__at(b, s)] } export function jobs_post_money(jobs_st: mut JobsState, b: int, s: int) -> int { return jobs_st.jobs__pm[jobs__at(jobs_st, b, s)] }
export function jobs_post_rep(b: int, s: int) -> int { return jobs__pr[jobs__at(b, s)] } export function jobs_post_rep(jobs_st: mut JobsState, b: int, s: int) -> int { return jobs_st.jobs__pr[jobs__at(jobs_st, b, s)] }
export function jobs_post_state(b: int, s: int) -> int { return jobs__ps[jobs__at(b, s)] } export function jobs_post_state(jobs_st: mut JobsState, b: int, s: int) -> int { return jobs_st.jobs__ps[jobs__at(jobs_st, b, s)] }
export function jobs_post_day(b: int, s: int) -> int { return jobs__pd[jobs__at(b, s)] } export function jobs_post_day(jobs_st: mut JobsState, b: int, s: int) -> int { return jobs_st.jobs__pd[jobs__at(jobs_st, b, s)] }
export function jobs_slots(b: int) -> int { export function jobs_slots(b: int) -> int {
if not jobs_board_ok(b) { return 0 } if not jobs_board_ok(b) { return 0 }
@ -14,38 +14,38 @@ export function jobs_slots(b: int) -> int {
} }
# the slots with something posted, in order # the slots with something posted, in order
export function jobs_posted(b: int) -> []int { export function jobs_posted(jobs_st: mut JobsState, b: int) -> []int {
let out = new []int let out = new []int
for s in 0 .. jobs_slots(b) { if jobs_post_kind(b, s) >= 0 { push(out, s) } } for s in 0 .. jobs_slots(b) { if jobs_post_kind(jobs_st, b, s) >= 0 { push(out, s) } }
return out return out
} }
# how many of a board's posts are paid # how many of a board's posts are paid
export function jobs_posts_done(b: int) -> int { export function jobs_posts_done(jobs_st: mut JobsState, b: int) -> int {
var n = 0 var n = 0
for s in 0 .. jobs_slots(b) { if jobs_post_kind(b, s) >= 0 and jobs_post_state(b, s) == JOBS_DONE { n += 1 } } for s in 0 .. jobs_slots(b) { if jobs_post_kind(jobs_st, b, s) >= 0 and jobs_post_state(jobs_st, b, s) == JOBS_DONE { n += 1 } }
return n return n
} }
export function jobs_total(b: int) -> int { export function jobs_total(jobs_st: mut JobsState, b: int) -> int {
jobs__ensure() jobs__ensure(jobs_st)
return jobs__total[b] return jobs_st.jobs__total[b]
} }
# a slot outright: the host's board on a guest, a test # a slot outright: the host's board on a guest, a test
export function jobs_post_set(b: int, s: int, kind: int, param: int, need: int, have: int, money: int, rep: int, state: int, day: int) -> void { export function jobs_post_set(jobs_st: mut JobsState, b: int, s: int, kind: int, param: int, need: int, have: int, money: int, rep: int, state: int, day: int) -> void {
let k = jobs__at(b, s) let k = jobs__at(jobs_st, b, s)
jobs__pk[k] = kind jobs_st.jobs__pk[k] = kind
jobs__pp[k] = param jobs_st.jobs__pp[k] = param
jobs__pn[k] = need jobs_st.jobs__pn[k] = need
jobs__ph[k] = have jobs_st.jobs__ph[k] = have
jobs__pm[k] = money jobs_st.jobs__pm[k] = money
jobs__pr[k] = rep jobs_st.jobs__pr[k] = rep
jobs__ps[k] = state jobs_st.jobs__ps[k] = state
jobs__pd[k] = day jobs_st.jobs__pd[k] = day
} }
export function jobs_set_total(b: int, n: int) -> void { export function jobs_set_total(jobs_st: mut JobsState, b: int, n: int) -> void {
jobs__ensure() jobs__ensure(jobs_st)
jobs__total[b] = n jobs_st.jobs__total[b] = n
} }

View file

@ -1,39 +1,39 @@
# save.ludic - a scope's jobs and boards in a section of their own, by key: a job the game has since # save.ludic - a scope's jobs and boards in a section of their own, by key: a job the game has since
# removed is dropped, a new one starts offered # removed is dropped, a new one starts offered
function jobs__reset_scope(sc: int) -> void { function jobs__reset_scope(jobs_st: mut JobsState, sc: int) -> void {
jobs__ensure() jobs__ensure(jobs_st)
for i in 0 .. JOB_COUNT { for i in 0 .. JOB_COUNT {
if Jobs[i].scope != sc { continue } if Jobs[i].scope != sc { continue }
jobs__st[i] = JOBS_OFFERED jobs_st.jobs__st[i] = JOBS_OFFERED
jobs__hv[i] = 0 jobs_st.jobs__hv[i] = 0
} }
for b in 0 .. BOARD_COUNT { for b in 0 .. BOARD_COUNT {
if JobBoards[b].scope != sc { continue } if JobBoards[b].scope != sc { continue }
for s in 0 .. JobBoards[b].slots { jobs__clear(jobs__off[b] + s) } for s in 0 .. JobBoards[b].slots { jobs__clear(jobs_st, jobs_st.jobs__off[b] + s) }
jobs__total[b] = 0 jobs_st.jobs__total[b] = 0
jobs__serial[b] = 0 jobs_st.jobs__serial[b] = 0
} }
jobs__primed = false jobs_st.jobs__primed = false
} }
function jobs__save_scope(sc: int) -> Val { function jobs__save_scope(jobs_st: JobsState, sc: int) -> Val {
let v = Value.object() let v = Value.object()
let js = Value.object() let js = Value.object()
for i in 0 .. JOB_COUNT { for i in 0 .. JOB_COUNT {
if Jobs[i].scope != sc or jobs__st[i] == JOBS_OFFERED { continue } if Jobs[i].scope != sc or jobs_st.jobs__st[i] == JOBS_OFFERED { continue }
sv_put_ints(js, Jobs[i].key, [jobs__st[i], jobs__hv[i]]) sv_put_ints(js, Jobs[i].key, [jobs_st.jobs__st[i], jobs_st.jobs__hv[i]])
} }
Value.put(v, "jobs", js) Value.put(v, "jobs", js)
let bs = Value.object() let bs = Value.object()
for b in 0 .. BOARD_COUNT { for b in 0 .. BOARD_COUNT {
if JobBoards[b].scope != sc { continue } if JobBoards[b].scope != sc { continue }
let o = Value.object() let o = Value.object()
sv_put_int(o, "total", jobs__total[b]) sv_put_int(o, "total", jobs_st.jobs__total[b])
sv_put_int(o, "serial", jobs__serial[b]) sv_put_int(o, "serial", jobs_st.jobs__serial[b])
let slots = new []int let slots = new []int
for s in 0 .. JobBoards[b].slots { for s in 0 .. JobBoards[b].slots {
let k = jobs__off[b] + s let k = jobs_st.jobs__off[b] + s
let row = [jobs__pk[k], jobs__pp[k], jobs__pn[k], jobs__ph[k], jobs__pb[k], jobs__pm[k], jobs__pr[k], jobs__ps[k], jobs__pd[k]] let row = [jobs_st.jobs__pk[k], jobs_st.jobs__pp[k], jobs_st.jobs__pn[k], jobs_st.jobs__ph[k], jobs_st.jobs__pb[k], jobs_st.jobs__pm[k], jobs_st.jobs__pr[k], jobs_st.jobs__ps[k], jobs_st.jobs__pd[k]]
for f in 0 .. len(row) { push(slots, row[f]) } for f in 0 .. len(row) { push(slots, row[f]) }
} }
sv_put_ints(o, "slots", slots) sv_put_ints(o, "slots", slots)
@ -43,13 +43,13 @@ function jobs__save_scope(sc: int) -> Val {
return v return v
} }
function jobs__load_scope(sc: int, v: Val) -> void { function jobs__load_scope(jobs_st: mut JobsState, sc: int, v: Val) -> void {
jobs__reset_scope(sc) jobs__reset_scope(jobs_st, sc)
if Value.has(v, "jobs") != 0 { if Value.has(v, "jobs") != 0 {
let js = Value.get(v, "jobs") let js = Value.get(v, "jobs")
for i in 0 .. JOB_COUNT { for i in 0 .. JOB_COUNT {
let p = sv_ints(js, Jobs[i].key) let p = sv_ints(js, Jobs[i].key)
if Jobs[i].scope == sc and len(p) == 2 { jobs_set(i, p[0], p[1]) } if Jobs[i].scope == sc and len(p) == 2 { jobs_set(jobs_st, i, p[0], p[1]) }
} }
} }
if Value.has(v, "boards") == 0 { return } if Value.has(v, "boards") == 0 { return }
@ -57,15 +57,15 @@ function jobs__load_scope(sc: int, v: Val) -> void {
for b in 0 .. BOARD_COUNT { for b in 0 .. BOARD_COUNT {
if JobBoards[b].scope != sc or Value.has(bs, JobBoards[b].key) == 0 { continue } if JobBoards[b].scope != sc or Value.has(bs, JobBoards[b].key) == 0 { continue }
let o = Value.get(bs, JobBoards[b].key) let o = Value.get(bs, JobBoards[b].key)
jobs__total[b] = sv_int(o, "total", 0) jobs_st.jobs__total[b] = sv_int(o, "total", 0)
jobs__serial[b] = sv_int(o, "serial", 0) jobs_st.jobs__serial[b] = sv_int(o, "serial", 0)
let r = sv_ints(o, "slots") let r = sv_ints(o, "slots")
for s in 0 .. JobBoards[b].slots { for s in 0 .. JobBoards[b].slots {
if s * 9 + 8 >= len(r) { break } if s * 9 + 8 >= len(r) { break }
let k = jobs__off[b] + s let k = jobs_st.jobs__off[b] + s
let q = s * 9 let q = s * 9
jobs_post_set(b, s, r[q], r[q + 1], r[q + 2], r[q + 3], r[q + 5], r[q + 6], r[q + 7], r[q + 8]) jobs_post_set(jobs_st, b, s, r[q], r[q + 1], r[q + 2], r[q + 3], r[q + 5], r[q + 6], r[q + 7], r[q + 8])
jobs__pb[k] = r[q + 4] jobs_st.jobs__pb[k] = r[q + 4]
} }
} }
} }

View file

@ -1,28 +1,30 @@
# state.ludic - each job's state and count, the posts on the boards, the done counts, the dice and # state.ludic - each job's state and count, the posts on the boards, the done counts, the dice and
# the facts. Made on first use, so the registries are complete by then. # the facts. Made on first use, so the registries are complete by then.
var jobs__st: []int = null export state JobsState {
var jobs__hv: []int = null jobs__st: []int = null
var jobs__seen: []bool = null # an offer already said jobs__hv: []int = null
var jobs__primed: bool = false # the first tick after a reset learns the offers quietly jobs__seen: []bool = null # an offer already said
var jobs__done_n: []int = null # handed in, per group jobs__primed: bool = false # the first tick after a reset learns the offers quietly
jobs__done_n: []int = null # handed in, per group
jobs__off: []int = null
jobs__pk: []int = null # kind, -1 empty
jobs__pp: []int = null
jobs__pn: []int = null
jobs__ph: []int = null
jobs__pb: []int = null # the evidence when it went up
jobs__pm: []int = null
jobs__pr: []int = null
jobs__ps: []int = null
jobs__pd: []int = null # the day it went up
jobs__total: []int = null # handed in, per board
jobs__serial: []int = null # posts made, per board: each roll its own seed
jobs__seed: int = 1000
jobs__dice: Rng = null
jobs__q: Queue<JobsFact> = null
}
# the posts, every board's slots end to end from jobs__off[b] # the posts, every board's slots end to end from jobs__off[b]
var jobs__off: []int = null
var jobs__pk: []int = null # kind, -1 empty
var jobs__pp: []int = null
var jobs__pn: []int = null
var jobs__ph: []int = null
var jobs__pb: []int = null # the evidence when it went up
var jobs__pm: []int = null
var jobs__pr: []int = null
var jobs__ps: []int = null
var jobs__pd: []int = null # the day it went up
var jobs__total: []int = null # handed in, per board
var jobs__serial: []int = null # posts made, per board: each roll its own seed
var jobs__seed: int = 1000
var jobs__dice: Rng = null
var jobs__q: Queue<JobsFact> = null
function jobs__ints(n: int, v: int) -> []int { function jobs__ints(n: int, v: int) -> []int {
let out = new []int let out = new []int
@ -30,39 +32,39 @@ function jobs__ints(n: int, v: int) -> []int {
return out return out
} }
function jobs__ensure() -> void { function jobs__ensure(jobs_st: mut JobsState) -> void {
if jobs__st != null { return } if jobs_st.jobs__st != null { return }
jobs__st = jobs__ints(JOB_COUNT, JOBS_OFFERED) jobs_st.jobs__st = jobs__ints(JOB_COUNT, JOBS_OFFERED)
jobs__hv = jobs__ints(JOB_COUNT, 0) jobs_st.jobs__hv = jobs__ints(JOB_COUNT, 0)
jobs__seen = new []bool jobs_st.jobs__seen = new []bool
for i in 0 .. JOB_COUNT { push(jobs__seen, false) } for i in 0 .. JOB_COUNT { push(jobs_st.jobs__seen, false) }
jobs__done_n = jobs__ints(JOBS_GROUPS, 0) jobs_st.jobs__done_n = jobs__ints(JOBS_GROUPS, 0)
jobs__off = new []int jobs_st.jobs__off = new []int
var n = 0 var n = 0
for b in 0 .. BOARD_COUNT { for b in 0 .. BOARD_COUNT {
push(jobs__off, n) push(jobs_st.jobs__off, n)
n += JobBoards[b].slots n += JobBoards[b].slots
} }
jobs__pk = jobs__ints(n, -1) jobs_st.jobs__pk = jobs__ints(n, -1)
jobs__pp = jobs__ints(n, -1) jobs_st.jobs__pp = jobs__ints(n, -1)
jobs__pn = jobs__ints(n, 0) jobs_st.jobs__pn = jobs__ints(n, 0)
jobs__ph = jobs__ints(n, 0) jobs_st.jobs__ph = jobs__ints(n, 0)
jobs__pb = jobs__ints(n, 0) jobs_st.jobs__pb = jobs__ints(n, 0)
jobs__pm = jobs__ints(n, 0) jobs_st.jobs__pm = jobs__ints(n, 0)
jobs__pr = jobs__ints(n, 0) jobs_st.jobs__pr = jobs__ints(n, 0)
jobs__ps = jobs__ints(n, JOBS_OFFERED) jobs_st.jobs__ps = jobs__ints(n, JOBS_OFFERED)
jobs__pd = jobs__ints(n, 0) jobs_st.jobs__pd = jobs__ints(n, 0)
jobs__total = jobs__ints(BOARD_COUNT, 0) jobs_st.jobs__total = jobs__ints(BOARD_COUNT, 0)
jobs__serial = jobs__ints(BOARD_COUNT, 0) jobs_st.jobs__serial = jobs__ints(BOARD_COUNT, 0)
jobs__dice = rng_new(jobs__seed) jobs_st.jobs__dice = rng_new(jobs_st.jobs__seed)
} }
export function jobs_facts() -> Queue<JobsFact> { export function jobs_facts(base_st: mut BaseState, jobs_st: mut JobsState) -> Queue<JobsFact> {
if jobs__q == null { jobs__q = queue_new("jobs.facts") } if jobs_st.jobs__q == null { jobs_st.jobs__q = queue_new(base_st, "jobs.facts") }
return jobs__q return jobs_st.jobs__q
} }
function jobs__fact(what: int, job: int, b: int, s: int, have: int, need: int) -> void { function jobs__fact(base_st: mut BaseState, jobs_st: mut JobsState, what: int, job: int, b: int, s: int, have: int, need: int) -> void {
let f = new JobsFact let f = new JobsFact
f.what = what f.what = what
f.job = job f.job = job
@ -77,17 +79,17 @@ function jobs__fact(what: int, job: int, b: int, s: int, have: int, need: int) -
f.rep = Jobs[job].rep f.rep = Jobs[job].rep
f.scope = Jobs[job].scope f.scope = Jobs[job].scope
} else { } else {
f.kind = jobs__pk[jobs__off[b] + s] f.kind = jobs_st.jobs__pk[jobs_st.jobs__off[b] + s]
f.param = jobs__pp[jobs__off[b] + s] f.param = jobs_st.jobs__pp[jobs_st.jobs__off[b] + s]
f.money = jobs__pm[jobs__off[b] + s] f.money = jobs_st.jobs__pm[jobs_st.jobs__off[b] + s]
f.rep = jobs__pr[jobs__off[b] + s] f.rep = jobs_st.jobs__pr[jobs_st.jobs__off[b] + s]
f.scope = JobBoards[b].scope f.scope = JobBoards[b].scope
} }
q_push(jobs_facts(), f) q_push(base_st, jobs_facts(base_st, jobs_st), f)
} }
# the base of every roll: a post's seed is this, the day, the board, the slot and how many before # the base of every roll: a post's seed is this, the day, the board, the slot and how many before
export function jobs_config_seed(seed: int) -> void { export function jobs_config_seed(jobs_st: mut JobsState, seed: int) -> void {
jobs__seed = seed jobs_st.jobs__seed = seed
jobs__dice = rng_new(seed) jobs_st.jobs__dice = rng_new(seed)
} }

View file

@ -1,39 +1,39 @@
# system.ludic - two systems, one per owner: "jobs" is this player's (personal jobs and boards, the # system.ludic - two systems, one per owner: "jobs" is this player's (personal jobs and boards, the
# done counts, the tick), "jobs.party" the party's, saved with the world # done counts, the tick), "jobs.party" the party's, saved with the world
export function jobs_reset() -> void { export function jobs_reset(base_st: mut BaseState, jobs_st: mut JobsState) -> void {
jobs__reset_scope(JOBS_SELF) jobs__reset_scope(jobs_st, JOBS_SELF)
for g in 0 .. JOBS_GROUPS { jobs__done_n[g] = 0 } for g in 0 .. JOBS_GROUPS { jobs_st.jobs__done_n[g] = 0 }
q_clear(jobs_facts()) q_clear(base_st, jobs_facts(base_st, jobs_st))
} }
export function jobs_party_reset() -> void { jobs__reset_scope(JOBS_PARTY) } export function jobs_party_reset(jobs_st: mut JobsState) -> void { jobs__reset_scope(jobs_st, JOBS_PARTY) }
export function jobs_save() -> Val { export function jobs_save(jobs_st: mut JobsState) -> Val {
jobs__ensure() jobs__ensure(jobs_st)
let v = jobs__save_scope(JOBS_SELF) let v = jobs__save_scope(jobs_st, JOBS_SELF)
sv_put_ints(v, "done", jobs__done_n) sv_put_ints(v, "done", jobs_st.jobs__done_n)
return v return v
} }
export function jobs_load(v: Val, version: int) -> void { export function jobs_load(jobs_st: mut JobsState, v: Val, version: int) -> void {
jobs__load_scope(JOBS_SELF, v) jobs__load_scope(jobs_st, JOBS_SELF, v)
let d = sv_ints(v, "done") let d = sv_ints(v, "done")
for g in 0 .. JOBS_GROUPS { for g in 0 .. JOBS_GROUPS {
jobs__done_n[g] = 0 jobs_st.jobs__done_n[g] = 0
if g < len(d) { jobs__done_n[g] = d[g] } if g < len(d) { jobs_st.jobs__done_n[g] = d[g] }
} }
} }
export function jobs_party_save() -> Val { export function jobs_party_save(jobs_st: mut JobsState) -> Val {
jobs__ensure() jobs__ensure(jobs_st)
return jobs__save_scope(JOBS_PARTY) return jobs__save_scope(jobs_st, JOBS_PARTY)
} }
export function jobs_party_load(v: Val, version: int) -> void { jobs__load_scope(JOBS_PARTY, v) } export function jobs_party_load(jobs_st: mut JobsState, v: Val, version: int) -> void { jobs__load_scope(jobs_st, JOBS_PARTY, v) }
function jobs__tick(t: Tick) -> void { function jobs__tick(base_st: mut BaseState, jobs_st: mut JobsState, t: Tick) -> void {
jobs__offers() jobs__offers(base_st, jobs_st)
jobs_recheck() jobs_recheck(base_st, jobs_st)
} }
export function jobs_system() -> System { export function jobs_system() -> System {

View file

@ -15,50 +15,52 @@ program JobsTest {
def JobBoards daily { slots: 3, keep: 1, auto: true, refill: false } def JobBoards daily { slots: 3, keep: 1, auto: true, refill: false }
def JobBoards ada { slots: 2, keep: 7, scope: JOBS_PARTY } def JobBoards ada { slots: 2, keep: 7, scope: JOBS_PARTY }
var rep: int = 0 state JobstestState {
var stage: int = 0 rep: int = 0
var day: int = 1 stage: int = 0
var guest: bool = false day: int = 1
var fish: int = 0 # the state's count of fish guest: bool = false
var paid: int = 0 fish: int = 0 # the state's count of fish
var paid_rep: int = 0 paid: int = 0
var paid_scope: int = -1 paid_rep: int = 0
paid_scope: int = -1
}
function world_rep(sc: int) -> int { return rep } function world_rep(jobstest_st: JobstestState, sc: int) -> int { return jobstest_st.rep }
function world_stage() -> int { return stage } function world_stage(jobstest_st: JobstestState) -> int { return jobstest_st.stage }
function world_day() -> int { return day } function world_day(jobstest_st: JobstestState) -> int { return jobstest_st.day }
function world_owns(sc: int) -> bool { return sc == JOBS_SELF or not guest } function world_owns(jobstest_st: JobstestState, sc: int) -> bool { return sc == JOBS_SELF or not jobstest_st.guest }
function world_evidence(kind: int, param: int) -> int { function world_evidence(jobstest_st: JobstestState, kind: int, param: int) -> int {
if kind == K_FISH { return fish } if kind == K_FISH { return jobstest_st.fish }
return -1 return -1
} }
function world_roll(b: int, s: int) -> void { function world_roll(jobs_st: mut JobsState, b: int, s: int) -> void {
let need = jobs_roll(2, 4) let need = jobs_roll(jobs_st, 2, 4)
if b == BOARD_DAILY { jobs_post(b, s, K_WOOD, -1, need, 7, 2) } else { jobs_post(b, s, K_FISH, -1, need, 9 * need, 3) } if b == BOARD_DAILY { jobs_post(jobs_st, b, s, K_WOOD, -1, need, 7, 2) } else { jobs_post(jobs_st, b, s, K_FISH, -1, need, 9 * need, 3) }
} }
function pay(sc: int, money: int, r: int) -> void { function pay(jobstest_st: mut JobstestState, sc: int, money: int, r: int) -> void {
paid += money jobstest_st.paid += money
paid_rep += r jobstest_st.paid_rep += r
paid_scope = sc jobstest_st.paid_scope = sc
} }
bind JobsWorld { rep: fn world_rep, stage: fn world_stage, day: fn world_day, owns: fn world_owns, evidence: fn world_evidence, roll: fn world_roll } bind JobsWorld { rep: fn world_rep, stage: fn world_stage, day: fn world_day, owns: fn world_owns, evidence: fn world_evidence, roll: fn world_roll }
bind JobsPay { reward: fn pay } bind JobsPay { reward: fn pay }
function fresh() -> void { function fresh(base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) -> void {
rep = 0 jobstest_st.rep = 0
stage = 0 jobstest_st.stage = 0
day = 1 jobstest_st.day = 1
guest = false jobstest_st.guest = false
fish = 0 jobstest_st.fish = 0
paid = 0 jobstest_st.paid = 0
paid_rep = 0 jobstest_st.paid_rep = 0
jobs_reset() jobs_reset(base_st, jobs_st)
jobs_party_reset() jobs_party_reset(jobs_st)
} }
function count_of(what: int) -> int { function count_of(base_st: mut BaseState, jobs_st: mut JobsState, what: int) -> int {
let fs = q_drain(jobs_facts()) let fs = q_drain(base_st, jobs_facts(base_st, jobs_st))
var n = 0 var n = 0
for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } } for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } }
return n return n
@ -70,166 +72,166 @@ program JobsTest {
expect_eq(BOARD_COUNT, 2) expect_eq(BOARD_COUNT, 2)
} }
test "a gate of standing and story" { test "a gate of standing and story" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
fresh() fresh(base_st, jobs_st, jobstest_st)
expect(jobs_offered(JOB_CATCH)) expect(jobs_offered(jobs_st, JOB_CATCH))
expect(not jobs_offered(JOB_DEER)) expect(not jobs_offered(jobs_st, JOB_DEER))
rep = 10 jobstest_st.rep = 10
expect(jobs_offered(JOB_DEER)) expect(jobs_offered(jobs_st, JOB_DEER))
expect(not jobs_offered(JOB_STILL)) expect(not jobs_offered(jobs_st, JOB_STILL))
stage = 2 jobstest_st.stage = 2
expect(jobs_offered(JOB_STILL)) expect(jobs_offered(jobs_st, JOB_STILL))
expect_eq(len(jobs_at(0, -1)), 3) expect_eq(len(jobs_at(jobs_st, 0, -1)), 3)
} }
test "counted by events, readied, handed in and paid" { test "counted by events, readied, handed in and paid" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
fresh() fresh(base_st, jobs_st, jobstest_st)
jobs_count(K_FISH, 0, 1) jobs_count(base_st, jobs_st, K_FISH, 0, 1)
expect_eq(jobs_have(JOB_CATCH), 0) expect_eq(jobs_have(jobs_st, JOB_CATCH), 0)
expect(jobs_take(JOB_CATCH)) expect(jobs_take(base_st, jobs_st, JOB_CATCH))
jobs_count(K_FISH, 0, 2) jobs_count(base_st, jobs_st, K_FISH, 0, 2)
expect_eq(jobs_state(JOB_CATCH), JOBS_ACTIVE) expect_eq(jobs_state(jobs_st, JOB_CATCH), JOBS_ACTIVE)
jobs_count(K_FISH, 0, 5) jobs_count(base_st, jobs_st, K_FISH, 0, 5)
expect_eq(jobs_have(JOB_CATCH), 3) expect_eq(jobs_have(jobs_st, JOB_CATCH), 3)
expect(jobs_ready_now(JOB_CATCH)) expect(jobs_ready_now(jobs_st, JOB_CATCH))
expect_eq(count_of(JOBS_F_COMPLETED), 1) expect_eq(count_of(base_st, jobs_st, JOBS_F_COMPLETED), 1)
expect(jobs_hand_in(JOB_CATCH)) expect(jobs_hand_in(base_st, jobs_st, JOB_CATCH))
expect(not jobs_hand_in(JOB_CATCH)) expect(not jobs_hand_in(base_st, jobs_st, JOB_CATCH))
expect_eq(paid, 30) expect_eq(jobstest_st.paid, 30)
expect_eq(paid_rep, 5) expect_eq(jobstest_st.paid_rep, 5)
expect_eq(jobs_done(0), 1) expect_eq(jobs_done(jobs_st, 0), 1)
expect_eq(count_of(JOBS_F_HANDED_IN), 1) expect_eq(count_of(base_st, jobs_st, JOBS_F_HANDED_IN), 1)
} }
test "the state already proves the work" { test "the state already proves the work" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
fresh() fresh(base_st, jobs_st, jobstest_st)
fish = 2 jobstest_st.fish = 2
jobs_take(JOB_CATCH) jobs_take(base_st, jobs_st, JOB_CATCH)
expect_eq(jobs_have(JOB_CATCH), 2) expect_eq(jobs_have(jobs_st, JOB_CATCH), 2)
fish = 3 jobstest_st.fish = 3
jobs_recheck() jobs_recheck(base_st, jobs_st)
expect(jobs_ready_now(JOB_CATCH)) expect(jobs_ready_now(jobs_st, JOB_CATCH))
} }
test "a param narrows the count" { test "a param narrows the count" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
fresh() fresh(base_st, jobs_st, jobstest_st)
rep = 10 jobstest_st.rep = 10
jobs_take(JOB_DEER) jobs_take(base_st, jobs_st, JOB_DEER)
jobs_count(K_PHOTO, 3, 1) jobs_count(base_st, jobs_st, K_PHOTO, 3, 1)
expect_eq(jobs_have(JOB_DEER), 0) expect_eq(jobs_have(jobs_st, JOB_DEER), 0)
jobs_count(K_PHOTO, 4, 1) jobs_count(base_st, jobs_st, K_PHOTO, 4, 1)
expect(jobs_ready_now(JOB_DEER)) expect(jobs_ready_now(jobs_st, JOB_DEER))
} }
test "the game's own play readies a job with no need" { test "the game's own play readies a job with no need" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
fresh() fresh(base_st, jobs_st, jobstest_st)
jobs_take(JOB_STILL) jobs_take(base_st, jobs_st, JOB_STILL)
jobs_mark(JOB_STILL, 2) jobs_mark(base_st, jobs_st, JOB_STILL, 2)
expect_eq(jobs_have(JOB_STILL), 4) expect_eq(jobs_have(jobs_st, JOB_STILL), 4)
expect_eq(jobs_state(JOB_STILL), JOBS_ACTIVE) expect_eq(jobs_state(jobs_st, JOB_STILL), JOBS_ACTIVE)
expect(jobs_complete(JOB_STILL)) expect(jobs_complete(base_st, jobs_st, JOB_STILL))
expect_eq(paid, 60) expect_eq(jobstest_st.paid, 60)
expect_eq(jobs_done(1), 1) expect_eq(jobs_done(jobs_st, 1), 1)
} }
test "a guest counts its own and leaves the party's to the host" { test "a guest counts its own and leaves the party's to the host" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
fresh() fresh(base_st, jobs_st, jobstest_st)
guest = true jobstest_st.guest = true
jobs_take(JOB_TEN) jobs_take(base_st, jobs_st, JOB_TEN)
jobs_take(JOB_CATCH) jobs_take(base_st, jobs_st, JOB_CATCH)
jobs_count(K_FISH, 0, 2) jobs_count(base_st, jobs_st, K_FISH, 0, 2)
expect_eq(jobs_have(JOB_TEN), 0) expect_eq(jobs_have(jobs_st, JOB_TEN), 0)
expect_eq(jobs_have(JOB_CATCH), 2) expect_eq(jobs_have(jobs_st, JOB_CATCH), 2)
jobs_count_in(JOBS_PARTY, K_FISH, 0, 4) jobs_count_in(base_st, jobs_st, JOBS_PARTY, K_FISH, 0, 4)
expect_eq(jobs_have(JOB_TEN), 4) expect_eq(jobs_have(jobs_st, JOB_TEN), 4)
expect_eq(jobs_have(JOB_CATCH), 2) expect_eq(jobs_have(jobs_st, JOB_CATCH), 2)
} }
test "an offer is said once it opens, never for what was open at the reset" { test "an offer is said once it opens, never for what was open at the reset" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
fresh() fresh(base_st, jobs_st, jobstest_st)
jobs_system().tick(tick_new(0.1, 1, 0.0)) jobs_system().tick(tick_new(0.1, 1, 0.0))
expect_eq(count_of(JOBS_F_OFFERED), 0) expect_eq(count_of(base_st, jobs_st, JOBS_F_OFFERED), 0)
rep = 10 jobstest_st.rep = 10
jobs_system().tick(tick_new(0.1, 2, 0.0)) jobs_system().tick(tick_new(0.1, 2, 0.0))
expect_eq(count_of(JOBS_F_OFFERED), 1) expect_eq(count_of(base_st, jobs_st, JOBS_F_OFFERED), 1)
} }
test "a board rolled by the day, the same every time" { test "a board rolled by the day, the same every time" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
fresh() fresh(base_st, jobs_st, jobstest_st)
day = 3 jobstest_st.day = 3
jobs_morning() jobs_morning(base_st, jobs_st)
expect_eq(len(jobs_posted(BOARD_DAILY)), 3) expect_eq(len(jobs_posted(jobs_st, BOARD_DAILY)), 3)
expect_eq(len(jobs_posted(BOARD_ADA)), 2) expect_eq(len(jobs_posted(jobs_st, BOARD_ADA)), 2)
let a = jobs_post_need(BOARD_DAILY, 0) * 100 + jobs_post_need(BOARD_DAILY, 1) * 10 + jobs_post_need(BOARD_DAILY, 2) let a = jobs_post_need(jobs_st, BOARD_DAILY, 0) * 100 + jobs_post_need(jobs_st, BOARD_DAILY, 1) * 10 + jobs_post_need(jobs_st, BOARD_DAILY, 2)
fresh() fresh(base_st, jobs_st, jobstest_st)
day = 3 jobstest_st.day = 3
jobs_morning() jobs_morning(base_st, jobs_st)
let b = jobs_post_need(BOARD_DAILY, 0) * 100 + jobs_post_need(BOARD_DAILY, 1) * 10 + jobs_post_need(BOARD_DAILY, 2) let b = jobs_post_need(jobs_st, BOARD_DAILY, 0) * 100 + jobs_post_need(jobs_st, BOARD_DAILY, 1) * 10 + jobs_post_need(jobs_st, BOARD_DAILY, 2)
expect_eq(a, b) expect_eq(a, b)
expect(jobs_post_need(BOARD_DAILY, 0) >= 2 and jobs_post_need(BOARD_DAILY, 0) <= 4) expect(jobs_post_need(jobs_st, BOARD_DAILY, 0) >= 2 and jobs_post_need(jobs_st, BOARD_DAILY, 0) <= 4)
} }
test "a daily pays the moment it is met and waits for the morning" { test "a daily pays the moment it is met and waits for the morning" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
fresh() fresh(base_st, jobs_st, jobstest_st)
day = 2 jobstest_st.day = 2
jobs_morning() jobs_morning(base_st, jobs_st)
let need = jobs_post_need(BOARD_DAILY, 1) let need = jobs_post_need(jobs_st, BOARD_DAILY, 1)
jobs_count(K_WOOD, -1, need) jobs_count(base_st, jobs_st, K_WOOD, -1, need)
expect_eq(jobs_post_state(BOARD_DAILY, 1), JOBS_DONE) expect_eq(jobs_post_state(jobs_st, BOARD_DAILY, 1), JOBS_DONE)
expect(jobs_posts_done(BOARD_DAILY) >= 1) expect(jobs_posts_done(jobs_st, BOARD_DAILY) >= 1)
expect_eq(paid, 7 * jobs_posts_done(BOARD_DAILY)) expect_eq(jobstest_st.paid, 7 * jobs_posts_done(jobs_st, BOARD_DAILY))
jobs_morning() jobs_morning(base_st, jobs_st)
expect_eq(jobs_post_state(BOARD_DAILY, 1), JOBS_DONE) expect_eq(jobs_post_state(jobs_st, BOARD_DAILY, 1), JOBS_DONE)
day = 3 jobstest_st.day = 3
jobs_morning() jobs_morning(base_st, jobs_st)
expect_eq(jobs_post_state(BOARD_DAILY, 1), JOBS_ACTIVE) expect_eq(jobs_post_state(jobs_st, BOARD_DAILY, 1), JOBS_ACTIVE)
} }
test "a post counts only what came after it went up, and is replaced when handed in" { test "a post counts only what came after it went up, and is replaced when handed in" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
fresh() fresh(base_st, jobs_st, jobstest_st)
fish = 5 jobstest_st.fish = 5
jobs_morning() jobs_morning(base_st, jobs_st)
let need = jobs_post_need(BOARD_ADA, 0) let need = jobs_post_need(jobs_st, BOARD_ADA, 0)
jobs_recheck() jobs_recheck(base_st, jobs_st)
expect_eq(jobs_post_have(BOARD_ADA, 0), 0) expect_eq(jobs_post_have(jobs_st, BOARD_ADA, 0), 0)
fish = 5 + need jobstest_st.fish = 5 + need
jobs_recheck() jobs_recheck(base_st, jobs_st)
expect_eq(jobs_post_state(BOARD_ADA, 0), JOBS_READY) expect_eq(jobs_post_state(jobs_st, BOARD_ADA, 0), JOBS_READY)
expect(jobs_post_hand_in(BOARD_ADA, 0)) expect(jobs_post_hand_in(base_st, jobs_st, BOARD_ADA, 0))
expect_eq(paid_scope, JOBS_PARTY) expect_eq(jobstest_st.paid_scope, JOBS_PARTY)
expect_eq(jobs_total(BOARD_ADA), 1) expect_eq(jobs_total(jobs_st, BOARD_ADA), 1)
expect_eq(jobs_post_state(BOARD_ADA, 0), JOBS_ACTIVE) expect_eq(jobs_post_state(jobs_st, BOARD_ADA, 0), JOBS_ACTIVE)
} }
test "a stale post comes down after its days" { test "a stale post comes down after its days" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
fresh() fresh(base_st, jobs_st, jobstest_st)
jobs_morning() jobs_morning(base_st, jobs_st)
day = 7 jobstest_st.day = 7
jobs_morning() jobs_morning(base_st, jobs_st)
expect_eq(jobs_post_day(BOARD_ADA, 0), 1) expect_eq(jobs_post_day(jobs_st, BOARD_ADA, 0), 1)
day = 8 jobstest_st.day = 8
jobs_morning() jobs_morning(base_st, jobs_st)
expect_eq(jobs_post_day(BOARD_ADA, 0), 8) expect_eq(jobs_post_day(jobs_st, BOARD_ADA, 0), 8)
} }
test "each scope saves its own, by key" { test "each scope saves its own, by key" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
fresh() fresh(base_st, jobs_st, jobstest_st)
jobs_take(JOB_CATCH) jobs_take(base_st, jobs_st, JOB_CATCH)
jobs_count(K_FISH, 0, 2) jobs_count(base_st, jobs_st, K_FISH, 0, 2)
jobs_take(JOB_TEN) jobs_take(base_st, jobs_st, JOB_TEN)
jobs_count(K_FISH, 0, 1) jobs_count(base_st, jobs_st, K_FISH, 0, 1)
day = 2 jobstest_st.day = 2
jobs_morning() jobs_morning(base_st, jobs_st)
let body = jobs_save() let body = jobs_save(jobs_st)
let world = jobs_party_save() let world = jobs_party_save(jobs_st)
let need = jobs_post_need(BOARD_ADA, 1) let need = jobs_post_need(jobs_st, BOARD_ADA, 1)
fresh() fresh(base_st, jobs_st, jobstest_st)
jobs_load(body, 1) jobs_load(jobs_st, body, 1)
expect_eq(jobs_have(JOB_CATCH), 3) expect_eq(jobs_have(jobs_st, JOB_CATCH), 3)
expect_eq(jobs_state(JOB_TEN), JOBS_OFFERED) expect_eq(jobs_state(jobs_st, JOB_TEN), JOBS_OFFERED)
jobs_party_load(world, 1) jobs_party_load(jobs_st, world, 1)
expect_eq(jobs_have(JOB_TEN), 1) expect_eq(jobs_have(jobs_st, JOB_TEN), 1)
expect_eq(jobs_post_need(BOARD_ADA, 1), need) expect_eq(jobs_post_need(jobs_st, BOARD_ADA, 1), need)
expect_eq(jobs_post_day(BOARD_DAILY, 0), 2) expect_eq(jobs_post_day(jobs_st, BOARD_DAILY, 0), 2)
} }
} }

Some files were not shown because too many files have changed in this diff Show more