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:
parent
1e8b5b0523
commit
07505e7ef2
294 changed files with 14996 additions and 14675 deletions
|
|
@ -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_NEVER: float = -1.0 # a reach that is never in reach (an animal without a camera)
|
||||
|
||||
var am_kind: int = 0
|
||||
var am_id: int = -1
|
||||
var am_t: float = 10000.0
|
||||
var am_dist: float = 0.0
|
||||
var am_reach: bool = false
|
||||
var am_off: float = 1.0
|
||||
export state AimState {
|
||||
am_kind: int = 0
|
||||
am_id: int = -1
|
||||
am_t: float = 10000.0
|
||||
am_dist: float = 0.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
|
||||
# AIM_REACH_MIN, because a per-kind reach chosen against its model was never chosen against standing
|
||||
# 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_begin() -> void {
|
||||
am_kind = AIM_NONE
|
||||
am_id = -1
|
||||
am_t = 10000.0
|
||||
am_dist = 0.0
|
||||
am_reach = false
|
||||
am_off = 1.0
|
||||
export function aim_begin(aim_st: mut AimState) -> void {
|
||||
aim_st.am_kind = AIM_NONE
|
||||
aim_st.am_id = -1
|
||||
aim_st.am_t = 10000.0
|
||||
aim_st.am_dist = 0.0
|
||||
aim_st.am_reach = false
|
||||
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
|
||||
# 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.
|
||||
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)
|
||||
if d > far { return false }
|
||||
let rr = r + aim_slack_m(d)
|
||||
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 }
|
||||
am_t = hit
|
||||
am_off = aim_miss(x, z, rr)
|
||||
am_kind = kind
|
||||
am_id = id
|
||||
am_dist = d
|
||||
am_reach = reach >= 0.0 and d < aim_reach_of(reach)
|
||||
aim_st.am_t = hit
|
||||
aim_st.am_off = aim_miss(x, z, rr)
|
||||
aim_st.am_kind = kind
|
||||
aim_st.am_id = id
|
||||
aim_st.am_dist = d
|
||||
aim_st.am_reach = reach >= 0.0 and d < aim_reach_of(reach)
|
||||
return true
|
||||
}
|
||||
|
||||
export function aim_kind() -> int { return am_kind }
|
||||
export function aim_id() -> int { return am_id }
|
||||
export function aim_dist() -> float { return am_dist }
|
||||
export function aim_in_reach() -> bool { return am_reach }
|
||||
export function aim_t() -> float { return am_t }
|
||||
export function aim_kind(aim_st: AimState) -> int { return aim_st.am_kind }
|
||||
export function aim_id(aim_st: AimState) -> int { return aim_st.am_id }
|
||||
export function aim_dist(aim_st: AimState) -> float { return aim_st.am_dist }
|
||||
export function aim_in_reach(aim_st: AimState) -> bool { return aim_st.am_reach }
|
||||
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
|
||||
export function aim_off_centre() -> float { return am_off }
|
||||
export function aim_off_centre(aim_st: AimState) -> float { return aim_st.am_off }
|
||||
|
|
|
|||
|
|
@ -3,19 +3,16 @@
|
|||
# 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
|
||||
# `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_blocked() -> int { return am_blocked }
|
||||
export function aim_probe_disagree() -> int { return am_disagree }
|
||||
export function aim_probe_seen(aim_st: AimState) -> int { return aim_st.am_seen }
|
||||
export function aim_probe_blocked(aim_st: AimState) -> int { return aim_st.am_blocked }
|
||||
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
|
||||
export function aim_probe_ground(px: float, pz: float, look: fn(float, float, float) -> void) -> bool {
|
||||
am_seen = 0
|
||||
am_blocked = 0
|
||||
am_disagree = 0
|
||||
export function aim_probe_ground(aim_st: mut AimState, px: float, pz: float, look: fn(float, float, float) -> void) -> bool {
|
||||
aim_st.am_seen = 0
|
||||
aim_st.am_blocked = 0
|
||||
aim_st.am_disagree = 0
|
||||
for sp in 0 .. 9 {
|
||||
var sx = px
|
||||
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
|
||||
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 {
|
||||
var dist = 60.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)
|
||||
if am_probe_gap(sx, sy, sz, ex, ey, ez, -0.5, 0.5) and not truth { continue }
|
||||
look(sx, sy, sz)
|
||||
am_seen += 1
|
||||
aim_st.am_seen += 1
|
||||
let clear = aim_clear_to(ex, ey, ez)
|
||||
if not clear { am_blocked += 1 }
|
||||
if clear == truth { am_disagree += 1 }
|
||||
if not clear { aim_st.am_blocked += 1 }
|
||||
if clear == truth { aim_st.am_disagree += 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,34 +6,36 @@ import "ludic.base"
|
|||
program AimTest {
|
||||
numbers float
|
||||
|
||||
var cx: float = 0.0
|
||||
var cy: float = 1.5
|
||||
var cz: float = 0.0
|
||||
var fy: float = 0.0
|
||||
function fk_x() -> float { return cx }
|
||||
function fk_y() -> float { return cy }
|
||||
function fk_z() -> float { return cz }
|
||||
function fk_fy() -> float { return fy }
|
||||
function fk_fz() -> float { return -Math.sqrt(1.0 - fy * fy) }
|
||||
state AimtestState {
|
||||
cx: float = 0.0
|
||||
cy: float = 1.5
|
||||
cz: float = 0.0
|
||||
fy: float = 0.0
|
||||
}
|
||||
function fk_x(aimtest_st: AimtestState) -> float { return aimtest_st.cx }
|
||||
function fk_y(aimtest_st: AimtestState) -> float { return aimtest_st.cy }
|
||||
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
|
||||
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 {
|
||||
cx = x
|
||||
cy = y
|
||||
cz = z
|
||||
function fk_look(aimtest_st: mut AimtestState, x: float, y: float, z: float) -> void {
|
||||
aimtest_st.cx = x
|
||||
aimtest_st.cy = y
|
||||
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 }
|
||||
|
||||
function fresh() -> void {
|
||||
cx = 0.0
|
||||
cy = 1.5
|
||||
cz = 0.0
|
||||
fy = 0.0
|
||||
aim_begin()
|
||||
function fresh(aim_st: mut AimState, aimtest_st: mut AimtestState) -> void {
|
||||
aimtest_st.cx = 0.0
|
||||
aimtest_st.cy = 1.5
|
||||
aimtest_st.cz = 0.0
|
||||
aimtest_st.fy = 0.0
|
||||
aim_begin(aim_st)
|
||||
}
|
||||
|
||||
test "the ray enters a cylinder in front and misses one to the side or behind" {
|
||||
fresh()
|
||||
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(aim_st, aimtest_st)
|
||||
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(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)
|
||||
}
|
||||
|
||||
test "a thin thing far off is still aimable, by an angle and not a metre" {
|
||||
fresh()
|
||||
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(aim_st, aimtest_st)
|
||||
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_off_centre() > 0.5)
|
||||
fresh()
|
||||
expect(not aim_offer(1, 7, 3.0, 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(aim_st) > 0.5)
|
||||
fresh(aim_st, aimtest_st)
|
||||
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" {
|
||||
fresh()
|
||||
expect(aim_offer(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(not aim_offer(1, 3, 0.0, 0.0, 2.0, -15.0, 0.5, 1.5, 30.0))
|
||||
expect_eq(aim_id(), 2)
|
||||
expect_near(aim_dist(), 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(2, 5, 0.0, 0.0, 2.0, -3.0, 0.5, 1.5, 2.0))
|
||||
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(aim_st, aimtest_st)
|
||||
expect(aim_offer(aim_st, 1, 1, 0.0, 0.0, 2.0, -20.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(aim_st, 1, 3, 0.0, 0.0, 2.0, -15.0, 0.5, 1.5, 30.0))
|
||||
expect_eq(aim_id(aim_st), 2)
|
||||
expect_near(aim_dist(aim_st), 8.0, 0.01)
|
||||
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(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" {
|
||||
fresh()
|
||||
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(aim_st, aimtest_st)
|
||||
expect(not aim_clear_to(0.0, 1.0, -60.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))
|
||||
cy = -5.0
|
||||
expect(not aim_offer(aim_st, 1, 1, 0.0, 0.0, 2.0, -60.0, 2.0, 1.5, 80.0))
|
||||
aimtest_st.cy = -5.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" {
|
||||
fresh()
|
||||
test "the crosshair decides which, the distance whether: every reach is floored at three metres" (aim_st: mut AimState, aimtest_st: mut AimtestState) {
|
||||
fresh(aim_st, aimtest_st)
|
||||
expect_near(aim_reach_of(1.5), AIM_REACH_MIN, 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)
|
||||
expect(aim_in_reach())
|
||||
expect_eq(aim_use(1), AIM_USE_THIS)
|
||||
fresh()
|
||||
aim_offer(1, 1, 0.0, 0.0, 2.0, -6.0, 0.3, 1.5, 30.0)
|
||||
expect(not aim_in_reach())
|
||||
expect_eq(aim_use(1), AIM_USE_NOTHING)
|
||||
expect_eq(aim_use(3), AIM_USE_NEAREST)
|
||||
fresh()
|
||||
aim_offer(1, 1, 0.0, 0.0, 3.0, -20.0, 0.3, 1.5, 30.0)
|
||||
expect_eq(aim_use(1), AIM_USE_NEAREST)
|
||||
fresh()
|
||||
aim_offer(2, 9, 0.0, 0.0, 2.0, -2.0, 0.3, AIM_NEVER, 30.0)
|
||||
expect(not aim_in_reach())
|
||||
expect_eq(aim_kind(), 2)
|
||||
aim_offer(aim_st, 1, 1, 0.0, 0.0, 2.0, -2.6, 0.3, 1.5, 30.0)
|
||||
expect(aim_in_reach(aim_st))
|
||||
expect_eq(aim_use(aim_st, 1), AIM_USE_THIS)
|
||||
fresh(aim_st, aimtest_st)
|
||||
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(aim_st))
|
||||
expect_eq(aim_use(aim_st, 1), AIM_USE_NOTHING)
|
||||
expect_eq(aim_use(aim_st, 3), AIM_USE_NEAREST)
|
||||
fresh(aim_st, aimtest_st)
|
||||
aim_offer(aim_st, 1, 1, 0.0, 0.0, 3.0, -20.0, 0.3, 1.5, 30.0)
|
||||
expect_eq(aim_use(aim_st, 1), AIM_USE_NEAREST)
|
||||
fresh(aim_st, aimtest_st)
|
||||
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(aim_st))
|
||||
expect_eq(aim_kind(aim_st), 2)
|
||||
}
|
||||
|
||||
test "the probe finds the bank and agrees with the ground about every line it hides" {
|
||||
fresh()
|
||||
expect(aim_probe_ground(0.0, 0.0, fn fk_look))
|
||||
expect(aim_probe_seen() > 100)
|
||||
expect(aim_probe_blocked() > 0)
|
||||
expect(not (aim_probe_disagree() > aim_probe_seen() / 50))
|
||||
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(aim_st, aimtest_st)
|
||||
expect(aim_probe_ground(aim_st, 0.0, 0.0, fn fk_look))
|
||||
expect(aim_probe_seen(aim_st) > 100)
|
||||
expect(aim_probe_blocked(aim_st) > 0)
|
||||
expect(not (aim_probe_disagree(aim_st) > aim_probe_seen(aim_st) / 50))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ export const AIM_USE_THIS: int = 0
|
|||
export const AIM_USE_NOTHING: int = 1
|
||||
export const AIM_USE_NEAREST: int = 2
|
||||
|
||||
export function aim_use(kind: int) -> int {
|
||||
if am_kind == kind and am_reach { return AIM_USE_THIS }
|
||||
if am_kind == kind and am_dist < AIM_USE_MAX { return AIM_USE_NOTHING }
|
||||
export function aim_use(aim_st: AimState, kind: int) -> int {
|
||||
if aim_st.am_kind == kind and aim_st.am_reach { return AIM_USE_THIS }
|
||||
if aim_st.am_kind == kind and aim_st.am_dist < AIM_USE_MAX { return AIM_USE_NOTHING }
|
||||
return AIM_USE_NEAREST
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,37 +16,47 @@ export property Clip {
|
|||
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
|
||||
# does not translate its joints, so applying them turns rounding into a pose.
|
||||
var positions: bool = false
|
||||
|
||||
function clips_init() -> void {
|
||||
if clips == null { clips = new []Clip }
|
||||
function clips_init(anim_st: mut AnimState) -> void {
|
||||
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
|
||||
export function anim_find(name: string) -> Clip {
|
||||
clips_init()
|
||||
for i in 0 .. len(clips) { if clips[i].name == name { return clips[i] } }
|
||||
export function anim_find(anim_st: mut AnimState, name: string) -> Clip {
|
||||
clips_init(anim_st)
|
||||
for i in 0 .. len(anim_st.clips) { if anim_st.clips[i].name == name { return anim_st.clips[i] } }
|
||||
return null
|
||||
}
|
||||
|
||||
export function anim_count() -> int {
|
||||
clips_init()
|
||||
return len(clips)
|
||||
export function anim_count(anim_st: mut AnimState) -> int {
|
||||
clips_init(anim_st)
|
||||
return len(anim_st.clips)
|
||||
}
|
||||
|
||||
# a clip made outside a glTF (a test, a generated clip); a name already held is refused
|
||||
export function anim_add(c: Clip) -> bool {
|
||||
if c == null or anim_find(c.name) != null { return false }
|
||||
push(clips, c)
|
||||
export function anim_add(anim_st: mut AnimState, c: Clip) -> bool {
|
||||
if c == null or anim_find(anim_st, c.name) != null { return false }
|
||||
push(anim_st.clips, c)
|
||||
return true
|
||||
}
|
||||
|
||||
export function anim_clear() -> void { clips = new []Clip }
|
||||
export function anim_set_positions(on: bool) -> void { positions = on }
|
||||
export function anim_positions() -> bool { return positions }
|
||||
export function anim_clear(anim_st: mut AnimState) -> void { anim_st.clips = new []Clip }
|
||||
export function anim_set_positions(anim_st: mut AnimState, on: bool) -> void { anim_st.positions = on }
|
||||
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
|
||||
# hundred keys long and a valley can have dozens of animals posed in a frame.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# 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
|
||||
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 q_a == null {
|
||||
q_a = floats(4)
|
||||
q_b = floats(4)
|
||||
q_cur = floats(4)
|
||||
if anim_st.q_a == null {
|
||||
anim_st.q_a = floats(4)
|
||||
anim_st.q_b = floats(4)
|
||||
anim_st.q_cur = floats(4)
|
||||
}
|
||||
var tt = t
|
||||
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]
|
||||
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
|
||||
# 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.
|
||||
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]
|
||||
for e in 0 .. 4 {
|
||||
q_a[e] = c.vals[vo + i0 * 4 + e]
|
||||
q_b[e] = c.vals[vo + i1 * 4 + e]
|
||||
anim_st.q_a[e] = c.vals[vo + i0 * 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 {
|
||||
q_store(rot, node, q_a)
|
||||
q_store(rot, node, anim_st.q_a)
|
||||
hit[node] = hit[node] | 1
|
||||
} else {
|
||||
q_load(q_cur, rot, node)
|
||||
q_nlerp(q_cur, q_cur, q_a, w)
|
||||
q_store(rot, node, q_cur)
|
||||
q_load(anim_st.q_cur, rot, node)
|
||||
q_nlerp(anim_st.q_cur, anim_st.q_cur, anim_st.q_a, w)
|
||||
q_store(rot, node, anim_st.q_cur)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +1,23 @@
|
|||
# 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
|
||||
# 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 {
|
||||
if parts == null {
|
||||
parts = new []floats
|
||||
for k in 0 .. 8 { push(parts, floats(4)) }
|
||||
function part(anim_st: mut AnimState, i: int) -> floats {
|
||||
if anim_st.parts == null {
|
||||
anim_st.parts = new []floats
|
||||
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 {
|
||||
let l = part(0)
|
||||
let rinv = part(1)
|
||||
let gp = part(2)
|
||||
let gpi = part(3)
|
||||
let tmp = part(4)
|
||||
let d = part(5)
|
||||
let v = part(6)
|
||||
export function anim_to_pose(anim_st: mut AnimState, sk: Skin, rot: floats, pos: floats, hit: words) -> void {
|
||||
let l = part(anim_st, 0)
|
||||
let rinv = part(anim_st, 1)
|
||||
let gp = part(anim_st, 2)
|
||||
let gpi = part(anim_st, 3)
|
||||
let tmp = part(anim_st, 4)
|
||||
let d = part(anim_st, 5)
|
||||
let v = part(anim_st, 6)
|
||||
for i in 0 .. sk.n_nodes {
|
||||
let p = sk.par[i]
|
||||
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)
|
||||
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 dy = pos[i * 3 + 1] - sk.rest_t[i * 3 + 1]
|
||||
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.
|
||||
function scratch(n: int) -> void {
|
||||
if cap >= n { return }
|
||||
rot_s = floats(n * 4)
|
||||
pos_s = floats(n * 3)
|
||||
hit_s = words(n)
|
||||
cap = n
|
||||
function scratch(anim_st: mut AnimState, n: int) -> void {
|
||||
if anim_st.cap >= n { return }
|
||||
anim_st.rot_s = floats(n * 4)
|
||||
anim_st.pos_s = floats(n * 3)
|
||||
anim_st.hit_s = words(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
|
||||
# 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 }
|
||||
let n = sk.n_nodes
|
||||
scratch(n)
|
||||
for i in 0 .. n { hit_s[i] = 0 }
|
||||
scratch(anim_st, n)
|
||||
for i in 0 .. n { anim_st.hit_s[i] = 0 }
|
||||
skin_reset(sk)
|
||||
anim_mix(a, sk, ta, 1.0, rot_s, pos_s, hit_s)
|
||||
if b != null and blend > 0.001 { anim_mix(b, sk, tb, blend, rot_s, pos_s, hit_s) }
|
||||
anim_to_pose(sk, 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(anim_st, b, sk, tb, blend, anim_st.rot_s, anim_st.pos_s, anim_st.hit_s) }
|
||||
anim_to_pose(anim_st, sk, anim_st.rot_s, anim_st.pos_s, anim_st.hit_s)
|
||||
skin_pose(sk)
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,31 +3,31 @@
|
|||
|
||||
# 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.
|
||||
export function anim_read_doc(dir: string) -> int {
|
||||
clips_init()
|
||||
if gltf_doc == null { return 0 }
|
||||
if value_has(gltf_doc, "animations") == 0 { return 0 }
|
||||
export function anim_read_doc(anim_st: mut AnimState, render3d_st: mut Render3dState, dir: string) -> int {
|
||||
clips_init(anim_st)
|
||||
if render3d_st.gltf_doc == null { 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
|
||||
# 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 }
|
||||
let bin_uri = value_as_str(value_get(value_at(buffers, 0), "uri"))
|
||||
gltf_bin = file_open(dir + "/" + bin_uri, "rb")
|
||||
if gltf_bin == null {
|
||||
render3d_st.gltf_bin = file_open(dir + "/" + bin_uri, "rb")
|
||||
if render3d_st.gltf_bin == null {
|
||||
print(`anim: cannot reopen {dir}/{bin_uri}`)
|
||||
return 0
|
||||
}
|
||||
let arr = value_get(gltf_doc, "animations")
|
||||
let arr = value_get(render3d_st.gltf_doc, "animations")
|
||||
var got = 0
|
||||
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 {
|
||||
push(clips, c)
|
||||
push(anim_st.clips, c)
|
||||
got += 1
|
||||
}
|
||||
}
|
||||
file_close(gltf_bin)
|
||||
gltf_bin = null
|
||||
file_close(render3d_st.gltf_bin)
|
||||
render3d_st.gltf_bin = null
|
||||
return got
|
||||
}
|
||||
|
||||
|
|
@ -39,10 +39,10 @@ function kept_path(ch: Val) -> string {
|
|||
return path
|
||||
}
|
||||
|
||||
function read_one(a: Val) -> Clip {
|
||||
function read_one(anim_st: mut AnimState, render3d_st: mut Render3dState, a: Val) -> Clip {
|
||||
var nm = ""
|
||||
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 samps = value_get(a, "samplers")
|
||||
let nc = value_count(chans)
|
||||
|
|
@ -55,7 +55,7 @@ function read_one(a: Val) -> Clip {
|
|||
let path = kept_path(ch)
|
||||
if path == "" { continue }
|
||||
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_t += cnt
|
||||
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")))
|
||||
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 }
|
||||
let tb = gltf_accessor_floats(value_as_int(value_get(sp, "input")))
|
||||
let nk = gltf_count
|
||||
let tb = gltf_accessor_floats(render3d_st, value_as_int(value_get(sp, "input")))
|
||||
let nk = render3d_st.gltf_count
|
||||
c.ckeys[k] = nk
|
||||
c.coff[k] = at
|
||||
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] }
|
||||
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
|
||||
if c.cpath[k] == AC_ROT { comps = 4 }
|
||||
c.cvoff[k] = av
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ program AnimTest {
|
|||
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
|
||||
function skin() -> Skin {
|
||||
gltf_doc = Json.parse("{\"nodes\": [{\"name\": \"root\", \"children\": [1]}, {\"name\": \"bone\", \"translation\": [0, 1, 0]}], \"skins\": [{\"joints\": [0, 1]}]}")
|
||||
return skin_load(0)
|
||||
function skin(render3d_st: mut Render3dState) -> Skin {
|
||||
render3d_st.gltf_doc = Json.parse("{\"nodes\": [{\"name\": \"root\", \"children\": [1]}, {\"name\": \"bone\", \"translation\": [0, 1, 0]}], \"skins\": [{\"joints\": [0, 1]}]}")
|
||||
return skin_load(render3d_st, 0)
|
||||
}
|
||||
|
||||
# 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]
|
||||
}
|
||||
|
||||
test "a clip is found by name, once" {
|
||||
anim_clear()
|
||||
expect(anim_add(turn("hiker_m_walk")))
|
||||
expect(not anim_add(turn("hiker_m_walk")))
|
||||
expect(anim_add(turn("hiker_f_walk")))
|
||||
expect_eq(anim_count(), 2)
|
||||
expect(anim_find("hiker_f_walk") != null)
|
||||
expect(anim_find("bear_walk") == null)
|
||||
test "a clip is found by name, once" (anim_st: mut AnimState) {
|
||||
anim_clear(anim_st)
|
||||
expect(anim_add(anim_st, turn("hiker_m_walk")))
|
||||
expect(not anim_add(anim_st, turn("hiker_m_walk")))
|
||||
expect(anim_add(anim_st, turn("hiker_f_walk")))
|
||||
expect_eq(anim_count(anim_st), 2)
|
||||
expect(anim_find(anim_st, "hiker_f_walk") != null)
|
||||
expect(anim_find(anim_st, "bear_walk") == null)
|
||||
}
|
||||
|
||||
test "a key pair is found by halving" {
|
||||
|
|
@ -61,56 +61,56 @@ program AnimTest {
|
|||
expect_eq(anim_key_at(c, 0, -1.0), 0)
|
||||
}
|
||||
|
||||
test "playing samples between keys, and wraps on the duration" {
|
||||
let sk = skin()
|
||||
test "playing samples between keys, and wraps on the duration" (anim_st: mut AnimState, render3d_st: mut Render3dState) {
|
||||
let sk = skin(render3d_st)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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(mid > 0.3 and mid < 0.45)
|
||||
}
|
||||
|
||||
test "a cross-fade lands between the two clips" {
|
||||
let sk = skin()
|
||||
test "a cross-fade lands between the two clips" (anim_st: mut AnimState, render3d_st: mut Render3dState) {
|
||||
let sk = skin(render3d_st)
|
||||
let a = turn("a")
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
test "a bone is found in the skin's own joints, not the file's first of that name" {
|
||||
gltf_doc = Json.parse("{\"nodes\": [{\"name\": \"bone\"}, {\"name\": \"root\", \"children\": [2]}, {\"name\": \"bone\"}], \"skins\": [{\"joints\": [1, 2]}]}")
|
||||
let sk = skin_load(0)
|
||||
test "a bone is found in the skin's own joints, not the file's first of that name" (render3d_st: mut Render3dState) {
|
||||
render3d_st.gltf_doc = Json.parse("{\"nodes\": [{\"name\": \"bone\"}, {\"name\": \"root\", \"children\": [2]}, {\"name\": \"bone\"}], \"skins\": [{\"joints\": [1, 2]}]}")
|
||||
let sk = skin_load(render3d_st, 0)
|
||||
expect_eq(skin_joint(sk, "bone"), 2)
|
||||
expect_eq(skin_joint_quiet(sk, "wing_l"), -1)
|
||||
expect_eq(skin_joint_quiet(null, "bone"), -1)
|
||||
}
|
||||
|
||||
test "no skin or no clip plays nothing" {
|
||||
expect(not anim_play(null, turn("t"), 0.0, null, 0.0, 0.0))
|
||||
expect(not anim_play(skin(), null, 0.0, null, 0.0, 0.0))
|
||||
test "no skin or no clip plays nothing" (anim_st: mut AnimState, render3d_st: mut Render3dState) {
|
||||
expect(not anim_play(anim_st, null, turn("t"), 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" {
|
||||
let sk = skin()
|
||||
test "keyed translations are ignored until asked for" (anim_st: mut AnimState, render3d_st: mut Render3dState) {
|
||||
let sk = skin(render3d_st)
|
||||
let c = turn("t")
|
||||
c.cpath[0] = AC_POS
|
||||
c.vals[0] = 0.0
|
||||
c.vals[1] = 3.0
|
||||
c.vals[3] = 0.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)
|
||||
anim_set_positions(true)
|
||||
anim_play(sk, c, 0.0, null, 0.0, 0.0)
|
||||
anim_set_positions(anim_st, true)
|
||||
anim_play(anim_st, sk, c, 0.0, null, 0.0, 0.0)
|
||||
expect_near(sk.pose_t[4], 2.0, 0.0001)
|
||||
}
|
||||
|
||||
|
|
@ -122,8 +122,8 @@ program AnimTest {
|
|||
b[at + 3] = (v >> 24) & 255
|
||||
}
|
||||
|
||||
test "clips are read out of a glTF's .bin, and a late first key is measured" {
|
||||
anim_clear()
|
||||
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_st)
|
||||
let dir = Os.temp_dir() + "/ludic-anim-test"
|
||||
Fs.mkdir(dir)
|
||||
let b = buffer(40)
|
||||
|
|
@ -133,10 +133,10 @@ program AnimTest {
|
|||
put_f(b, 8 + 24, HALF)
|
||||
put_f(b, 8 + 28, HALF)
|
||||
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\"}}]}]}")
|
||||
expect_eq(anim_read_doc(dir), 1)
|
||||
expect_eq(anim_read_doc(dir), 0)
|
||||
let c = anim_find("bear_walk")
|
||||
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(anim_st, render3d_st, dir), 1)
|
||||
expect_eq(anim_read_doc(anim_st, render3d_st, dir), 0)
|
||||
let c = anim_find(anim_st, "bear_walk")
|
||||
expect(c != null)
|
||||
expect_eq(c.n, 1)
|
||||
expect_near(c.dur, 1.0, 0.0001)
|
||||
|
|
|
|||
|
|
@ -1,33 +1,37 @@
|
|||
# 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
|
||||
# -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
|
||||
var last_pitch: float = 0.0
|
||||
var last_pan: float = 0.0
|
||||
var emitted: int = 0
|
||||
export state AudioState {
|
||||
last_gain: float = 0.0
|
||||
last_pitch: float = 0.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 }
|
||||
last_gain = gain
|
||||
last_pitch = pitch
|
||||
last_pan = pan
|
||||
emitted += 1
|
||||
audio_st.last_gain = gain
|
||||
audio_st.last_pitch = pitch
|
||||
audio_st.last_pan = pan
|
||||
audio_st.emitted += 1
|
||||
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
|
||||
# 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)
|
||||
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
|
||||
}
|
||||
|
||||
# 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 }
|
||||
emitted += 1
|
||||
audio_st.emitted += 1
|
||||
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
|
||||
# the numbers are the only thing a test can hear
|
||||
export function aud_last_gain() -> float { return last_gain }
|
||||
export function aud_last_pitch() -> float { return last_pitch }
|
||||
export function aud_last_pan() -> float { return last_pan }
|
||||
export function aud_emitted() -> int { return emitted }
|
||||
export function aud_last_gain(audio_st: AudioState) -> float { return audio_st.last_gain }
|
||||
export function aud_last_pitch(audio_st: AudioState) -> float { return audio_st.last_pitch }
|
||||
export function aud_last_pan(audio_st: AudioState) -> float { return audio_st.last_pan }
|
||||
export function aud_emitted(audio_st: AudioState) -> int { return audio_st.emitted }
|
||||
|
|
|
|||
|
|
@ -2,25 +2,23 @@
|
|||
# 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
|
||||
# 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 {
|
||||
tried += 1
|
||||
export function aud_load(audio_st: mut AudioState, path: string) -> int {
|
||||
audio_st.tried += 1
|
||||
if not is_windowed() { return 0 }
|
||||
var h = Audio.load(path)
|
||||
if h == 0 {
|
||||
let out = aud_unpack(path, base_name(path))
|
||||
if out != null { h = Audio.load(out) }
|
||||
}
|
||||
if h != 0 { live += 1 }
|
||||
if h != 0 { audio_st.live += 1 }
|
||||
return h
|
||||
}
|
||||
|
||||
# how many clips were asked for and how many loaded: a silent game looks exactly like a working one
|
||||
# unless something says so
|
||||
export function aud_tried() -> int { return tried }
|
||||
export function aud_live() -> int { return live }
|
||||
export function aud_tried(audio_st: AudioState) -> int { return audio_st.tried }
|
||||
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
|
||||
# packed one: a write interrupted halfway would otherwise play truncated for ever.
|
||||
|
|
|
|||
|
|
@ -4,35 +4,37 @@
|
|||
import "ludic.audio"
|
||||
program AudioTest {
|
||||
numbers float
|
||||
var lx: float = 0.0
|
||||
var lz: float = 0.0
|
||||
var lyaw: float = 0.0
|
||||
function t_x() -> float { return lx }
|
||||
function t_z() -> float { return lz }
|
||||
function t_yaw() -> float { return lyaw }
|
||||
state AudiotestState {
|
||||
lx: float = 0.0
|
||||
lz: float = 0.0
|
||||
lyaw: float = 0.0
|
||||
}
|
||||
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 cache() -> string { return dir() + "/cache" }
|
||||
bind AudioListener { x: fn t_x, z: fn t_z, yaw: fn t_yaw }
|
||||
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 mid = aud_gain_at(0.0, -100.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_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)
|
||||
lx = 1000.0
|
||||
audiotest_st.lx = 1000.0
|
||||
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_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)
|
||||
lyaw = 3.14159265
|
||||
audiotest_st.lyaw = 3.14159265
|
||||
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)
|
||||
}
|
||||
|
||||
test "emit hands the runtime what it was told, and a sound out of earshot is not played" {
|
||||
aud_emit(7, 0.5, 0.97, -0.25)
|
||||
expect_near(aud_last_gain(), 0.5, 0.0001)
|
||||
expect_near(aud_last_pan(), -0.25, 0.0001)
|
||||
expect_eq(aud_emitted(), 1)
|
||||
aud_emit(0, 1.0, 1.0, 0.0)
|
||||
expect_eq(aud_emitted(), 1)
|
||||
expect(not aud_emit_at(7, 0.0, -500.0, 100.0))
|
||||
expect(aud_emit_at(7, 30.0, 0.0, 100.0))
|
||||
expect(aud_last_pan() > 0.5)
|
||||
expect(aud_last_gain() > 0.4 and aud_last_gain() < 0.5)
|
||||
expect(aud_last_pitch() < 1.0)
|
||||
test "emit hands the runtime what it was told, and a sound out of earshot is not played" (audio_st: mut AudioState) {
|
||||
aud_emit(audio_st, 7, 0.5, 0.97, -0.25)
|
||||
expect_near(aud_last_gain(audio_st), 0.5, 0.0001)
|
||||
expect_near(aud_last_pan(audio_st), -0.25, 0.0001)
|
||||
expect_eq(aud_emitted(audio_st), 1)
|
||||
aud_emit(audio_st, 0, 1.0, 1.0, 0.0)
|
||||
expect_eq(aud_emitted(audio_st), 1)
|
||||
expect(not aud_emit_at(audio_st, 7, 0.0, -500.0, 100.0))
|
||||
expect(aud_emit_at(audio_st, 7, 30.0, 0.0, 100.0))
|
||||
expect(aud_last_pan(audio_st) > 0.5)
|
||||
expect(aud_last_gain(audio_st) > 0.4 and aud_last_gain(audio_st) < 0.5)
|
||||
expect(aud_last_pitch(audio_st) < 1.0)
|
||||
}
|
||||
|
||||
function wav(path: string, rate: int, frames: int) -> void {
|
||||
|
|
@ -92,9 +94,9 @@ program AudioTest {
|
|||
expect(aud_unpack(dir() + "/nothing.wav", "nothing.wav") == null)
|
||||
}
|
||||
|
||||
test "nothing loads headless, and the count says so" {
|
||||
expect_eq(aud_load(dir() + "/click.wav"), 0)
|
||||
expect_eq(aud_tried(), 1)
|
||||
expect_eq(aud_live(), 0)
|
||||
test "nothing loads headless, and the count says so" (audio_st: mut AudioState) {
|
||||
expect_eq(aud_load(audio_st, dir() + "/click.wav"), 0)
|
||||
expect_eq(aud_tried(audio_st), 1)
|
||||
expect_eq(aud_live(audio_st), 0)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
var qb_names: []string = null
|
||||
var qb_pending: []int = null
|
||||
export state BaseState {
|
||||
qb_names: []string = null
|
||||
qb_pending: []int = null
|
||||
cs_list: []System = null
|
||||
}
|
||||
|
||||
function qb_init() -> void {
|
||||
if qb_names != null { return }
|
||||
qb_names = new []string
|
||||
qb_pending = new []int
|
||||
function qb_init(base_st: mut BaseState) -> void {
|
||||
if base_st.qb_names != null { return }
|
||||
base_st.qb_names = new []string
|
||||
base_st.qb_pending = new []int
|
||||
}
|
||||
|
||||
# a queue; the name is only for core_undrained
|
||||
export function queue_new<T>(name: string) -> Queue<T> {
|
||||
qb_init()
|
||||
export function queue_new<T>(base_st: mut BaseState, name: string) -> Queue<T> {
|
||||
qb_init(base_st)
|
||||
let q = new Queue<T>
|
||||
q.items = new []T
|
||||
q.id = len(qb_names)
|
||||
push(qb_names, name)
|
||||
push(qb_pending, 0)
|
||||
q.id = len(base_st.qb_names)
|
||||
push(base_st.qb_names, name)
|
||||
push(base_st.qb_pending, 0)
|
||||
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)
|
||||
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
|
||||
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
|
||||
q.items = new []T
|
||||
qb_pending[q.id] = 0
|
||||
base_st.qb_pending[q.id] = 0
|
||||
return out
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
# left behind is a route nobody wrote
|
||||
export function core_undrained() -> []string {
|
||||
qb_init()
|
||||
export function core_undrained(base_st: mut BaseState) -> []string {
|
||||
qb_init(base_st)
|
||||
let out = new []string
|
||||
for i in 0 .. len(qb_names) {
|
||||
if qb_pending[i] > 0 { push(out, qb_names[i]) }
|
||||
for i in 0 .. len(base_st.qb_names) {
|
||||
if base_st.qb_pending[i] > 0 { push(out, base_st.qb_names[i]) }
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,14 +16,13 @@ export property System {
|
|||
# fishing_tick }` from its own module; the runner starts from these, then core_add appends
|
||||
export open registry Systems of System as SYS
|
||||
|
||||
var cs_list: []System = null
|
||||
|
||||
function cs_all() -> []System {
|
||||
if cs_list == null {
|
||||
cs_list = new []System
|
||||
for i in 0 .. SYS_COUNT { push(cs_list, Systems[i]) }
|
||||
function cs_all(base_st: mut BaseState) -> []System {
|
||||
if base_st.cs_list == null {
|
||||
base_st.cs_list = new []System
|
||||
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 {
|
||||
|
|
@ -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
|
||||
export function core_add(s: System) -> void {
|
||||
let all = cs_all()
|
||||
export function core_add(base_st: mut BaseState, s: System) -> void {
|
||||
let all = cs_all(base_st)
|
||||
for i in 0 .. len(all) {
|
||||
if all[i].key == 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)
|
||||
}
|
||||
|
||||
export function core_clear() -> void { cs_list = new []System }
|
||||
export function core_count() -> int { return len(cs_all()) }
|
||||
export function core_clear(base_st: mut BaseState) -> void { base_st.cs_list = new []System }
|
||||
export function core_count(base_st: mut BaseState) -> int { return len(cs_all(base_st)) }
|
||||
|
||||
export function core_init_all() -> void {
|
||||
let all = cs_all()
|
||||
export function core_init_all(base_st: mut BaseState) -> void {
|
||||
let all = cs_all(base_st)
|
||||
for i in 0 .. len(all) { if all[i].init != null { all[i].init() } }
|
||||
}
|
||||
|
||||
export function core_reset_all() -> void {
|
||||
let all = cs_all()
|
||||
export function core_reset_all(base_st: mut BaseState) -> void {
|
||||
let all = cs_all(base_st)
|
||||
for i in 0 .. len(all) { if all[i].reset != null { all[i].reset() } }
|
||||
}
|
||||
|
||||
export function core_tick_all(t: Tick) -> void {
|
||||
let all = cs_all()
|
||||
export function core_tick_all(base_st: mut BaseState, t: Tick) -> void {
|
||||
let all = cs_all(base_st)
|
||||
for ph in 0 .. PH_COUNT {
|
||||
for i in 0 .. len(all) {
|
||||
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
|
||||
export function core_save_all() -> Val {
|
||||
export function core_save_all(base_st: mut BaseState) -> Val {
|
||||
let root = save_tree()
|
||||
let all = cs_all()
|
||||
let all = cs_all(base_st)
|
||||
for i in 0 .. len(all) {
|
||||
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
|
||||
export function core_load_all(root: Val) -> void {
|
||||
let all = cs_all()
|
||||
export function core_load_all(base_st: mut BaseState, root: Val) -> void {
|
||||
let all = cs_all(base_st)
|
||||
for i in 0 .. len(all) {
|
||||
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() }
|
||||
|
|
|
|||
|
|
@ -12,52 +12,52 @@ program QueueTest {
|
|||
return c
|
||||
}
|
||||
|
||||
function fifo_case() -> void {
|
||||
let q: Queue<int> = queue_new("ints")
|
||||
q_push(q, 3)
|
||||
q_push(q, 1)
|
||||
q_push(q, 2)
|
||||
function fifo_case(base_st: mut BaseState) -> void {
|
||||
let q: Queue<int> = queue_new(base_st, "ints")
|
||||
q_push(base_st, q, 3)
|
||||
q_push(base_st, q, 1)
|
||||
q_push(base_st, q, 2)
|
||||
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(xs[0], 3)
|
||||
expect_eq(xs[1], 1)
|
||||
expect_eq(xs[2], 2)
|
||||
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 {
|
||||
let q: Queue<Caught> = queue_new("caught")
|
||||
q_push(q, caught(2, 1.5))
|
||||
q_push(q, caught(4, 0.25))
|
||||
let cs = q_drain(q)
|
||||
function record_case(base_st: mut BaseState) -> void {
|
||||
let q: Queue<Caught> = queue_new(base_st, "caught")
|
||||
q_push(base_st, q, caught(2, 1.5))
|
||||
q_push(base_st, q, caught(4, 0.25))
|
||||
let cs = q_drain(base_st, q)
|
||||
expect_eq(cs[1].species, 4)
|
||||
expect(cs[0].weight == 1.5)
|
||||
}
|
||||
|
||||
function clear_case() -> void {
|
||||
let q: Queue<string> = queue_new("words")
|
||||
q_push(q, "a")
|
||||
q_clear(q)
|
||||
function clear_case(base_st: mut BaseState) -> void {
|
||||
let q: Queue<string> = queue_new(base_st, "words")
|
||||
q_push(base_st, q, "a")
|
||||
q_clear(base_st, q)
|
||||
expect_eq(q_len(q), 0)
|
||||
}
|
||||
|
||||
function undrained_case() -> void {
|
||||
let a: Queue<int> = queue_new("left_behind")
|
||||
let b: Queue<int> = queue_new("read")
|
||||
q_push(a, 1)
|
||||
q_push(b, 2)
|
||||
q_drain(b)
|
||||
let names = core_undrained()
|
||||
function undrained_case(base_st: mut BaseState) -> void {
|
||||
let a: Queue<int> = queue_new(base_st, "left_behind")
|
||||
let b: Queue<int> = queue_new(base_st, "read")
|
||||
q_push(base_st, a, 1)
|
||||
q_push(base_st, b, 2)
|
||||
q_drain(base_st, b)
|
||||
let names = core_undrained(base_st)
|
||||
expect_eq(len(names), 1)
|
||||
expect(names[0] == "left_behind")
|
||||
q_drain(a)
|
||||
expect_eq(len(core_undrained()), 0)
|
||||
q_drain(base_st, a)
|
||||
expect_eq(len(core_undrained(base_st)), 0)
|
||||
}
|
||||
|
||||
test "a queue drains in the order it was pushed" { fifo_case() }
|
||||
test "a queue holds records" { record_case() }
|
||||
test "clear drops what is waiting" { clear_case() }
|
||||
test "the undrained queues are named, and a drain clears the report" { undrained_case() }
|
||||
test "a queue drains in the order it was pushed" (base_st: mut BaseState) { fifo_case(base_st) }
|
||||
test "a queue holds records" (base_st: mut BaseState) { record_case(base_st) }
|
||||
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" (base_st: mut BaseState) { undrained_case(base_st) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,19 +3,21 @@
|
|||
import "ludic.base"
|
||||
program RegistryTest {
|
||||
numbers float
|
||||
var trail: string = ""
|
||||
function late_tick(t: Tick) -> void { trail = trail + "l" }
|
||||
function early_tick(t: Tick) -> void { trail = trail + "e" }
|
||||
function added_tick(t: Tick) -> void { trail = trail + "a" }
|
||||
state RegistrytestState {
|
||||
trail: string = ""
|
||||
}
|
||||
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 early { phase: PH_INPUT, tick: fn early_tick }
|
||||
test "the declared systems run, phase by phase" {
|
||||
expect_eq(core_count(), 2)
|
||||
test "the declared systems run, phase by phase" (base_st: mut BaseState, registrytest_st: RegistrytestState) {
|
||||
expect_eq(core_count(base_st), 2)
|
||||
expect(Systems[SYS_LATE].key == "late")
|
||||
let s = system_new("added", PH_INPUT)
|
||||
s.tick = fn added_tick
|
||||
core_add(s)
|
||||
core_tick_all(tick_new(0.016, 1, 0.0))
|
||||
expect(trail == "eal")
|
||||
core_add(base_st, s)
|
||||
core_tick_all(base_st, tick_new(0.016, 1, 0.0))
|
||||
expect(registrytest_st.trail == "eal")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,57 +10,57 @@ program RouteTest {
|
|||
bind FishingWorld { is_water: fn water_everywhere_but_land }
|
||||
|
||||
# the route: a landed fish goes into the pack
|
||||
function route_fishing_pack(t: Tick) -> void {
|
||||
let fish = q_drain(caught)
|
||||
for i in 0 .. len(fish) { pack_add(fish[i].species, 1) }
|
||||
function route_fishing_pack(base_st: mut BaseState, toy_fishing_st: ToyFishingState, toy_pack_st: mut ToyPackState, t: Tick) -> void {
|
||||
let fish = q_drain(base_st, toy_fishing_st.caught)
|
||||
for i in 0 .. len(fish) { pack_add(toy_pack_st, fish[i].species, 1) }
|
||||
}
|
||||
|
||||
function game_start() -> void {
|
||||
core_clear()
|
||||
core_add(fishing_system())
|
||||
function game_start(base_st: mut BaseState) -> void {
|
||||
core_clear(base_st)
|
||||
core_add(base_st, fishing_system())
|
||||
let r = system_new("route.fishing_pack", PH_RESOLVE)
|
||||
r.tick = fn route_fishing_pack
|
||||
core_add(r)
|
||||
core_add(pack_system())
|
||||
core_reset_all()
|
||||
core_add(base_st, r)
|
||||
core_add(base_st, pack_system())
|
||||
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" {
|
||||
game_start()
|
||||
expect(fishing_cast(1.0, 2.0))
|
||||
expect(fishing_cast(3.0, 2.0))
|
||||
expect(fishing_cast(5.0, 2.0))
|
||||
expect(not fishing_cast(500.0, 2.0))
|
||||
core_tick_all(tick_new(1.0 / 60.0, 0, 0.0))
|
||||
expect_eq(pack_total(), 3)
|
||||
expect_eq(len(core_undrained()), 0)
|
||||
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(base_st)
|
||||
expect(fishing_cast(toy_fishing_st, 1.0, 2.0))
|
||||
expect(fishing_cast(toy_fishing_st, 3.0, 2.0))
|
||||
expect(fishing_cast(toy_fishing_st, 5.0, 2.0))
|
||||
expect(not fishing_cast(toy_fishing_st, 500.0, 2.0))
|
||||
core_tick_all(base_st, tick_new(1.0 / 60.0, 0, 0.0))
|
||||
expect_eq(pack_total(toy_pack_st), 3)
|
||||
expect_eq(len(core_undrained(base_st)), 0)
|
||||
}
|
||||
|
||||
test "the pack saves its own section and the fishing, which saves nothing, is reset" {
|
||||
game_start()
|
||||
fishing_cast(1.0, 1.0)
|
||||
core_tick_all(tick_new(1.0 / 60.0, 0, 0.0))
|
||||
let text = save_encode(core_save_all())
|
||||
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(base_st)
|
||||
fishing_cast(toy_fishing_st, 1.0, 1.0)
|
||||
core_tick_all(base_st, tick_new(1.0 / 60.0, 0, 0.0))
|
||||
let text = save_encode(core_save_all(base_st))
|
||||
let tree = save_decode(text)
|
||||
expect_eq(Value.count(tree), 1)
|
||||
expect_eq(load_section(tree, "pack").version, 1)
|
||||
expect(not load_section(tree, "fishing").found)
|
||||
core_reset_all()
|
||||
expect_eq(pack_total(), 0)
|
||||
core_load_all(save_decode(text))
|
||||
expect_eq(pack_total(), 1)
|
||||
core_reset_all(base_st)
|
||||
expect_eq(pack_total(toy_pack_st), 0)
|
||||
core_load_all(base_st, save_decode(text))
|
||||
expect_eq(pack_total(toy_pack_st), 1)
|
||||
}
|
||||
|
||||
test "the same seed lands the same fish" {
|
||||
game_start()
|
||||
fishing_cast(1.0, 1.0)
|
||||
core_tick_all(tick_new(1.0 / 60.0, 0, 0.0))
|
||||
let first = save_encode(core_save_all())
|
||||
game_start()
|
||||
fishing_cast(1.0, 1.0)
|
||||
core_tick_all(tick_new(1.0 / 60.0, 0, 0.0))
|
||||
expect(save_encode(core_save_all()) == first)
|
||||
test "the same seed lands the same fish" (base_st: mut BaseState, toy_fishing_st: mut ToyFishingState) {
|
||||
game_start(base_st)
|
||||
fishing_cast(toy_fishing_st, 1.0, 1.0)
|
||||
core_tick_all(base_st, tick_new(1.0 / 60.0, 0, 0.0))
|
||||
let first = save_encode(core_save_all(base_st))
|
||||
game_start(base_st)
|
||||
fishing_cast(toy_fishing_st, 1.0, 1.0)
|
||||
core_tick_all(base_st, tick_new(1.0 / 60.0, 0, 0.0))
|
||||
expect(save_encode(core_save_all(base_st)) == first)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,31 +3,33 @@
|
|||
import "ludic.base"
|
||||
program SystemTest {
|
||||
numbers float
|
||||
var trail: string = ""
|
||||
var apples: int = 0
|
||||
var hours: float = 0.0
|
||||
state SystemtestState {
|
||||
trail: string = ""
|
||||
apples: int = 0
|
||||
hours: float = 0.0
|
||||
}
|
||||
|
||||
function draw_tick(t: Tick) -> void { trail = trail + "d" }
|
||||
function think_tick(t: Tick) -> void { trail = trail + "t" }
|
||||
function input_tick(t: Tick) -> void { trail = trail + "i" }
|
||||
function commit_tick(t: Tick) -> void { trail = trail + "c" }
|
||||
function apples_init() -> void { trail = trail + "A" }
|
||||
function apples_reset() -> void { apples = 0 }
|
||||
function apples_tick(t: Tick) -> void { apples += 1 }
|
||||
function apples_save() -> Val {
|
||||
function draw_tick(systemtest_st: mut SystemtestState, t: Tick) -> void { systemtest_st.trail = systemtest_st.trail + "d" }
|
||||
function think_tick(systemtest_st: mut SystemtestState, t: Tick) -> void { systemtest_st.trail = systemtest_st.trail + "t" }
|
||||
function input_tick(systemtest_st: mut SystemtestState, t: Tick) -> void { systemtest_st.trail = systemtest_st.trail + "i" }
|
||||
function commit_tick(systemtest_st: mut SystemtestState, t: Tick) -> void { systemtest_st.trail = systemtest_st.trail + "c" }
|
||||
function apples_init(systemtest_st: mut SystemtestState) -> void { systemtest_st.trail = systemtest_st.trail + "A" }
|
||||
function apples_reset(systemtest_st: mut SystemtestState) -> void { systemtest_st.apples = 0 }
|
||||
function apples_tick(systemtest_st: mut SystemtestState, t: Tick) -> void { systemtest_st.apples += 1 }
|
||||
function apples_save(systemtest_st: SystemtestState) -> Val {
|
||||
let v = Value.object()
|
||||
sv_put_int(v, "n", apples)
|
||||
sv_put_int(v, "n", systemtest_st.apples)
|
||||
return v
|
||||
}
|
||||
function apples_load(v: Val, version: int) -> void { apples = sv_int(v, "n", 0) * version }
|
||||
function clock_reset() -> void { hours = 6.0 }
|
||||
function clock_tick(t: Tick) -> void { hours = hours + t.hours }
|
||||
function clock_save() -> Val {
|
||||
function apples_load(systemtest_st: mut SystemtestState, v: Val, version: int) -> void { systemtest_st.apples = sv_int(v, "n", 0) * version }
|
||||
function clock_reset(systemtest_st: mut SystemtestState) -> void { systemtest_st.hours = 6.0 }
|
||||
function clock_tick(systemtest_st: mut SystemtestState, t: Tick) -> void { systemtest_st.hours = systemtest_st.hours + t.hours }
|
||||
function clock_save(systemtest_st: SystemtestState) -> Val {
|
||||
let v = Value.object()
|
||||
sv_put_float(v, "h", hours)
|
||||
sv_put_float(v, "h", systemtest_st.hours)
|
||||
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 {
|
||||
let s = system_new(key, phase)
|
||||
|
|
@ -35,71 +37,71 @@ program SystemTest {
|
|||
return s
|
||||
}
|
||||
|
||||
function setup() -> void {
|
||||
core_clear()
|
||||
trail = ""
|
||||
core_add(sys("draw", PH_PRESENT, fn draw_tick))
|
||||
core_add(sys("think", PH_SIMULATE, fn think_tick))
|
||||
function setup(base_st: mut BaseState, systemtest_st: mut SystemtestState) -> void {
|
||||
core_clear(base_st)
|
||||
systemtest_st.trail = ""
|
||||
core_add(base_st, sys("draw", PH_PRESENT, fn draw_tick))
|
||||
core_add(base_st, sys("think", PH_SIMULATE, fn think_tick))
|
||||
let a = sys("apples", PH_SIMULATE, fn apples_tick)
|
||||
a.init = fn apples_init
|
||||
a.reset = fn apples_reset
|
||||
a.save = fn apples_save
|
||||
a.load = fn apples_load
|
||||
a.version = 2
|
||||
core_add(a)
|
||||
core_add(sys("input", PH_INPUT, fn input_tick))
|
||||
core_add(sys("commit", PH_COMMIT, fn commit_tick))
|
||||
core_add(base_st, a)
|
||||
core_add(base_st, sys("input", PH_INPUT, fn input_tick))
|
||||
core_add(base_st, sys("commit", PH_COMMIT, fn commit_tick))
|
||||
let c = sys("clock", PH_SIMULATE, fn clock_tick)
|
||||
c.reset = fn clock_reset
|
||||
c.save = fn clock_save
|
||||
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" {
|
||||
setup()
|
||||
core_tick_all(tick_new(1.0 / 60.0, 0, 0.0))
|
||||
expect(trail == "itcd")
|
||||
expect_eq(apples, 1)
|
||||
test "tick runs phase by phase, in the order added within a phase" (base_st: mut BaseState, systemtest_st: mut SystemtestState) {
|
||||
setup(base_st, systemtest_st)
|
||||
core_tick_all(base_st, tick_new(1.0 / 60.0, 0, 0.0))
|
||||
expect(systemtest_st.trail == "itcd")
|
||||
expect_eq(systemtest_st.apples, 1)
|
||||
}
|
||||
|
||||
test "init and reset run in the order added and skip a null verb" {
|
||||
setup()
|
||||
core_init_all()
|
||||
expect(trail == "A")
|
||||
apples = 9
|
||||
core_reset_all()
|
||||
expect_eq(apples, 0)
|
||||
expect(hours == 6.0)
|
||||
expect_eq(core_count(), 6)
|
||||
test "init and reset run in the order added and skip a null verb" (base_st: mut BaseState, systemtest_st: mut SystemtestState) {
|
||||
setup(base_st, systemtest_st)
|
||||
core_init_all(base_st)
|
||||
expect(systemtest_st.trail == "A")
|
||||
systemtest_st.apples = 9
|
||||
core_reset_all(base_st)
|
||||
expect_eq(systemtest_st.apples, 0)
|
||||
expect(systemtest_st.hours == 6.0)
|
||||
expect_eq(core_count(base_st), 6)
|
||||
}
|
||||
|
||||
test "save writes each system under its key with its version, and load reads it back" {
|
||||
setup()
|
||||
core_reset_all()
|
||||
apples = 4
|
||||
core_tick_all(tick_new(0.016, 1, 0.5))
|
||||
let root = core_save_all()
|
||||
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(base_st, systemtest_st)
|
||||
core_reset_all(base_st)
|
||||
systemtest_st.apples = 4
|
||||
core_tick_all(base_st, tick_new(0.016, 1, 0.5))
|
||||
let root = core_save_all(base_st)
|
||||
expect_eq(Value.count(root), 2)
|
||||
expect_eq(load_section(root, "apples").version, 2)
|
||||
expect_eq(load_section(root, "clock").version, 1)
|
||||
let text = save_encode(root)
|
||||
core_reset_all()
|
||||
core_load_all(save_decode(text))
|
||||
expect_eq(apples, 10)
|
||||
expect(hours == 6.5)
|
||||
core_reset_all(base_st)
|
||||
core_load_all(base_st, save_decode(text))
|
||||
expect_eq(systemtest_st.apples, 10)
|
||||
expect(systemtest_st.hours == 6.5)
|
||||
}
|
||||
|
||||
test "a system with no section in the save is reset" {
|
||||
setup()
|
||||
apples = 3
|
||||
hours = 11.0
|
||||
test "a system with no section in the save is reset" (base_st: mut BaseState, systemtest_st: mut SystemtestState) {
|
||||
setup(base_st, systemtest_st)
|
||||
systemtest_st.apples = 3
|
||||
systemtest_st.hours = 11.0
|
||||
let root = save_tree()
|
||||
let v = Value.object()
|
||||
sv_put_float(v, "h", 20.25)
|
||||
save_section(root, "clock", 1, v)
|
||||
core_load_all(root)
|
||||
expect_eq(apples, 0)
|
||||
expect(hours == 20.25)
|
||||
core_load_all(base_st, root)
|
||||
expect_eq(systemtest_st.apples, 0)
|
||||
expect(systemtest_st.hours == 20.25)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,26 +4,28 @@ numbers float
|
|||
import "ludic.base"
|
||||
export property Caught { species: int = 0, weight: float = 0.0 }
|
||||
export port FishingWorld { is_water: fn(float, float) -> bool } # its port
|
||||
var dice: Rng = null
|
||||
var casts: int = 0
|
||||
export var caught: Queue<Caught> = null # its facts
|
||||
export function fishing_cast(x: float, z: float) -> bool { # its verb
|
||||
export state ToyFishingState {
|
||||
dice: Rng = null
|
||||
casts: int = 0
|
||||
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 }
|
||||
casts += 1
|
||||
toy_fishing_st.casts += 1
|
||||
return true
|
||||
}
|
||||
function fishing_reset() -> void {
|
||||
casts = 0
|
||||
dice = rng_new(7)
|
||||
caught = queue_new("fishing.caught")
|
||||
function fishing_reset(base_st: mut BaseState, toy_fishing_st: mut ToyFishingState) -> void {
|
||||
toy_fishing_st.casts = 0
|
||||
toy_fishing_st.dice = rng_new(7)
|
||||
toy_fishing_st.caught = queue_new(base_st, "fishing.caught")
|
||||
}
|
||||
function fishing_tick(t: Tick) -> void {
|
||||
while casts > 0 {
|
||||
function fishing_tick(base_st: mut BaseState, toy_fishing_st: mut ToyFishingState, t: Tick) -> void {
|
||||
while toy_fishing_st.casts > 0 {
|
||||
let c = new Caught
|
||||
c.species = rng_between(dice, 0, 2)
|
||||
c.weight = 0.5 + rng_float(dice)
|
||||
q_push(caught, c)
|
||||
casts -= 1
|
||||
c.species = rng_between(toy_fishing_st.dice, 0, 2)
|
||||
c.weight = 0.5 + rng_float(toy_fishing_st.dice)
|
||||
q_push(base_st, toy_fishing_st.caught, c)
|
||||
toy_fishing_st.casts -= 1
|
||||
}
|
||||
}
|
||||
export function fishing_system() -> System {
|
||||
|
|
|
|||
|
|
@ -2,22 +2,24 @@
|
|||
module toy_pack uses ludic_base
|
||||
numbers float
|
||||
import "ludic.base"
|
||||
var counts: []int = null
|
||||
export function pack_add(kind: int, n: int) -> void { counts[kind] = counts[kind] + n }
|
||||
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) }
|
||||
export state ToyPackState {
|
||||
counts: []int = null
|
||||
}
|
||||
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()
|
||||
sv_put_ints(v, "counts", counts)
|
||||
sv_put_ints(v, "counts", toy_pack_st.counts)
|
||||
return v
|
||||
}
|
||||
function pack_load(v: Val, version: int) -> void {
|
||||
pack_reset()
|
||||
function pack_load(toy_pack_st: mut ToyPackState, v: Val, version: int) -> void {
|
||||
pack_reset(toy_pack_st)
|
||||
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 {
|
||||
let s = system_new("pack", PH_COMMIT)
|
||||
|
|
|
|||
|
|
@ -1,70 +1,70 @@
|
|||
# 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
|
||||
# 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 {
|
||||
if chr_fps and chr_eyes_allowed() {
|
||||
chr_camera_eyes(dt)
|
||||
export function char_camera(character_st: mut CharacterState, dt: float) -> void {
|
||||
if character_st.chr_fps and chr_eyes_allowed(character_st) {
|
||||
chr_camera_eyes(character_st, dt)
|
||||
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)
|
||||
export function char_camera_third(dt: float) -> void {
|
||||
chr_eyes_now = false
|
||||
export function char_camera_third(character_st: mut CharacterState, dt: float) -> void {
|
||||
character_st.chr_eyes_now = false
|
||||
let k = Math.min(1.0, 10.0 * dt)
|
||||
var ty = chr_y + 1.45
|
||||
if chr_swim { ty = chr_y + 0.9 }
|
||||
chr_piv_x = Math.lerp(chr_piv_x, chr_x, k)
|
||||
chr_piv_y = Math.lerp(chr_piv_y, ty, k)
|
||||
chr_piv_z = Math.lerp(chr_piv_z, chr_z, k)
|
||||
let cyw = Math.cos(chr_cam_yaw)
|
||||
let syw = Math.sin(chr_cam_yaw)
|
||||
let cpt = Math.cos(chr_cam_pitch)
|
||||
var ty = character_st.chr_y + 1.45
|
||||
if character_st.chr_swim { ty = character_st.chr_y + 0.9 }
|
||||
character_st.chr_piv_x = Math.lerp(character_st.chr_piv_x, character_st.chr_x, k)
|
||||
character_st.chr_piv_y = Math.lerp(character_st.chr_piv_y, ty, k)
|
||||
character_st.chr_piv_z = Math.lerp(character_st.chr_piv_z, character_st.chr_z, k)
|
||||
let cyw = Math.cos(character_st.chr_cam_yaw)
|
||||
let syw = Math.sin(character_st.chr_cam_yaw)
|
||||
let cpt = Math.cos(character_st.chr_cam_pitch)
|
||||
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 ax = chr_piv_x + cyw * chr_shoulder()
|
||||
let az = chr_piv_z - syw * chr_shoulder()
|
||||
let cx = ax - fx * chr_cam_dist
|
||||
var cyy = chr_piv_y - fy * chr_cam_dist
|
||||
let cz = az - fz * chr_cam_dist
|
||||
let ax = character_st.chr_piv_x + cyw * chr_shoulder(character_st)
|
||||
let az = character_st.chr_piv_z - syw * chr_shoulder(character_st)
|
||||
let cx = ax - fx * character_st.chr_cam_dist
|
||||
var cyy = character_st.chr_piv_y - fy * character_st.chr_cam_dist
|
||||
let cz = az - fz * character_st.chr_cam_dist
|
||||
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
|
||||
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_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 }
|
||||
chr_eye_x = cx
|
||||
chr_eye_y = cyy
|
||||
chr_eye_z = cz
|
||||
character_st.chr_eye_x = cx
|
||||
character_st.chr_eye_y = cyy
|
||||
character_st.chr_eye_z = cz
|
||||
let ddx = ax - cx
|
||||
let ddz = az - cz
|
||||
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_yaw = Math.atan2(-ddx, -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
|
||||
function chr_camera_eyes(dt: float) -> void {
|
||||
chr_eyes_now = true
|
||||
function chr_camera_eyes(character_st: mut CharacterState, dt: float) -> void {
|
||||
character_st.chr_eyes_now = true
|
||||
var eye = 1.62
|
||||
if chr_swim { eye = 0.42 }
|
||||
if chr_dive { eye = 0.30 }
|
||||
let moving = Math.clamp(chr_speed / 5.0, 0.0, 1.0)
|
||||
let want = 0.035 * moving * (1.0 - Math.cos(chr_phase * 2.0))
|
||||
chr_fps_bob = Math.lerp(chr_fps_bob, want, Math.min(1.0, 12.0 * dt))
|
||||
if character_st.chr_swim { eye = 0.42 }
|
||||
if character_st.chr_dive { eye = 0.30 }
|
||||
let moving = Math.clamp(character_st.chr_speed / 5.0, 0.0, 1.0)
|
||||
let want = 0.035 * moving * (1.0 - Math.cos(character_st.chr_phase * 2.0))
|
||||
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
|
||||
let ey = Math.max(chr_y + eye + chr_fps_bob, CharacterGround.bed(chr_x, chr_z) + 0.30)
|
||||
chr_piv_x = chr_x
|
||||
chr_piv_y = ey
|
||||
chr_piv_z = chr_z
|
||||
chr_eye_x = chr_x
|
||||
chr_eye_y = ey
|
||||
chr_eye_z = chr_z
|
||||
chr_eye_yaw = chr_cam_yaw
|
||||
chr_eye_pitch = chr_cam_pitch
|
||||
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)
|
||||
character_st.chr_piv_x = character_st.chr_x
|
||||
character_st.chr_piv_y = ey
|
||||
character_st.chr_piv_z = character_st.chr_z
|
||||
character_st.chr_eye_x = character_st.chr_x
|
||||
character_st.chr_eye_y = ey
|
||||
character_st.chr_eye_z = character_st.chr_z
|
||||
character_st.chr_eye_yaw = character_st.chr_cam_yaw
|
||||
character_st.chr_eye_pitch = character_st.chr_cam_pitch
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# 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.
|
||||
export function char_slope_blocks(nh: float, fx: float, fz: float) -> bool {
|
||||
if nh - chr_y > CHAR_STEP { return true }
|
||||
let sx = chr_x + fx * CHAR_SLOPE_PROBE
|
||||
let sz = chr_z + fz * CHAR_SLOPE_PROBE
|
||||
return CharacterGround.height(sx, sz) - chr_y > CHAR_SLOPE_PROBE * CharacterGround.slope_limit()
|
||||
export function char_slope_blocks(character_st: CharacterState, nh: float, fx: float, fz: float) -> bool {
|
||||
if nh - character_st.chr_y > CHAR_STEP { return true }
|
||||
let sx = character_st.chr_x + fx * CHAR_SLOPE_PROBE
|
||||
let sz = character_st.chr_z + fz * CHAR_SLOPE_PROBE
|
||||
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_*
|
||||
var chr_out_x: float = 0.0
|
||||
var chr_out_z: float = 0.0
|
||||
function chr_push(x: float, z: float, r: float, y0: float, y1: float) -> bool {
|
||||
chr_ask_x = x
|
||||
chr_ask_z = z
|
||||
function chr_push(character_st: mut CharacterState, x: float, z: float, r: float, y0: float, y1: float) -> bool {
|
||||
character_st.chr_ask_x = x
|
||||
character_st.chr_ask_z = z
|
||||
let hit = CharacterGround.push(x, z, r, y0, y1)
|
||||
chr_out_x = x
|
||||
chr_out_z = z
|
||||
character_st.chr_out_x = x
|
||||
character_st.chr_out_z = z
|
||||
if hit {
|
||||
chr_out_x = CharacterGround.pushed_x()
|
||||
chr_out_z = CharacterGround.pushed_z()
|
||||
character_st.chr_out_x = CharacterGround.pushed_x()
|
||||
character_st.chr_out_z = CharacterGround.pushed_z()
|
||||
}
|
||||
return hit
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,14 +39,70 @@ export port CharacterInput {
|
|||
look_always: fn() -> bool = fn chr__no # the look is held: never settle behind
|
||||
}
|
||||
|
||||
var chr_ask_x: float = 0.0
|
||||
var chr_ask_z: float = 0.0
|
||||
export state CharacterState {
|
||||
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__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__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_z() -> float { return chr_ask_z }
|
||||
function chr__asked_x(character_st: CharacterState) -> float { return character_st.chr_ask_x }
|
||||
function chr__asked_z(character_st: CharacterState) -> float { return character_st.chr_ask_z }
|
||||
function chr__slope_plain(x: float, z: float) -> float {
|
||||
let e = 2.0
|
||||
let dx = CharacterGround.height(x + e, z) - CharacterGround.height(x - e, z)
|
||||
|
|
|
|||
|
|
@ -1,48 +1,48 @@
|
|||
# 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_y() -> float { return chr_y }
|
||||
export function body_z() -> float { return chr_z }
|
||||
export function body_yaw() -> float { return chr_yaw }
|
||||
export function body_speed() -> float { return chr_speed }
|
||||
export function body_moving() -> float { return chr_move }
|
||||
export function body_run() -> float { return chr_run }
|
||||
export function body_phase() -> float { return chr_phase }
|
||||
export function body_time() -> float { return chr_time }
|
||||
export function body_water() -> float { return chr_water }
|
||||
export function body_airborne() -> bool { return chr_air }
|
||||
export function body_vy() -> float { return chr_vy }
|
||||
export function body_landing() -> float { return chr_land }
|
||||
export function body_steep() -> float { return chr_steep }
|
||||
export function body_step() -> float { return chr_step }
|
||||
export function body_sitting() -> bool { return chr_sit }
|
||||
export function body_sit_mode() -> int { return chr_sit_mode }
|
||||
export function body_sit_t() -> float { return chr_sit_t }
|
||||
export function body_swimming() -> bool { return chr_swim }
|
||||
export function body_diving() -> bool { return chr_dive }
|
||||
export function body_depth() -> float { return chr_depth }
|
||||
export function body_breath() -> float { return chr_breath }
|
||||
export function body_floating() -> float { return chr_float }
|
||||
export function body_swim_phase() -> float { return chr_swim_phase }
|
||||
export function body_can_run() -> bool { return chr_can_run }
|
||||
export function body_walk_speed() -> float { return chr_walk_speed }
|
||||
export function body_run_speed() -> float { return chr_run_speed }
|
||||
export function body_held() -> bool { return chr_holds != 0 }
|
||||
export function body_fps() -> bool { return chr_fps }
|
||||
export function body_cam_yaw() -> float { return chr_cam_yaw }
|
||||
export function body_cam_pitch() -> float { return chr_cam_pitch }
|
||||
export function body_cam_dist() -> float { return chr_cam_dist }
|
||||
export function body_x(character_st: CharacterState) -> float { return character_st.chr_x }
|
||||
export function body_y(character_st: CharacterState) -> float { return character_st.chr_y }
|
||||
export function body_z(character_st: CharacterState) -> float { return character_st.chr_z }
|
||||
export function body_yaw(character_st: CharacterState) -> float { return character_st.chr_yaw }
|
||||
export function body_speed(character_st: CharacterState) -> float { return character_st.chr_speed }
|
||||
export function body_moving(character_st: CharacterState) -> float { return character_st.chr_move }
|
||||
export function body_run(character_st: CharacterState) -> float { return character_st.chr_run }
|
||||
export function body_phase(character_st: CharacterState) -> float { return character_st.chr_phase }
|
||||
export function body_time(character_st: CharacterState) -> float { return character_st.chr_time }
|
||||
export function body_water(character_st: CharacterState) -> float { return character_st.chr_water }
|
||||
export function body_airborne(character_st: CharacterState) -> bool { return character_st.chr_air }
|
||||
export function body_vy(character_st: CharacterState) -> float { return character_st.chr_vy }
|
||||
export function body_landing(character_st: CharacterState) -> float { return character_st.chr_land }
|
||||
export function body_steep(character_st: CharacterState) -> float { return character_st.chr_steep }
|
||||
export function body_step(character_st: CharacterState) -> float { return character_st.chr_step }
|
||||
export function body_sitting(character_st: CharacterState) -> bool { return character_st.chr_sit }
|
||||
export function body_sit_mode(character_st: CharacterState) -> int { return character_st.chr_sit_mode }
|
||||
export function body_sit_t(character_st: CharacterState) -> float { return character_st.chr_sit_t }
|
||||
export function body_swimming(character_st: CharacterState) -> bool { return character_st.chr_swim }
|
||||
export function body_diving(character_st: CharacterState) -> bool { return character_st.chr_dive }
|
||||
export function body_depth(character_st: CharacterState) -> float { return character_st.chr_depth }
|
||||
export function body_breath(character_st: CharacterState) -> float { return character_st.chr_breath }
|
||||
export function body_floating(character_st: CharacterState) -> float { return character_st.chr_float }
|
||||
export function body_swim_phase(character_st: CharacterState) -> float { return character_st.chr_swim_phase }
|
||||
export function body_can_run(character_st: CharacterState) -> bool { return character_st.chr_can_run }
|
||||
export function body_walk_speed(character_st: CharacterState) -> float { return character_st.chr_walk_speed }
|
||||
export function body_run_speed(character_st: CharacterState) -> float { return character_st.chr_run_speed }
|
||||
export function body_held(character_st: CharacterState) -> bool { return character_st.chr_holds != 0 }
|
||||
export function body_fps(character_st: CharacterState) -> bool { return character_st.chr_fps }
|
||||
export function body_cam_yaw(character_st: CharacterState) -> float { return character_st.chr_cam_yaw }
|
||||
export function body_cam_pitch(character_st: CharacterState) -> float { return character_st.chr_cam_pitch }
|
||||
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
|
||||
export function body_eye_x() -> float { return chr_eye_x }
|
||||
export function body_eye_y() -> float { return chr_eye_y }
|
||||
export function body_eye_z() -> float { return chr_eye_z }
|
||||
export function body_eye_yaw() -> float { return chr_eye_yaw }
|
||||
export function body_eye_pitch() -> float { return chr_eye_pitch }
|
||||
export function body_eyes_now() -> bool { return chr_eyes_now }
|
||||
export function body_eye_x(character_st: CharacterState) -> float { return character_st.chr_eye_x }
|
||||
export function body_eye_y(character_st: CharacterState) -> float { return character_st.chr_eye_y }
|
||||
export function body_eye_z(character_st: CharacterState) -> float { return character_st.chr_eye_z }
|
||||
export function body_eye_yaw(character_st: CharacterState) -> float { return character_st.chr_eye_yaw }
|
||||
export function body_eye_pitch(character_st: CharacterState) -> float { return character_st.chr_eye_pitch }
|
||||
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
|
||||
export function body_safe_x() -> float { return chr_safe_x }
|
||||
export function body_safe_z() -> float { return chr_safe_z }
|
||||
export function body_safe_x(character_st: CharacterState) -> float { return character_st.chr_safe_x }
|
||||
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
|
||||
export function body_breath_max() -> float { return CHAR_BREATH_MAX * CharacterBody.breath() }
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
# 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
|
||||
# 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)
|
||||
if Math.abs(g - from_y) > CHAR_DROP { 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 hi = g
|
||||
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)
|
||||
}
|
||||
|
||||
export function char_safe_spot(x: float, z: float, from_y: float, max_depth: float) -> bool {
|
||||
chr_safe_x = x
|
||||
chr_safe_z = z
|
||||
if chr_safe_ok(x, z, from_y, max_depth) { return true }
|
||||
export function char_safe_spot(character_st: mut CharacterState, x: float, z: float, from_y: float, max_depth: float) -> bool {
|
||||
character_st.chr_safe_x = x
|
||||
character_st.chr_safe_z = z
|
||||
if chr_safe_ok(character_st, x, z, from_y, max_depth) { return true }
|
||||
for r in 0 .. 9 {
|
||||
let rad = 0.8 + float(r) * 1.1
|
||||
for k in 0 .. 16 {
|
||||
let a = float(k) * (chr_pi / 8.0)
|
||||
let px = x + Math.sin(a) * rad
|
||||
let pz = z + Math.cos(a) * rad
|
||||
if chr_safe_ok(px, pz, from_y, max_depth) {
|
||||
chr_safe_x = px
|
||||
chr_safe_z = pz
|
||||
if chr_safe_ok(character_st, px, pz, from_y, max_depth) {
|
||||
character_st.chr_safe_x = px
|
||||
character_st.chr_safe_z = pz
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,54 +1,54 @@
|
|||
# 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
|
||||
# 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 {
|
||||
chr_sit = true
|
||||
chr_sit_mode = mode
|
||||
chr_speed = 0.0
|
||||
chr_move = 0.0
|
||||
chr_run = 0.0
|
||||
chr_sit_x = x
|
||||
chr_sit_z = z
|
||||
chr_sit_yaw = yaw
|
||||
chr_water = CharacterGround.water(chr_x, chr_z)
|
||||
export function char_sit(character_st: mut CharacterState, x: float, z: float, yaw: float, mode: int) -> void {
|
||||
character_st.chr_sit = true
|
||||
character_st.chr_sit_mode = mode
|
||||
character_st.chr_speed = 0.0
|
||||
character_st.chr_move = 0.0
|
||||
character_st.chr_run = 0.0
|
||||
character_st.chr_sit_x = x
|
||||
character_st.chr_sit_z = z
|
||||
character_st.chr_sit_yaw = yaw
|
||||
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
|
||||
if chr_sit { want = 1.0 }
|
||||
chr_sit_t = char_toward(chr_sit_t, want, 4.0 * dt)
|
||||
if not chr_sit { return }
|
||||
if character_st.chr_sit { want = 1.0 }
|
||||
character_st.chr_sit_t = char_toward(character_st.chr_sit_t, want, 4.0 * dt)
|
||||
if not character_st.chr_sit { return }
|
||||
let k = Math.min(1.0, 8.0 * dt)
|
||||
chr_x = Math.lerp(chr_x, chr_sit_x, k)
|
||||
chr_z = Math.lerp(chr_z, chr_sit_z, k)
|
||||
chr_yaw = chr_yaw + char_wrap(chr_sit_yaw - chr_yaw) * k
|
||||
chr_y = CharacterGround.height(chr_x, chr_z)
|
||||
chr_water = CharacterGround.water(chr_x, chr_z)
|
||||
character_st.chr_x = Math.lerp(character_st.chr_x, character_st.chr_sit_x, k)
|
||||
character_st.chr_z = Math.lerp(character_st.chr_z, character_st.chr_sit_z, k)
|
||||
character_st.chr_yaw = character_st.chr_yaw + char_wrap(character_st.chr_sit_yaw - character_st.chr_yaw) * k
|
||||
character_st.chr_y = CharacterGround.height(character_st.chr_x, character_st.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,
|
||||
# not in the water. Asked again while sitting, it stands up. The answer is a CHAR_* for the game
|
||||
# to word.
|
||||
export function char_sit_here() -> int {
|
||||
if chr_sit {
|
||||
char_stand()
|
||||
export function char_sit_here(character_st: mut CharacterState) -> int {
|
||||
if character_st.chr_sit {
|
||||
char_stand(character_st)
|
||||
return CHAR_STOOD
|
||||
}
|
||||
if CharacterBody.riding() { return CHAR_NOT_RIDING }
|
||||
if chr_air { return CHAR_NOT_AIRBORNE }
|
||||
if CharacterGround.slope(chr_x, chr_z) > 0.35 { return CHAR_NOT_STEEP }
|
||||
if chr_y < chr_water + 0.9 { return CHAR_NOT_WET }
|
||||
char_sit(chr_x, chr_z, chr_yaw, CHAR_SIT_GROUND)
|
||||
if character_st.chr_air { return CHAR_NOT_AIRBORNE }
|
||||
if CharacterGround.slope(character_st.chr_x, character_st.chr_z) > 0.35 { return CHAR_NOT_STEEP }
|
||||
if character_st.chr_y < character_st.chr_water + 0.9 { return CHAR_NOT_WET }
|
||||
char_sit(character_st, character_st.chr_x, character_st.chr_z, character_st.chr_yaw, CHAR_SIT_GROUND)
|
||||
return CHAR_SAT
|
||||
}
|
||||
|
||||
# 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 {
|
||||
chr_sit = false
|
||||
if char_safe_spot(chr_x, chr_z, chr_y, 0.0) {
|
||||
chr_x = chr_safe_x
|
||||
chr_z = chr_safe_z
|
||||
chr_y = CharacterGround.height(chr_x, chr_z)
|
||||
export function char_stand(character_st: mut CharacterState) -> void {
|
||||
character_st.chr_sit = false
|
||||
if char_safe_spot(character_st, character_st.chr_x, character_st.chr_z, character_st.chr_y, 0.0) {
|
||||
character_st.chr_x = character_st.chr_safe_x
|
||||
character_st.chr_z = character_st.chr_safe_z
|
||||
character_st.chr_y = CharacterGround.height(character_st.chr_x, character_st.chr_z)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,77 +1,25 @@
|
|||
# state.ludic - the body, its camera, and the queue of what happened to it
|
||||
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
|
||||
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> {
|
||||
if chr_fact_q == null { chr_fact_q = queue_new("character.facts") }
|
||||
return chr_fact_q
|
||||
export function char_facts(base_st: mut BaseState, character_st: mut CharacterState) -> Queue<CharacterFact> {
|
||||
if character_st.chr_fact_q == null { character_st.chr_fact_q = queue_new(base_st, "character.facts") }
|
||||
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
|
||||
f.what = what
|
||||
f.x = chr_x
|
||||
f.y = chr_y
|
||||
f.z = chr_z
|
||||
f.x = character_st.chr_x
|
||||
f.y = character_st.chr_y
|
||||
f.z = character_st.chr_z
|
||||
f.v = v
|
||||
q_push(char_facts(), f)
|
||||
q_push(base_st, char_facts(base_st, character_st), f)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,61 +1,61 @@
|
|||
# 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 {
|
||||
let diff = char_wrap(Math.atan2(-dx, -dz) - chr_yaw)
|
||||
chr_yaw = chr_yaw + Math.clamp(diff, -rate, rate)
|
||||
function chr_turn_to(character_st: mut CharacterState, dx: float, dz: float, rate: float) -> float {
|
||||
let diff = char_wrap(Math.atan2(-dx, -dz) - character_st.chr_yaw)
|
||||
character_st.chr_yaw = character_st.chr_yaw + Math.clamp(diff, -rate, rate)
|
||||
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
|
||||
if mag > 0.05 and not chr_air {
|
||||
let diff = chr_turn_to(dx, dz, Math.lerp(10.0, 5.0, chr_run) * dt)
|
||||
if mag > 0.05 and not character_st.chr_air {
|
||||
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)
|
||||
var top = chr_walk_speed * CharacterBody.pace()
|
||||
if run { top = chr_run_speed * CharacterBody.pace() }
|
||||
var top = character_st.chr_walk_speed * CharacterBody.pace()
|
||||
if run { top = character_st.chr_run_speed * CharacterBody.pace() }
|
||||
if CharacterBody.sneaking() { top = 1.3 }
|
||||
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
|
||||
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
|
||||
if chr_swim {
|
||||
if character_st.chr_swim {
|
||||
target = mag * 1.5
|
||||
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
|
||||
if target > chr_speed { acc = 9.0 }
|
||||
if chr_swim { acc = 4.5 }
|
||||
chr_speed = char_toward(chr_speed, target, acc * dt)
|
||||
if target > character_st.chr_speed { acc = 9.0 }
|
||||
if character_st.chr_swim { acc = 4.5 }
|
||||
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
|
||||
# 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 {
|
||||
if chr_land > 0.0 { chr_land = chr_land - dt }
|
||||
if chr_steep > 0.0 { chr_steep = chr_steep - dt }
|
||||
if chr_swim { chr_swim_phase = (chr_swim_phase + dt * (1.6 + chr_speed * 0.9)) % (2.0 * chr_pi) }
|
||||
let stride = Math.lerp(1.5, 2.7, chr_run)
|
||||
let phase0 = chr_phase
|
||||
chr_phase = (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 chr_cross(phase0, chr_phase, 0.0) or chr_cross(phase0, chr_phase, chr_pi) { chr_say(CHAR_FOOTFALL, chr_speed) }
|
||||
function chr_gait(base_st: mut BaseState, character_st: mut CharacterState, dist: float, dyaw: float, dpitch: float, dt: float) -> void {
|
||||
if character_st.chr_land > 0.0 { character_st.chr_land = character_st.chr_land - dt }
|
||||
if character_st.chr_steep > 0.0 { character_st.chr_steep = character_st.chr_steep - dt }
|
||||
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, character_st.chr_run)
|
||||
let phase0 = character_st.chr_phase
|
||||
character_st.chr_phase = (character_st.chr_phase + dist / stride * (2.0 * chr_pi)) % (2.0 * chr_pi)
|
||||
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, 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
|
||||
if chr_speed > 0.12 { mv = 1.0 }
|
||||
chr_move = char_toward(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)
|
||||
chr_look(dyaw, dpitch)
|
||||
if character_st.chr_speed > 0.12 { mv = 1.0 }
|
||||
character_st.chr_move = char_toward(character_st.chr_move, mv, 5.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(character_st, dyaw, dpitch)
|
||||
# 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() {
|
||||
chr_cam_yaw = chr_cam_yaw + char_wrap(chr_yaw - chr_cam_yaw) * Math.min(1.0, 1.1 * dt)
|
||||
if dyaw == 0.0 and character_st.chr_speed > 0.4 and not character_st.chr_lock 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.1 * dt)
|
||||
}
|
||||
}
|
||||
|
||||
function chr_look(dyaw: float, dpitch: float) -> void {
|
||||
chr_cam_yaw = chr_cam_yaw + dyaw
|
||||
chr_cam_pitch = Math.clamp(chr_cam_pitch + dpitch, -0.96, 0.61)
|
||||
function chr_look(character_st: mut CharacterState, dyaw: float, dpitch: float) -> void {
|
||||
character_st.chr_cam_yaw = character_st.chr_cam_yaw + dyaw
|
||||
character_st.chr_cam_pitch = Math.clamp(character_st.chr_cam_pitch + dpitch, -0.96, 0.61)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
# 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
|
||||
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) }
|
||||
chr_dive_tick(jump, dt)
|
||||
chr_breath_tick(dt)
|
||||
chr_current_tick(dt)
|
||||
let step = chr_speed * dt
|
||||
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(character_st, jump, dt)
|
||||
chr_breath_tick(base_st, character_st, dt)
|
||||
chr_current_tick(base_st, character_st, dt)
|
||||
let step = character_st.chr_speed * dt
|
||||
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)
|
||||
let mx = chr_out_x - chr_x
|
||||
let mz = chr_out_z - chr_z
|
||||
chr_step = Math.sqrt(mx * mx + mz * mz)
|
||||
chr_x = chr_out_x
|
||||
chr_z = chr_out_z
|
||||
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 = character_st.chr_out_x - character_st.chr_x
|
||||
let mz = character_st.chr_out_z - character_st.chr_z
|
||||
character_st.chr_step = Math.sqrt(mx * mx + mz * mz)
|
||||
character_st.chr_x = character_st.chr_out_x
|
||||
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
|
||||
let bob = 0.04 * Math.sin(chr_time * 3.8)
|
||||
chr_y = chr_water - 0.30 + bob - chr_depth
|
||||
let bob = 0.04 * Math.sin(character_st.chr_time * 3.8)
|
||||
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
|
||||
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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
# 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)
|
||||
export function char_reset() -> void {
|
||||
char_teleport(0.0, 0.0, 0.0)
|
||||
chr_time = 0.0
|
||||
chr_phase = 0.0
|
||||
chr_breath = CHAR_BREATH_MAX
|
||||
chr_can_run = true
|
||||
chr_sit_t = 0.0
|
||||
chr_steep = 0.0
|
||||
chr_land = 0.0
|
||||
chr_edge_said = 0.0
|
||||
chr_fps = false
|
||||
q_clear(char_facts())
|
||||
export function char_reset(base_st: mut BaseState, character_st: mut CharacterState) -> void {
|
||||
char_teleport(character_st, 0.0, 0.0, 0.0)
|
||||
character_st.chr_time = 0.0
|
||||
character_st.chr_phase = 0.0
|
||||
character_st.chr_breath = CHAR_BREATH_MAX
|
||||
character_st.chr_can_run = true
|
||||
character_st.chr_sit_t = 0.0
|
||||
character_st.chr_steep = 0.0
|
||||
character_st.chr_land = 0.0
|
||||
character_st.chr_edge_said = 0.0
|
||||
character_st.chr_fps = false
|
||||
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 {
|
||||
let s = system_new("character", PH_INPUT)
|
||||
|
|
|
|||
|
|
@ -20,24 +20,32 @@ program CharacterTest {
|
|||
return -10000.0
|
||||
}
|
||||
# a boulder at (0, 6), a metre round and 40 cm high, and a pillar at (-6, 0)
|
||||
var px: float = 0.0
|
||||
var pz: float = 0.0
|
||||
function fk_ring(x: float, z: float, cx: float, cz: float, r: float) -> bool {
|
||||
state CharactertestState {
|
||||
px: float = 0.0
|
||||
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 dz = z - cz
|
||||
let d = Math.sqrt(dx * dx + dz * dz)
|
||||
if d >= r { return false }
|
||||
let k = r / Math.max(d, 0.001)
|
||||
px = cx + dx * k
|
||||
pz = cz + dz * k
|
||||
charactertest_st.px = cx + dx * k
|
||||
charactertest_st.pz = cz + dz * k
|
||||
return true
|
||||
}
|
||||
function fk_push(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 }
|
||||
return fk_ring(x, z, -6.0, 0.0, 0.5 + r)
|
||||
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(charactertest_st, x, z, 0.0, 6.0, 1.0 + r) { return true }
|
||||
return fk_ring(charactertest_st, x, z, -6.0, 0.0, 0.5 + r)
|
||||
}
|
||||
function fk_px() -> float { return px }
|
||||
function fk_pz() -> float { return pz }
|
||||
function fk_px(charactertest_st: CharactertestState) -> float { return charactertest_st.px }
|
||||
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 {
|
||||
let dx = x
|
||||
let dz = z - 6.0
|
||||
|
|
@ -45,228 +53,222 @@ program CharacterTest {
|
|||
return base
|
||||
}
|
||||
|
||||
var riding: bool = false
|
||||
var rides: int = 0
|
||||
var sneaking: bool = false
|
||||
var pace: float = 1.0
|
||||
var eyes: bool = true
|
||||
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 }
|
||||
function fk_riding(charactertest_st: CharactertestState) -> bool { return charactertest_st.riding }
|
||||
function fk_ride(charactertest_st: mut CharactertestState, ix: float, iz: float, run: bool, dt: float) -> void { charactertest_st.rides += 1 }
|
||||
function fk_sneak(charactertest_st: CharactertestState) -> bool { return charactertest_st.sneaking }
|
||||
function fk_pace(charactertest_st: CharactertestState) -> float { return charactertest_st.pace }
|
||||
function fk_eyes(charactertest_st: CharactertestState) -> bool { return charactertest_st.eyes }
|
||||
|
||||
var mz: float = 0.0
|
||||
function fk_move_z() -> float { return mz }
|
||||
function fk_move_z(charactertest_st: CharactertestState) -> float { return charactertest_st.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 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 }
|
||||
|
||||
function fresh(x: float, z: float, yaw: float) -> void {
|
||||
char_reset()
|
||||
char_release(CHAR_HOLD_SCREEN)
|
||||
char_teleport(x, z, yaw)
|
||||
function fresh(base_st: mut BaseState, character_st: mut CharacterState, x: float, z: float, yaw: float) -> void {
|
||||
char_reset(base_st, character_st)
|
||||
char_release(character_st, CHAR_HOLD_SCREEN)
|
||||
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 {
|
||||
var n = 0
|
||||
for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } }
|
||||
return n
|
||||
}
|
||||
# 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)
|
||||
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
|
||||
const EAST: float = -1.5707963
|
||||
const SOUTH: float = 3.14159265
|
||||
|
||||
test "a 45 cm deck is a step at 60 Hz and at 240" {
|
||||
fresh(1.0, 0.0, EAST)
|
||||
walk(2.0, 60.0, false)
|
||||
expect(body_x() > 5.0)
|
||||
expect_near(body_y(), 0.45, 0.01)
|
||||
fresh(1.0, 0.0, EAST)
|
||||
walk(2.0, 240.0, false)
|
||||
expect(body_x() > 5.0)
|
||||
expect_near(body_y(), 0.45, 0.01)
|
||||
expect_eq(count(facts(), CHAR_TOO_STEEP), 0)
|
||||
test "a 45 cm deck is a step at 60 Hz and at 240" (base_st: mut BaseState, character_st: mut CharacterState) {
|
||||
fresh(base_st, character_st, 1.0, 0.0, EAST)
|
||||
walk(base_st, character_st, 2.0, 60.0, false)
|
||||
expect(body_x(character_st) > 5.0)
|
||||
expect_near(body_y(character_st), 0.45, 0.01)
|
||||
fresh(base_st, character_st, 1.0, 0.0, EAST)
|
||||
walk(base_st, character_st, 2.0, 240.0, false)
|
||||
expect(body_x(character_st) > 5.0)
|
||||
expect_near(body_y(character_st), 0.45, 0.01)
|
||||
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" {
|
||||
fresh(0.0, -8.0, 0.0)
|
||||
walk(3.0, 30.0, false)
|
||||
let at30 = body_z()
|
||||
expect(body_steep() > 0.0)
|
||||
expect_eq(count(facts(), CHAR_TOO_STEEP), 1)
|
||||
fresh(0.0, -8.0, 0.0)
|
||||
walk(3.0, 240.0, false)
|
||||
expect_near(body_z(), at30, 0.25)
|
||||
expect(body_z() > -10.0)
|
||||
expect(body_y() < 0.3)
|
||||
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(base_st, character_st, 0.0, -8.0, 0.0)
|
||||
walk(base_st, character_st, 3.0, 30.0, false)
|
||||
let at30 = body_z(character_st)
|
||||
expect(body_steep(character_st) > 0.0)
|
||||
expect_eq(count(facts(base_st, character_st), CHAR_TOO_STEEP), 1)
|
||||
fresh(base_st, character_st, 0.0, -8.0, 0.0)
|
||||
walk(base_st, character_st, 3.0, 240.0, false)
|
||||
expect_near(body_z(character_st), at30, 0.25)
|
||||
expect(body_z(character_st) > -10.0)
|
||||
expect(body_y(character_st) < 0.3)
|
||||
}
|
||||
|
||||
test "a gentle rise is walked up" {
|
||||
fresh(0.0, 8.0, SOUTH)
|
||||
walk(3.0, 60.0, false)
|
||||
expect(body_z() > 14.0)
|
||||
expect(body_y() > 1.0)
|
||||
test "a gentle rise is walked up" (base_st: mut BaseState, character_st: mut CharacterState) {
|
||||
fresh(base_st, character_st, 0.0, 8.0, SOUTH)
|
||||
walk(base_st, character_st, 3.0, 60.0, false)
|
||||
expect(body_z(character_st) > 14.0)
|
||||
expect(body_y(character_st) > 1.0)
|
||||
}
|
||||
|
||||
test "the boots set the limit: a rise they cannot take is refused" {
|
||||
fresh(0.0, 8.0, SOUTH)
|
||||
walk(0.2, 60.0, false)
|
||||
expect(not char_slope_blocks(body_y() + 0.1, 0.0, 1.0))
|
||||
test "the boots set the limit: a rise they cannot take is refused" (base_st: mut BaseState, character_st: mut CharacterState) {
|
||||
fresh(base_st, character_st, 0.0, 8.0, SOUTH)
|
||||
walk(base_st, character_st, 0.2, 60.0, false)
|
||||
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" {
|
||||
fresh(0.0, 0.0, SOUTH)
|
||||
walk(2.0, 60.0, false)
|
||||
expect_near(body_speed(), 3.2, 0.05)
|
||||
fresh(0.0, 0.0, SOUTH)
|
||||
walk(2.0, 60.0, true)
|
||||
expect_near(body_speed(), 6.8, 0.05)
|
||||
pace = 0.5
|
||||
fresh(0.0, 0.0, SOUTH)
|
||||
walk(2.0, 60.0, false)
|
||||
expect_near(body_speed(), 1.6, 0.05)
|
||||
pace = 1.0
|
||||
sneaking = true
|
||||
fresh(0.0, 0.0, SOUTH)
|
||||
walk(2.0, 60.0, true)
|
||||
expect_near(body_speed(), 1.3, 0.05)
|
||||
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(base_st, character_st, 0.0, 0.0, SOUTH)
|
||||
walk(base_st, character_st, 2.0, 60.0, false)
|
||||
expect_near(body_speed(character_st), 3.2, 0.05)
|
||||
fresh(base_st, character_st, 0.0, 0.0, SOUTH)
|
||||
walk(base_st, character_st, 2.0, 60.0, true)
|
||||
expect_near(body_speed(character_st), 6.8, 0.05)
|
||||
charactertest_st.pace = 0.5
|
||||
fresh(base_st, character_st, 0.0, 0.0, SOUTH)
|
||||
walk(base_st, character_st, 2.0, 60.0, false)
|
||||
expect_near(body_speed(character_st), 1.6, 0.05)
|
||||
charactertest_st.pace = 1.0
|
||||
charactertest_st.sneaking = true
|
||||
fresh(base_st, character_st, 0.0, 0.0, SOUTH)
|
||||
walk(base_st, character_st, 2.0, 60.0, true)
|
||||
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" {
|
||||
fresh(0.0, 3.0, SOUTH)
|
||||
walk(2.0, 60.0, false)
|
||||
expect(body_z() < 5.1)
|
||||
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(base_st, character_st, 0.0, 3.0, SOUTH)
|
||||
walk(base_st, character_st, 2.0, 60.0, false)
|
||||
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(3.0, 6.0, 0.0), 0.0, 0.001)
|
||||
# and the world's own ground never includes it
|
||||
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" {
|
||||
fresh(18.0, 0.0, EAST)
|
||||
walk(3.0, 60.0, false)
|
||||
let fs = facts()
|
||||
expect(body_swimming())
|
||||
test "wading shelves into a swim, and the shallows give the ground back" (base_st: mut BaseState, character_st: mut CharacterState) {
|
||||
fresh(base_st, character_st, 18.0, 0.0, EAST)
|
||||
walk(base_st, character_st, 3.0, 60.0, false)
|
||||
let fs = facts(base_st, character_st)
|
||||
expect(body_swimming(character_st))
|
||||
expect_eq(count(fs, CHAR_STARTED_SWIMMING), 1)
|
||||
expect_near(body_y(), -0.30, 0.06)
|
||||
expect(body_breath() > 40.0)
|
||||
char_face(-EAST)
|
||||
char_look(-EAST, 0.0)
|
||||
walk(8.0, 60.0, false)
|
||||
expect(not body_swimming())
|
||||
expect_eq(count(facts(), CHAR_LEFT_WATER), 1)
|
||||
expect_near(body_y(character_st), -0.30, 0.06)
|
||||
expect(body_breath(character_st) > 40.0)
|
||||
char_face(character_st, -EAST)
|
||||
char_look(character_st, -EAST, 0.0)
|
||||
walk(base_st, character_st, 8.0, 60.0, false)
|
||||
expect(not body_swimming(character_st))
|
||||
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" {
|
||||
fresh(30.0, 0.0, 0.0)
|
||||
char_resume_swim(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) }
|
||||
let fs = facts()
|
||||
test "the jump held dives, the air runs out, and the body surfaces" (base_st: mut BaseState, character_st: mut CharacterState) {
|
||||
fresh(base_st, character_st, 30.0, 0.0, 0.0)
|
||||
char_resume_swim(character_st, false, 0.0, 2.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(base_st, character_st)
|
||||
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) }
|
||||
expect(body_depth() < 0.05)
|
||||
expect(body_breath() > 10.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(character_st) < 0.05)
|
||||
expect(body_breath(character_st) > 10.0)
|
||||
}
|
||||
|
||||
test "a jump leaves the ground, falls, lands, and says both" {
|
||||
fresh(0.0, 0.0, 0.0)
|
||||
char_update(0.0, 0.0, false, true, 0.0, 0.0, 1.0 / 60.0)
|
||||
expect(body_airborne())
|
||||
test "a jump leaves the ground, falls, lands, and says both" (base_st: mut BaseState, character_st: mut CharacterState) {
|
||||
fresh(base_st, character_st, 0.0, 0.0, 0.0)
|
||||
char_update(base_st, character_st, 0.0, 0.0, false, true, 0.0, 0.0, 1.0 / 60.0)
|
||||
expect(body_airborne(character_st))
|
||||
var guard = 0
|
||||
while body_airborne() and guard < 200 {
|
||||
char_update(0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0)
|
||||
while body_airborne(character_st) and guard < 200 {
|
||||
char_update(base_st, character_st, 0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0)
|
||||
guard += 1
|
||||
}
|
||||
let fs = facts()
|
||||
expect(not body_airborne())
|
||||
let fs = facts(base_st, character_st)
|
||||
expect(not body_airborne(character_st))
|
||||
expect_eq(count(fs, CHAR_JUMPED), 1)
|
||||
expect_eq(count(fs, CHAR_LANDED), 1)
|
||||
expect(guard > 30)
|
||||
char_allow_run(false)
|
||||
char_update(0.0, 0.0, false, true, 0.0, 0.0, 1.0 / 60.0)
|
||||
expect(not body_airborne())
|
||||
char_allow_run(character_st, false)
|
||||
char_update(base_st, character_st, 0.0, 0.0, false, true, 0.0, 0.0, 1.0 / 60.0)
|
||||
expect(not body_airborne(character_st))
|
||||
}
|
||||
|
||||
test "the safe spot clears a collider, keeps out of deep water, and allows a wade when asked" {
|
||||
expect(char_safe_spot(-6.0, 0.0, 0.0, 0.0))
|
||||
let dx = body_safe_x() + 6.0
|
||||
let dz = body_safe_z()
|
||||
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(character_st, -6.0, 0.0, 0.0, 0.0))
|
||||
let dx = body_safe_x(character_st) + 6.0
|
||||
let dz = body_safe_z(character_st)
|
||||
expect(dx * dx + dz * dz > 0.81)
|
||||
expect(not char_safe_spot(40.0, 0.0, 0.0, 0.0))
|
||||
expect(char_safe_spot(20.5, 0.0, 0.0, 1.25))
|
||||
expect(char_safe_spot(15.0, 0.0, 0.0, 0.0))
|
||||
expect_near(body_safe_x(), 15.0, 0.001)
|
||||
expect(not char_safe_spot(character_st, 40.0, 0.0, 0.0, 0.0))
|
||||
expect(char_safe_spot(character_st, 20.5, 0.0, 0.0, 1.25))
|
||||
expect(char_safe_spot(character_st, 15.0, 0.0, 0.0, 0.0))
|
||||
expect_near(body_safe_x(character_st), 15.0, 0.001)
|
||||
}
|
||||
|
||||
test "sitting where you stand, and the refusals" {
|
||||
fresh(0.0, 0.0, 0.0)
|
||||
expect_eq(char_sit_here(), CHAR_SAT)
|
||||
for i in 0 .. 30 { char_update(0.0, 0.0, false, false, 0.0, 0.0, 1.0 / 60.0) }
|
||||
expect(body_sitting())
|
||||
expect_near(body_sit_t(), 1.0, 0.01)
|
||||
expect_eq(char_sit_here(), CHAR_STOOD)
|
||||
expect(not body_sitting())
|
||||
fresh(0.0, -14.0, 0.0)
|
||||
expect_eq(char_sit_here(), CHAR_NOT_STEEP)
|
||||
fresh(24.0, 0.0, 0.0)
|
||||
char_resume_swim(false, 0.0, 10.0)
|
||||
expect_eq(char_sit_here(), CHAR_NOT_WET)
|
||||
riding = true
|
||||
fresh(0.0, 0.0, 0.0)
|
||||
expect_eq(char_sit_here(), CHAR_NOT_RIDING)
|
||||
riding = false
|
||||
test "sitting where you stand, and the refusals" (base_st: mut BaseState, character_st: mut CharacterState, charactertest_st: mut CharactertestState) {
|
||||
fresh(base_st, character_st, 0.0, 0.0, 0.0)
|
||||
expect_eq(char_sit_here(character_st), CHAR_SAT)
|
||||
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(character_st))
|
||||
expect_near(body_sit_t(character_st), 1.0, 0.01)
|
||||
expect_eq(char_sit_here(character_st), CHAR_STOOD)
|
||||
expect(not body_sitting(character_st))
|
||||
fresh(base_st, character_st, 0.0, -14.0, 0.0)
|
||||
expect_eq(char_sit_here(character_st), CHAR_NOT_STEEP)
|
||||
fresh(base_st, character_st, 24.0, 0.0, 0.0)
|
||||
char_resume_swim(character_st, false, 0.0, 10.0)
|
||||
expect_eq(char_sit_here(character_st), CHAR_NOT_WET)
|
||||
charactertest_st.riding = true
|
||||
fresh(base_st, character_st, 0.0, 0.0, 0.0)
|
||||
expect_eq(char_sit_here(character_st), CHAR_NOT_RIDING)
|
||||
charactertest_st.riding = false
|
||||
}
|
||||
|
||||
test "a hold stops the stick, and only its own reason lets it go" {
|
||||
fresh(0.0, 0.0, 0.0)
|
||||
mz = 1.0
|
||||
char_hold(CHAR_HOLD_STAGE)
|
||||
char_hold(CHAR_HOLD_SCREEN)
|
||||
for i in 0 .. 60 { char_tick(1.0 / 60.0) }
|
||||
expect_near(body_z(), 0.0, 0.001)
|
||||
char_release(CHAR_HOLD_SCREEN)
|
||||
char_release(CHAR_HOLD_SCREEN)
|
||||
expect(body_held())
|
||||
char_release(CHAR_HOLD_STAGE)
|
||||
for i in 0 .. 60 { char_tick(1.0 / 60.0) }
|
||||
expect(body_z() < -1.0)
|
||||
mz = 0.0
|
||||
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(base_st, character_st, 0.0, 0.0, 0.0)
|
||||
charactertest_st.mz = 1.0
|
||||
char_hold(character_st, CHAR_HOLD_STAGE)
|
||||
char_hold(character_st, CHAR_HOLD_SCREEN)
|
||||
for i in 0 .. 60 { char_tick(base_st, character_st, 1.0 / 60.0) }
|
||||
expect_near(body_z(character_st), 0.0, 0.001)
|
||||
char_release(character_st, CHAR_HOLD_SCREEN)
|
||||
char_release(character_st, CHAR_HOLD_SCREEN)
|
||||
expect(body_held(character_st))
|
||||
char_release(character_st, CHAR_HOLD_STAGE)
|
||||
for i in 0 .. 60 { char_tick(base_st, character_st, 1.0 / 60.0) }
|
||||
expect(body_z(character_st) < -1.0)
|
||||
charactertest_st.mz = 0.0
|
||||
}
|
||||
|
||||
test "riding hands the stick over and leaves the body where the ride puts it" {
|
||||
fresh(0.0, 0.0, 0.0)
|
||||
riding = true
|
||||
rides = 0
|
||||
char_board(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) }
|
||||
expect_eq(rides, 10)
|
||||
char_ride_at(4.0, 1.2, 3.0, 0.0, 2.0)
|
||||
expect_near(body_x(), 4.0, 0.001)
|
||||
expect_near(body_y(), 1.2, 0.001)
|
||||
riding = false
|
||||
char_step_off(5.0, 0.0)
|
||||
expect_near(body_y(), 0.45, 0.001)
|
||||
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(base_st, character_st, 0.0, 0.0, 0.0)
|
||||
charactertest_st.riding = true
|
||||
charactertest_st.rides = 0
|
||||
char_board(character_st, 3.0, 3.0, 0.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(charactertest_st.rides, 10)
|
||||
char_ride_at(character_st, 4.0, 1.2, 3.0, 0.0, 2.0)
|
||||
expect_near(body_x(character_st), 4.0, 0.001)
|
||||
expect_near(body_y(character_st), 1.2, 0.001)
|
||||
charactertest_st.riding = false
|
||||
char_step_off(character_st, 5.0, 0.0)
|
||||
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" {
|
||||
fresh(0.0, 0.0, 0.0)
|
||||
char_camera(1.0)
|
||||
expect(body_eye_z() > 1.0)
|
||||
expect(body_eye_y() > 0.55)
|
||||
expect(not body_eyes_now())
|
||||
expect_eq(char_fps_toggle(), CHAR_EYES_ON)
|
||||
char_camera(1.0)
|
||||
expect(body_eyes_now())
|
||||
expect_near(body_eye_y(), 1.62, 0.05)
|
||||
expect_eq(char_fps_toggle(), CHAR_EYES_OFF)
|
||||
char_sit(0.0, 0.0, 0.0, CHAR_SIT_SEAT)
|
||||
expect_eq(char_fps_toggle(), CHAR_EYES_SITTING)
|
||||
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(base_st, character_st, 0.0, 0.0, 0.0)
|
||||
char_camera(character_st, 1.0)
|
||||
expect(body_eye_z(character_st) > 1.0)
|
||||
expect(body_eye_y(character_st) > 0.55)
|
||||
expect(not body_eyes_now(character_st))
|
||||
expect_eq(char_fps_toggle(character_st), CHAR_EYES_ON)
|
||||
char_camera(character_st, 1.0)
|
||||
expect(body_eyes_now(character_st))
|
||||
expect_near(body_eye_y(character_st), 1.62, 0.05)
|
||||
expect_eq(char_fps_toggle(character_st), CHAR_EYES_OFF)
|
||||
char_sit(character_st, 0.0, 0.0, 0.0, CHAR_SIT_SEAT)
|
||||
expect_eq(char_fps_toggle(character_st), CHAR_EYES_SITTING)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,28 @@
|
|||
# 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 {
|
||||
var run = run0 and chr_can_run
|
||||
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 character_st.chr_can_run
|
||||
var ix = ix0
|
||||
var iz = iz0
|
||||
var jump = jump0
|
||||
chr_step = 0.0
|
||||
chr_time = chr_time + dt
|
||||
chr_water = CharacterGround.water(chr_x, chr_z)
|
||||
chr_sit_tick(dt)
|
||||
character_st.chr_step = 0.0
|
||||
character_st.chr_time = character_st.chr_time + dt
|
||||
character_st.chr_water = CharacterGround.water(character_st.chr_x, character_st.chr_z)
|
||||
chr_sit_tick(character_st, dt)
|
||||
if CharacterBody.riding() {
|
||||
chr_ride_update(ix, iz, run, dyaw, dpitch, dt)
|
||||
chr_ride_update(character_st, ix, iz, run, dyaw, dpitch, dt)
|
||||
return
|
||||
}
|
||||
if CharacterBody.sneaking() { run = false }
|
||||
# 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 chr_sit {
|
||||
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 character_st.chr_sit {
|
||||
ix = 0.0
|
||||
iz = 0.0
|
||||
jump = false
|
||||
}
|
||||
# the stick in the camera's frame: forward on the ground is (-sin yaw, -cos yaw)
|
||||
let cy = Math.cos(chr_cam_yaw)
|
||||
let sy = Math.sin(chr_cam_yaw)
|
||||
let cy = Math.cos(character_st.chr_cam_yaw)
|
||||
let sy = Math.sin(character_st.chr_cam_yaw)
|
||||
var dx = -sy * iz + cy * ix
|
||||
var dz = -cy * iz - sy * ix
|
||||
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
|
||||
mag = 1.0
|
||||
}
|
||||
chr_steer(dx, dz, mag, run, dt)
|
||||
let dist = chr_travel(dx, dz, -Math.sin(chr_yaw), -Math.cos(chr_yaw), jump, dt)
|
||||
chr_gait(dist, dyaw, dpitch, dt)
|
||||
char_camera(dt)
|
||||
chr_steer(character_st, dx, dz, mag, run, 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(base_st, character_st, dist, dyaw, dpitch, dt)
|
||||
char_camera(character_st, dt)
|
||||
}
|
||||
|
||||
# something else carries the body (CharacterBody.ride moves it with char_ride_at); the orbit
|
||||
# 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)
|
||||
chr_look(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) }
|
||||
char_camera(dt)
|
||||
chr_look(character_st, dyaw, dpitch)
|
||||
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(character_st, dt)
|
||||
}
|
||||
|
||||
# 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 iz = 0.0
|
||||
var run = false
|
||||
var jump = false
|
||||
var dyaw = 0.0
|
||||
var dpitch = 0.0
|
||||
if chr_holds == 0 {
|
||||
if character_st.chr_holds == 0 {
|
||||
ix = CharacterInput.move_x()
|
||||
iz = CharacterInput.move_z()
|
||||
run = CharacterInput.run()
|
||||
|
|
@ -62,7 +62,7 @@ export function char_tick(dt: float) -> void {
|
|||
dyaw = CharacterInput.look_yaw()
|
||||
dpitch = CharacterInput.look_pitch()
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,125 +1,125 @@
|
|||
# verbs.ludic - every change the rest of a game may make to the body
|
||||
export function char_config(walk: float, run: float) -> void {
|
||||
chr_walk_speed = walk
|
||||
chr_run_speed = run
|
||||
export function char_config(character_st: mut CharacterState, walk: float, run: float) -> void {
|
||||
character_st.chr_walk_speed = walk
|
||||
character_st.chr_run_speed = run
|
||||
}
|
||||
|
||||
# put the body somewhere, standing, the camera behind it
|
||||
export function char_teleport(x: float, z: float, yaw: float) -> void {
|
||||
chr_x = x
|
||||
chr_z = z
|
||||
chr_yaw = yaw
|
||||
chr_y = CharacterGround.height(x, z)
|
||||
char_halt()
|
||||
chr_sit = false
|
||||
chr_swim = false
|
||||
chr_dive = false
|
||||
chr_depth = 0.0
|
||||
chr_float = 0.0
|
||||
export function char_teleport(character_st: mut CharacterState, x: float, z: float, yaw: float) -> void {
|
||||
character_st.chr_x = x
|
||||
character_st.chr_z = z
|
||||
character_st.chr_yaw = yaw
|
||||
character_st.chr_y = CharacterGround.height(x, z)
|
||||
char_halt(character_st)
|
||||
character_st.chr_sit = false
|
||||
character_st.chr_swim = false
|
||||
character_st.chr_dive = false
|
||||
character_st.chr_depth = 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
|
||||
chr_water = CharacterGround.water(x, z)
|
||||
chr_cam_yaw = yaw
|
||||
chr_piv_x = chr_x
|
||||
chr_piv_y = chr_y + 1.45
|
||||
chr_piv_z = chr_z
|
||||
char_camera(1.0)
|
||||
character_st.chr_water = CharacterGround.water(x, z)
|
||||
character_st.chr_cam_yaw = yaw
|
||||
character_st.chr_piv_x = character_st.chr_x
|
||||
character_st.chr_piv_y = character_st.chr_y + 1.45
|
||||
character_st.chr_piv_z = character_st.chr_z
|
||||
char_camera(character_st, 1.0)
|
||||
}
|
||||
|
||||
# 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_release(reason: int) -> void { if (chr_holds & reason) != 0 { 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(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
|
||||
export function char_halt() -> void {
|
||||
chr_air = false
|
||||
chr_vy = 0.0
|
||||
chr_speed = 0.0
|
||||
chr_move = 0.0
|
||||
chr_run = 0.0
|
||||
export function char_halt(character_st: mut CharacterState) -> void {
|
||||
character_st.chr_air = false
|
||||
character_st.chr_vy = 0.0
|
||||
character_st.chr_speed = 0.0
|
||||
character_st.chr_move = 0.0
|
||||
character_st.chr_run = 0.0
|
||||
}
|
||||
|
||||
# out of strength in the water: the stroke stops and the body floats
|
||||
export function char_go_limp() -> void {
|
||||
chr_speed = 0.0
|
||||
chr_float = 1.0
|
||||
export function char_go_limp(character_st: mut CharacterState) -> void {
|
||||
character_st.chr_speed = 0.0
|
||||
character_st.chr_float = 1.0
|
||||
}
|
||||
|
||||
# set down somewhere without the teleport's reset (a swimmer turned back)
|
||||
export function char_push_to(x: float, z: float) -> void {
|
||||
chr_x = x
|
||||
chr_z = z
|
||||
chr_speed = 0.0
|
||||
export function char_push_to(character_st: mut CharacterState, x: float, z: float) -> void {
|
||||
character_st.chr_x = x
|
||||
character_st.chr_z = z
|
||||
character_st.chr_speed = 0.0
|
||||
}
|
||||
|
||||
# stood on the ground at a point, facing a way
|
||||
export function char_stand_on(x: float, z: float, yaw: float) -> void {
|
||||
chr_x = x
|
||||
chr_z = z
|
||||
chr_yaw = yaw
|
||||
chr_y = CharacterGround.height(x, z)
|
||||
export function char_stand_on(character_st: mut CharacterState, x: float, z: float, yaw: float) -> void {
|
||||
character_st.chr_x = x
|
||||
character_st.chr_z = z
|
||||
character_st.chr_yaw = yaw
|
||||
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
|
||||
export function char_resume_swim(dive: bool, depth: float, breath: float) -> void {
|
||||
chr_water = CharacterGround.water(chr_x, chr_z)
|
||||
chr_swim = true
|
||||
chr_dive = dive
|
||||
chr_depth = depth
|
||||
chr_breath = breath
|
||||
chr_y = chr_water - 0.30 - chr_depth
|
||||
export function char_resume_swim(character_st: mut CharacterState, dive: bool, depth: float, breath: float) -> void {
|
||||
character_st.chr_water = CharacterGround.water(character_st.chr_x, character_st.chr_z)
|
||||
character_st.chr_swim = true
|
||||
character_st.chr_dive = dive
|
||||
character_st.chr_depth = depth
|
||||
character_st.chr_breath = breath
|
||||
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
|
||||
export function char_map_changed() -> void {
|
||||
chr_swim = false
|
||||
chr_dive = false
|
||||
chr_depth = 0.0
|
||||
chr_water = CharacterGround.water(chr_x, chr_z)
|
||||
export function char_map_changed(character_st: mut CharacterState) -> void {
|
||||
character_st.chr_swim = false
|
||||
character_st.chr_dive = false
|
||||
character_st.chr_depth = 0.0
|
||||
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_allow_run(on: bool) -> void { chr_can_run = on }
|
||||
export function char_look(yaw: float, pitch: float) -> void {
|
||||
chr_cam_yaw = yaw
|
||||
chr_cam_pitch = pitch
|
||||
export function char_face(character_st: mut CharacterState, yaw: float) -> void { character_st.chr_yaw = yaw }
|
||||
export function char_allow_run(character_st: mut CharacterState, on: bool) -> void { character_st.chr_can_run = on }
|
||||
export function char_look(character_st: mut CharacterState, yaw: float, pitch: float) -> void {
|
||||
character_st.chr_cam_yaw = yaw
|
||||
character_st.chr_cam_pitch = pitch
|
||||
}
|
||||
export function char_look_pitch(pitch: float) -> void { chr_cam_pitch = pitch }
|
||||
export function char_orbit(dist: float) -> void { chr_cam_dist = Math.clamp(dist, 1.8, 9.0) }
|
||||
export function char_lock_orbit(on: bool) -> void { chr_lock = on }
|
||||
export function char_set_fps(on: bool) -> void { chr_fps = on }
|
||||
export function char_look_pitch(character_st: mut CharacterState, pitch: float) -> void { character_st.chr_cam_pitch = pitch }
|
||||
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(character_st: mut CharacterState, on: bool) -> void { character_st.chr_lock = 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
|
||||
export function char_fps_toggle() -> int {
|
||||
if not chr_fps and not chr_eyes_allowed() {
|
||||
if chr_sit { return CHAR_EYES_SITTING }
|
||||
export function char_fps_toggle(character_st: mut CharacterState) -> int {
|
||||
if not character_st.chr_fps and not chr_eyes_allowed(character_st) {
|
||||
if character_st.chr_sit { return CHAR_EYES_SITTING }
|
||||
return CHAR_EYES_RIDING
|
||||
}
|
||||
chr_fps = not chr_fps
|
||||
if chr_fps { return CHAR_EYES_ON }
|
||||
character_st.chr_fps = not character_st.chr_fps
|
||||
if character_st.chr_fps { return CHAR_EYES_ON }
|
||||
return CHAR_EYES_OFF
|
||||
}
|
||||
|
||||
# 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 {
|
||||
chr_sit = false
|
||||
chr_x = x
|
||||
chr_z = z
|
||||
chr_yaw = yaw
|
||||
chr_speed = 0.0
|
||||
chr_move = 0.0
|
||||
chr_run = 0.0
|
||||
export function char_board(character_st: mut CharacterState, x: float, z: float, yaw: float) -> void {
|
||||
character_st.chr_sit = false
|
||||
character_st.chr_x = x
|
||||
character_st.chr_z = z
|
||||
character_st.chr_yaw = yaw
|
||||
character_st.chr_speed = 0.0
|
||||
character_st.chr_move = 0.0
|
||||
character_st.chr_run = 0.0
|
||||
}
|
||||
|
||||
export function char_ride_at(x: float, y: float, z: float, yaw: float, speed: float) -> void {
|
||||
chr_x = x
|
||||
chr_y = y
|
||||
chr_z = z
|
||||
chr_yaw = yaw
|
||||
chr_speed = speed
|
||||
export function char_ride_at(character_st: mut CharacterState, x: float, y: float, z: float, yaw: float, speed: float) -> void {
|
||||
character_st.chr_x = x
|
||||
character_st.chr_y = y
|
||||
character_st.chr_z = z
|
||||
character_st.chr_yaw = yaw
|
||||
character_st.chr_speed = speed
|
||||
}
|
||||
|
||||
export function char_step_off(x: float, z: float) -> void {
|
||||
chr_x = x
|
||||
chr_z = z
|
||||
chr_y = CharacterGround.height(x, z)
|
||||
export function char_step_off(character_st: mut CharacterState, x: float, z: float) -> void {
|
||||
character_st.chr_x = x
|
||||
character_st.chr_z = z
|
||||
character_st.chr_y = CharacterGround.height(x, z)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,69 +1,69 @@
|
|||
# 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 {
|
||||
if chr_swim {
|
||||
chr_swim_tick(dx, dz, jump, dt)
|
||||
return chr_speed * dt
|
||||
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 character_st.chr_swim {
|
||||
chr_swim_tick(base_st, character_st, dx, dz, jump, dt)
|
||||
return character_st.chr_speed * dt
|
||||
}
|
||||
if chr_air { return chr_fly(dx, dz, dt) }
|
||||
return chr_walk(fx, fz, jump, dt)
|
||||
if character_st.chr_air { return chr_fly(base_st, character_st, dx, dz, dt) }
|
||||
return chr_walk(base_st, character_st, fx, fz, jump, dt)
|
||||
}
|
||||
|
||||
# ballistic: gravity, the takeoff's velocity carried, a little steering
|
||||
function chr_fly(dx: float, dz: float, dt: float) -> float {
|
||||
chr_vy = chr_vy - 14.0 * dt
|
||||
chr_air_vx = chr_air_vx + dx * (4.0 * dt)
|
||||
chr_air_vz = 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_y = chr_y + chr_vy * dt
|
||||
let ground = char_stand_at(chr_out_x, chr_out_z, chr_y)
|
||||
chr_x = chr_out_x
|
||||
chr_z = chr_out_z
|
||||
if chr_y < ground and chr_vy < 0.0 {
|
||||
chr_y = ground
|
||||
chr_air = false
|
||||
chr_land = 0.22
|
||||
chr_speed = Math.sqrt(chr_air_vx * chr_air_vx + chr_air_vz * chr_air_vz)
|
||||
chr_say(CHAR_LANDED, chr_speed)
|
||||
function chr_fly(base_st: mut BaseState, character_st: mut CharacterState, dx: float, dz: float, dt: float) -> float {
|
||||
character_st.chr_vy = character_st.chr_vy - 14.0 * dt
|
||||
character_st.chr_air_vx = character_st.chr_air_vx + dx * (4.0 * dt)
|
||||
character_st.chr_air_vz = character_st.chr_air_vz + dz * (4.0 * dt)
|
||||
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)
|
||||
character_st.chr_y = character_st.chr_y + character_st.chr_vy * dt
|
||||
let ground = char_stand_at(character_st.chr_out_x, character_st.chr_out_z, character_st.chr_y)
|
||||
character_st.chr_x = character_st.chr_out_x
|
||||
character_st.chr_z = character_st.chr_out_z
|
||||
if character_st.chr_y < ground and character_st.chr_vy < 0.0 {
|
||||
character_st.chr_y = ground
|
||||
character_st.chr_air = false
|
||||
character_st.chr_land = 0.22
|
||||
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(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
|
||||
function chr_walk(fx: float, fz: float, jump: bool, dt: float) -> float {
|
||||
var dist = chr_speed * dt
|
||||
if dist > 0.0 { dist = chr_walk_step(fx, fz, dist) } else { chr_y = CharacterGround.height(chr_x, chr_z) }
|
||||
if jump and chr_can_run and chr_land < 0.05 {
|
||||
chr_air = true
|
||||
chr_vy = 5.0
|
||||
chr_air_vx = fx * chr_speed
|
||||
chr_air_vz = fz * chr_speed
|
||||
chr_say(CHAR_JUMPED, 0.0)
|
||||
function chr_walk(base_st: mut BaseState, character_st: mut CharacterState, fx: float, fz: float, jump: bool, dt: float) -> float {
|
||||
var dist = character_st.chr_speed * dt
|
||||
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 character_st.chr_can_run and character_st.chr_land < 0.05 {
|
||||
character_st.chr_air = true
|
||||
character_st.chr_vy = 5.0
|
||||
character_st.chr_air_vx = fx * character_st.chr_speed
|
||||
character_st.chr_air_vz = fz * character_st.chr_speed
|
||||
chr_say(base_st, character_st, CHAR_JUMPED, 0.0)
|
||||
}
|
||||
return dist
|
||||
}
|
||||
|
||||
function chr_walk_step(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)
|
||||
let nx = chr_out_x
|
||||
let nz = chr_out_z
|
||||
let nh = char_stand_at(nx, nz, chr_y)
|
||||
if char_slope_blocks(nh, fx, fz) {
|
||||
if not (chr_steep > 0.0) { chr_say(CHAR_TOO_STEEP, nh - chr_y) }
|
||||
chr_steep = 1.2
|
||||
chr_speed = 0.0
|
||||
function chr_walk_step(base_st: mut BaseState, character_st: mut CharacterState, fx: float, fz: float, dist: float) -> float {
|
||||
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 = character_st.chr_out_x
|
||||
let nz = character_st.chr_out_z
|
||||
let nh = char_stand_at(nx, nz, character_st.chr_y)
|
||||
if char_slope_blocks(character_st, nh, fx, fz) {
|
||||
if not (character_st.chr_steep > 0.0) { chr_say(base_st, character_st, CHAR_TOO_STEEP, nh - character_st.chr_y) }
|
||||
character_st.chr_steep = 1.2
|
||||
character_st.chr_speed = 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 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 {
|
||||
chr_enter_water(nx, nz)
|
||||
chr_enter_water(base_st, character_st, nx, nz)
|
||||
return dist
|
||||
}
|
||||
let mx = nx - chr_x
|
||||
let mz = nz - chr_z
|
||||
chr_step = Math.sqrt(mx * mx + mz * mz)
|
||||
chr_x = nx
|
||||
chr_z = nz
|
||||
chr_y = nh
|
||||
let mx = nx - character_st.chr_x
|
||||
let mz = nz - character_st.chr_z
|
||||
character_st.chr_step = Math.sqrt(mx * mx + mz * mz)
|
||||
character_st.chr_x = nx
|
||||
character_st.chr_z = nz
|
||||
character_st.chr_y = nh
|
||||
return dist
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,73 +1,73 @@
|
|||
# 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
|
||||
# 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 {
|
||||
if chr_swim { return }
|
||||
chr_swim = true
|
||||
chr_dive = false
|
||||
chr_depth = 0.0
|
||||
chr_breath = body_breath_max()
|
||||
chr_float = 0.0
|
||||
chr_air = false
|
||||
chr_vy = 0.0
|
||||
chr_sit = false
|
||||
chr_x = x
|
||||
chr_z = z
|
||||
chr_water = CharacterGround.water(x, z)
|
||||
chr_y = chr_water - 0.30
|
||||
chr_say(CHAR_STARTED_SWIMMING, 0.0)
|
||||
function chr_enter_water(base_st: mut BaseState, character_st: mut CharacterState, x: float, z: float) -> void {
|
||||
if character_st.chr_swim { return }
|
||||
character_st.chr_swim = true
|
||||
character_st.chr_dive = false
|
||||
character_st.chr_depth = 0.0
|
||||
character_st.chr_breath = body_breath_max()
|
||||
character_st.chr_float = 0.0
|
||||
character_st.chr_air = false
|
||||
character_st.chr_vy = 0.0
|
||||
character_st.chr_sit = false
|
||||
character_st.chr_x = x
|
||||
character_st.chr_z = z
|
||||
character_st.chr_water = CharacterGround.water(x, z)
|
||||
character_st.chr_y = character_st.chr_water - 0.30
|
||||
chr_say(base_st, character_st, CHAR_STARTED_SWIMMING, 0.0)
|
||||
}
|
||||
|
||||
function chr_leave_water() -> void {
|
||||
if not chr_swim { return }
|
||||
chr_swim = false
|
||||
chr_dive = false
|
||||
chr_depth = 0.0
|
||||
chr_y = CharacterGround.height(chr_x, chr_z)
|
||||
chr_speed = Math.min(chr_speed, 1.0)
|
||||
chr_say(CHAR_LEFT_WATER, 0.0)
|
||||
function chr_leave_water(base_st: mut BaseState, character_st: mut CharacterState) -> void {
|
||||
if not character_st.chr_swim { return }
|
||||
character_st.chr_swim = false
|
||||
character_st.chr_dive = false
|
||||
character_st.chr_depth = 0.0
|
||||
character_st.chr_y = CharacterGround.height(character_st.chr_x, character_st.chr_z)
|
||||
character_st.chr_speed = Math.min(character_st.chr_speed, 1.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
|
||||
function chr_dive_tick(jump: bool, dt: float) -> void {
|
||||
let bed = Math.max(chr_bed_depth(chr_x, chr_z), 0.0)
|
||||
function chr_dive_tick(character_st: mut CharacterState, jump: bool, dt: float) -> void {
|
||||
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))
|
||||
if jump and max_d > 0.2 {
|
||||
chr_dive = true
|
||||
chr_depth = Math.min(chr_depth + 1.2 * dt, max_d)
|
||||
character_st.chr_dive = true
|
||||
character_st.chr_depth = Math.min(character_st.chr_depth + 1.2 * dt, max_d)
|
||||
} else {
|
||||
chr_depth = Math.max(chr_depth - 1.6 * dt, 0.0)
|
||||
if chr_depth < 0.05 { chr_dive = false }
|
||||
character_st.chr_depth = Math.max(character_st.chr_depth - 1.6 * dt, 0.0)
|
||||
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
|
||||
function chr_breath_tick(dt: float) -> void {
|
||||
if not (chr_depth > 0.55) {
|
||||
chr_breath = Math.min(chr_breath + dt * 12.0, body_breath_max())
|
||||
function chr_breath_tick(base_st: mut BaseState, character_st: mut CharacterState, dt: float) -> void {
|
||||
if not (character_st.chr_depth > 0.55) {
|
||||
character_st.chr_breath = Math.min(character_st.chr_breath + dt * 12.0, body_breath_max())
|
||||
return
|
||||
}
|
||||
chr_breath = chr_breath - dt
|
||||
if chr_breath < 0.0 {
|
||||
chr_breath = 0.0
|
||||
chr_dive = false
|
||||
chr_depth = Math.max(chr_depth - 3.0 * dt, 0.0)
|
||||
chr_say(CHAR_OUT_OF_AIR, dt)
|
||||
character_st.chr_breath = character_st.chr_breath - dt
|
||||
if character_st.chr_breath < 0.0 {
|
||||
character_st.chr_breath = 0.0
|
||||
character_st.chr_dive = false
|
||||
character_st.chr_depth = Math.max(character_st.chr_depth - 3.0 * dt, 0.0)
|
||||
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
|
||||
# turning round always works. Said once every twelve seconds while it is strong.
|
||||
function chr_current_tick(dt: float) -> void {
|
||||
let fx = -Math.sin(chr_yaw)
|
||||
let fz = -Math.cos(chr_yaw)
|
||||
let take = Math.clamp(CharacterGround.current(chr_x, chr_z, fx, fz), 0.0, 1.0)
|
||||
function chr_current_tick(base_st: mut BaseState, character_st: mut CharacterState, dt: float) -> void {
|
||||
let fx = -Math.sin(character_st.chr_yaw)
|
||||
let fz = -Math.cos(character_st.chr_yaw)
|
||||
let take = Math.clamp(CharacterGround.current(character_st.chr_x, character_st.chr_z, fx, fz), 0.0, 1.0)
|
||||
if take > 0.0 {
|
||||
chr_speed = chr_speed * (1.0 - take)
|
||||
if take > 0.55 and chr_edge_said < 0.0 {
|
||||
chr_edge_said = 12.0
|
||||
chr_say(CHAR_CURRENT, take)
|
||||
character_st.chr_speed = character_st.chr_speed * (1.0 - take)
|
||||
if take > 0.55 and character_st.chr_edge_said < 0.0 {
|
||||
character_st.chr_edge_said = 12.0
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +1,45 @@
|
|||
# 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.
|
||||
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 {
|
||||
cal_month_days = month_days
|
||||
cal_year_months = len(season_of_month)
|
||||
cal_season = season_of_month
|
||||
export function clock_calendar(clock_st: mut ClockState, month_days: int, season_of_month: []int) -> void {
|
||||
clock_st.cal_month_days = month_days
|
||||
clock_st.cal_year_months = len(season_of_month)
|
||||
clock_st.cal_season = season_of_month
|
||||
}
|
||||
|
||||
export function clock_month_days() -> int { return cal_month_days }
|
||||
export function clock_year_months() -> int { return cal_year_months }
|
||||
export function clock_month_days(clock_st: ClockState) -> int { return clock_st.cal_month_days }
|
||||
export function clock_year_months(clock_st: ClockState) -> int { return clock_st.cal_year_months }
|
||||
|
||||
# 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_year_of(day: int) -> int { return (day - 1) / (cal_month_days * cal_year_months) + 1 }
|
||||
export function clock_day_of_month_of(day: int) -> int { return (day - 1) % cal_month_days + 1 }
|
||||
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(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(clock_st: ClockState, day: int) -> int { return (day - 1) % clock_st.cal_month_days + 1 }
|
||||
|
||||
export function clock_season_of(day: int) -> int {
|
||||
if cal_season == null { return 0 }
|
||||
return cal_season[clock_month_of(day)]
|
||||
export function clock_season_of(clock_st: ClockState, day: int) -> int {
|
||||
if clock_st.cal_season == null { return 0 }
|
||||
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_year() -> int { return clock_year_of(ck_day) }
|
||||
export function clock_day_of_month() -> int { return clock_day_of_month_of(ck_day) }
|
||||
export function clock_season() -> int { return clock_season_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(clock_st: ClockState) -> int { return clock_year_of(clock_st, clock_st.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(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
|
||||
export function clock_days_to_change_from(day: int) -> int {
|
||||
let here = clock_season_of(day)
|
||||
for d in 1 .. cal_month_days * cal_year_months {
|
||||
if clock_season_of(day + d) != here { return d }
|
||||
export function clock_days_to_change_from(clock_st: ClockState, day: int) -> int {
|
||||
let here = clock_season_of(clock_st, day)
|
||||
for d in 1 .. clock_st.cal_month_days * clock_st.cal_year_months {
|
||||
if clock_season_of(clock_st, day + d) != here { return d }
|
||||
}
|
||||
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
|
||||
export function clock_first_day_of(season: int) -> int {
|
||||
for d in 1 .. cal_month_days * cal_year_months + 1 {
|
||||
if clock_season_of(d) == season { return d }
|
||||
export function clock_first_day_of(clock_st: ClockState, season: int) -> int {
|
||||
for d in 1 .. clock_st.cal_month_days * clock_st.cal_year_months + 1 {
|
||||
if clock_season_of(clock_st, d) == season { return d }
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
# queries.ludic - what the world asks the clock
|
||||
export function clock_day() -> int { return ck_day }
|
||||
export function clock_hours() -> float { return ck_hours }
|
||||
export function clock_moon() -> float { return ck_moon }
|
||||
export function clock_scale() -> float { return ck_scale }
|
||||
export function clock_day(clock_st: ClockState) -> int { return clock_st.ck_day }
|
||||
export function clock_hours(clock_st: ClockState) -> float { return clock_st.ck_hours }
|
||||
export function clock_moon(clock_st: ClockState) -> float { return clock_st.ck_moon }
|
||||
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
|
||||
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
|
||||
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"
|
||||
export function clock_hhmm() -> string {
|
||||
let h = int(Math.floor(ck_hours))
|
||||
let m = int(Math.floor((ck_hours - float(h)) * 60.0))
|
||||
export function clock_hhmm(clock_st: ClockState) -> string {
|
||||
let h = int(Math.floor(clock_st.ck_hours))
|
||||
let m = int(Math.floor((clock_st.ck_hours - float(h)) * 60.0))
|
||||
var hs = `{h}`
|
||||
var ms = `{m}`
|
||||
if h < 10 { hs = `0{h}` }
|
||||
|
|
|
|||
|
|
@ -20,23 +20,28 @@ export property DayTurned {
|
|||
how: int = 0
|
||||
}
|
||||
|
||||
var ck_day: int = 1
|
||||
var ck_hours: float = 0.0
|
||||
var ck_moon: float = 0.0
|
||||
var ck_scale: float = 60.0 # game seconds per real second
|
||||
var ck_start_hours: float = 8.0
|
||||
var ck_night_from: float = 20.0
|
||||
var ck_night_to: float = 6.0
|
||||
var ck_moon_days: float = 29.5
|
||||
var ck_moon_lo: float = 0.0
|
||||
var ck_moon_hi: float = 1.0
|
||||
var ck_dice: Rng = null
|
||||
var ck_days: Queue<DayTurned> = null
|
||||
export state ClockState {
|
||||
cal_month_days: int = 30
|
||||
cal_year_months: int = 12
|
||||
cal_season: []int = null
|
||||
ck_day: int = 1
|
||||
ck_hours: float = 0.0
|
||||
ck_moon: float = 0.0
|
||||
ck_scale: float = 60.0 # game seconds per real second
|
||||
ck_start_hours: float = 8.0
|
||||
ck_night_from: float = 20.0
|
||||
ck_night_to: float = 6.0
|
||||
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
|
||||
export function clock_days() -> Queue<DayTurned> {
|
||||
if ck_days == null { ck_days = queue_new("clock.days") }
|
||||
return ck_days
|
||||
export function clock_days(base_st: mut BaseState, clock_st: mut ClockState) -> Queue<DayTurned> {
|
||||
if clock_st.ck_days == null { clock_st.ck_days = queue_new(base_st, "clock.days") }
|
||||
return clock_st.ck_days
|
||||
}
|
||||
|
||||
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() }
|
||||
|
||||
# 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 {
|
||||
ck_start_hours = start_hours
|
||||
ck_night_from = night_from
|
||||
ck_night_to = night_to
|
||||
export function clock_config(clock_st: mut ClockState, start_hours: float, night_from: float, night_to: float) -> void {
|
||||
clock_st.ck_start_hours = start_hours
|
||||
clock_st.ck_night_from = night_from
|
||||
clock_st.ck_night_to = night_to
|
||||
}
|
||||
|
||||
# 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 {
|
||||
ck_moon_days = days
|
||||
ck_moon_lo = lo
|
||||
ck_moon_hi = hi
|
||||
export function clock_config_moon(clock_st: mut ClockState, days: float, lo: float, hi: float) -> void {
|
||||
clock_st.ck_moon_days = days
|
||||
clock_st.ck_moon_lo = lo
|
||||
clock_st.ck_moon_hi = hi
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
# 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
|
||||
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()
|
||||
sv_put_int(v, "day", ck_day)
|
||||
sv_put_float(v, "hours", ck_hours)
|
||||
sv_put_float(v, "moon", ck_moon)
|
||||
sv_put_int(v, "day", clock_st.ck_day)
|
||||
sv_put_float(v, "hours", clock_st.ck_hours)
|
||||
sv_put_float(v, "moon", clock_st.ck_moon)
|
||||
return v
|
||||
}
|
||||
|
||||
export function clock_load(v: Val, version: int) -> void {
|
||||
ck_day = sv_int(v, "day", 1)
|
||||
clock_set_hours(sv_float(v, "hours", ck_start_hours))
|
||||
ck_moon = sv_float(v, "moon", 0.5)
|
||||
q_clear(clock_days())
|
||||
export function clock_load(base_st: mut BaseState, clock_st: mut ClockState, v: Val, version: int) -> void {
|
||||
clock_st.ck_day = sv_int(v, "day", 1)
|
||||
clock_set_hours(clock_st, sv_float(v, "hours", clock_st.ck_start_hours))
|
||||
clock_st.ck_moon = sv_float(v, "moon", 0.5)
|
||||
q_clear(base_st, clock_days(base_st, clock_st))
|
||||
}
|
||||
|
||||
export function clock_system() -> System {
|
||||
|
|
|
|||
|
|
@ -4,123 +4,125 @@ import "ludic.clock"
|
|||
import "ludic.base"
|
||||
program ClockTest {
|
||||
numbers float
|
||||
var owns: bool = true
|
||||
var rest: float = 1.0
|
||||
state ClocktestState {
|
||||
owns: bool = true
|
||||
rest: float = 1.0
|
||||
}
|
||||
|
||||
function fake_owns() -> bool { return owns }
|
||||
function fake_rest() -> float { return rest }
|
||||
function fake_owns(clocktest_st: ClocktestState) -> bool { return clocktest_st.owns }
|
||||
function fake_rest(clocktest_st: ClocktestState) -> float { return clocktest_st.rest }
|
||||
function fake_seed() -> int { return 42 }
|
||||
|
||||
bind ClockWorld { owns_world: fn fake_owns, rest_scale: fn fake_rest, seed: fn fake_seed }
|
||||
|
||||
function fresh() -> void {
|
||||
owns = true
|
||||
rest = 1.0
|
||||
clock_config(8.0, 20.0, 6.0)
|
||||
clock_config_moon(29.5, 0.2, 0.8)
|
||||
clock_set_scale(60.0)
|
||||
function fresh(base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) -> void {
|
||||
clocktest_st.owns = true
|
||||
clocktest_st.rest = 1.0
|
||||
clock_config(clock_st, 8.0, 20.0, 6.0)
|
||||
clock_config_moon(clock_st, 29.5, 0.2, 0.8)
|
||||
clock_set_scale(clock_st, 60.0)
|
||||
let seasons = new []int
|
||||
for m in 0 .. 12 { push(seasons, m / 3) }
|
||||
clock_calendar(10, seasons)
|
||||
clock_reset()
|
||||
clock_calendar(clock_st, 10, seasons)
|
||||
clock_reset(base_st, clock_st)
|
||||
}
|
||||
|
||||
function turned() -> []DayTurned { return q_drain(clock_days()) }
|
||||
function waiting() -> int { return q_len(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(base_st: mut BaseState, clock_st: mut ClockState) -> int { return q_len(clock_days(base_st, clock_st)) }
|
||||
function run_hours(h: float) -> void {
|
||||
let s = clock_system()
|
||||
s.tick(tick_new(0.016, 0, h))
|
||||
}
|
||||
|
||||
test "a trip starts on day one at the start hour, the moon inside its range" {
|
||||
fresh()
|
||||
expect_eq(clock_day(), 1)
|
||||
expect(clock_hours() == 8.0)
|
||||
expect(clock_moon() >= 0.2 and clock_moon() < 0.8)
|
||||
expect(clock_hhmm() == "08:00")
|
||||
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(base_st, clock_st, clocktest_st)
|
||||
expect_eq(clock_day(clock_st), 1)
|
||||
expect(clock_hours(clock_st) == 8.0)
|
||||
expect(clock_moon(clock_st) >= 0.2 and clock_moon(clock_st) < 0.8)
|
||||
expect(clock_hhmm(clock_st) == "08:00")
|
||||
}
|
||||
|
||||
test "the same seed gives the same moon" {
|
||||
fresh()
|
||||
let a = clock_moon()
|
||||
clock_reset()
|
||||
expect(clock_moon() == a)
|
||||
test "the same seed gives the same moon" (base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) {
|
||||
fresh(base_st, clock_st, clocktest_st)
|
||||
let a = clock_moon(clock_st)
|
||||
clock_reset(base_st, clock_st)
|
||||
expect(clock_moon(clock_st) == a)
|
||||
}
|
||||
|
||||
test "a real minute is a game hour at scale 60, and resting speeds it" {
|
||||
fresh()
|
||||
expect(clock_hours_in(60.0) == 1.0)
|
||||
rest = 4.0
|
||||
expect(clock_rate() == 240.0)
|
||||
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(base_st, clock_st, clocktest_st)
|
||||
expect(clock_hours_in(clock_st, 60.0) == 1.0)
|
||||
clocktest_st.rest = 4.0
|
||||
expect(clock_rate(clock_st) == 240.0)
|
||||
}
|
||||
|
||||
test "past midnight the day turns, once, as a fact" {
|
||||
fresh()
|
||||
test "past midnight the day turns, once, as a fact" (base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) {
|
||||
fresh(base_st, clock_st, clocktest_st)
|
||||
run_hours(15.0)
|
||||
expect_eq(waiting(), 0)
|
||||
expect_eq(waiting(base_st, clock_st), 0)
|
||||
run_hours(2.5)
|
||||
expect_eq(clock_day(), 2)
|
||||
expect(clock_hours() == 1.5)
|
||||
let t = turned()
|
||||
expect_eq(clock_day(clock_st), 2)
|
||||
expect(clock_hours(clock_st) == 1.5)
|
||||
let t = turned(base_st, clock_st)
|
||||
expect_eq(len(t), 1)
|
||||
expect_eq(t[0].day, 2)
|
||||
expect_eq(t[0].how, DAY_STAYED_UP)
|
||||
}
|
||||
|
||||
test "a machine that does not own the world does not move it" {
|
||||
fresh()
|
||||
owns = false
|
||||
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(base_st, clock_st, clocktest_st)
|
||||
clocktest_st.owns = false
|
||||
run_hours(20.0)
|
||||
expect(clock_hours() == 8.0)
|
||||
expect_eq(clock_day(), 1)
|
||||
expect(clock_hours(clock_st) == 8.0)
|
||||
expect_eq(clock_day(clock_st), 1)
|
||||
}
|
||||
|
||||
test "a night slept turns the day and moves the moon a month's share" {
|
||||
fresh()
|
||||
let m0 = clock_moon()
|
||||
clock_set_hours(6.5)
|
||||
clock_new_day()
|
||||
expect_eq(clock_day(), 2)
|
||||
expect(clock_moon() > m0)
|
||||
expect_eq(turned()[0].how, DAY_SLEPT)
|
||||
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(base_st, clock_st, clocktest_st)
|
||||
let m0 = clock_moon(clock_st)
|
||||
clock_set_hours(clock_st, 6.5)
|
||||
clock_new_day(base_st, clock_st)
|
||||
expect_eq(clock_day(clock_st), 2)
|
||||
expect(clock_moon(clock_st) > m0)
|
||||
expect_eq(turned(base_st, clock_st)[0].how, DAY_SLEPT)
|
||||
}
|
||||
|
||||
test "night is outside the day's hours, and the hour wraps" {
|
||||
fresh()
|
||||
clock_set_hours(21.0)
|
||||
expect(clock_is_night())
|
||||
clock_set_hours(12.0)
|
||||
expect(not clock_is_night())
|
||||
clock_set_hours(25.0)
|
||||
expect(clock_hours() == 1.0)
|
||||
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(base_st, clock_st, clocktest_st)
|
||||
clock_set_hours(clock_st, 21.0)
|
||||
expect(clock_is_night(clock_st))
|
||||
clock_set_hours(clock_st, 12.0)
|
||||
expect(not clock_is_night(clock_st))
|
||||
clock_set_hours(clock_st, 25.0)
|
||||
expect(clock_hours(clock_st) == 1.0)
|
||||
}
|
||||
|
||||
test "the calendar is a function of the day" {
|
||||
fresh()
|
||||
expect_eq(clock_season_of(1), 0)
|
||||
expect_eq(clock_month_of(31), 3)
|
||||
expect_eq(clock_season_of(31), 1)
|
||||
expect_eq(clock_day_of_month_of(35), 5)
|
||||
expect_eq(clock_year_of(121), 2)
|
||||
expect_eq(clock_first_day_of(2), 61)
|
||||
clock_set_day(28)
|
||||
expect_eq(clock_days_to_change(), 3)
|
||||
expect_eq(clock_season(), 0)
|
||||
test "the calendar is a function of the day" (base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) {
|
||||
fresh(base_st, clock_st, clocktest_st)
|
||||
expect_eq(clock_season_of(clock_st, 1), 0)
|
||||
expect_eq(clock_month_of(clock_st, 31), 3)
|
||||
expect_eq(clock_season_of(clock_st, 31), 1)
|
||||
expect_eq(clock_day_of_month_of(clock_st, 35), 5)
|
||||
expect_eq(clock_year_of(clock_st, 121), 2)
|
||||
expect_eq(clock_first_day_of(clock_st, 2), 61)
|
||||
clock_set_day(clock_st, 28)
|
||||
expect_eq(clock_days_to_change(clock_st), 3)
|
||||
expect_eq(clock_season(clock_st), 0)
|
||||
}
|
||||
|
||||
test "a save section reads back the same clock" {
|
||||
fresh()
|
||||
clock_set_day(17)
|
||||
clock_set_hours(13.25)
|
||||
clock_set_moon(0.375)
|
||||
test "a save section reads back the same clock" (base_st: mut BaseState, clock_st: mut ClockState, clocktest_st: mut ClocktestState) {
|
||||
fresh(base_st, clock_st, clocktest_st)
|
||||
clock_set_day(clock_st, 17)
|
||||
clock_set_hours(clock_st, 13.25)
|
||||
clock_set_moon(clock_st, 0.375)
|
||||
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)
|
||||
clock_reset()
|
||||
clock_reset(base_st, clock_st)
|
||||
let n = load_section(save_decode(text), "clock")
|
||||
clock_load(n.data, n.version)
|
||||
expect_eq(clock_day(), 17)
|
||||
expect(clock_hours() == 13.25)
|
||||
expect(clock_moon() == 0.375)
|
||||
clock_load(base_st, clock_st, n.data, n.version)
|
||||
expect_eq(clock_day(clock_st), 17)
|
||||
expect(clock_hours(clock_st) == 13.25)
|
||||
expect(clock_moon(clock_st) == 0.375)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,43 @@
|
|||
# verbs.ludic - the only way the clock changes
|
||||
export function clock_set_scale(s: float) -> void { ck_scale = s }
|
||||
export function clock_set_day(d: int) -> void { ck_day = d }
|
||||
export function clock_set_moon(m: float) -> void { ck_moon = m }
|
||||
export function clock_set_scale(clock_st: mut ClockState, s: float) -> void { clock_st.ck_scale = s }
|
||||
export function clock_set_day(clock_st: mut ClockState, d: int) -> void { clock_st.ck_day = d }
|
||||
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
|
||||
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
|
||||
if x < 0.0 { x = x + 24.0 }
|
||||
ck_hours = x
|
||||
clock_st.ck_hours = x
|
||||
}
|
||||
|
||||
function ck_turn(how: int) -> void {
|
||||
ck_day += 1
|
||||
ck_moon = (ck_moon + 1.0 / ck_moon_days) % 1.0
|
||||
function ck_turn(base_st: mut BaseState, clock_st: mut ClockState, how: int) -> void {
|
||||
clock_st.ck_day += 1
|
||||
clock_st.ck_moon = (clock_st.ck_moon + 1.0 / clock_st.ck_moon_days) % 1.0
|
||||
let d = new DayTurned
|
||||
d.day = ck_day
|
||||
d.day = clock_st.ck_day
|
||||
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
|
||||
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
|
||||
# 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 }
|
||||
ck_hours = ck_hours + gh
|
||||
while not (ck_hours < 24.0) {
|
||||
ck_hours = ck_hours - 24.0
|
||||
ck_turn(DAY_STAYED_UP)
|
||||
clock_st.ck_hours = clock_st.ck_hours + gh
|
||||
while not (clock_st.ck_hours < 24.0) {
|
||||
clock_st.ck_hours = clock_st.ck_hours - 24.0
|
||||
ck_turn(base_st, clock_st, DAY_STAYED_UP)
|
||||
}
|
||||
}
|
||||
|
||||
# a new trip: day one at the start hour, the moon somewhere in its month
|
||||
export function clock_reset() -> void {
|
||||
ck_dice = rng_new(ClockWorld.seed())
|
||||
ck_day = 1
|
||||
ck_hours = ck_start_hours
|
||||
ck_moon = rng_span(ck_dice, ck_moon_lo, ck_moon_hi) % 1.0
|
||||
q_clear(clock_days())
|
||||
export function clock_reset(base_st: mut BaseState, clock_st: mut ClockState) -> void {
|
||||
clock_st.ck_dice = rng_new(ClockWorld.seed())
|
||||
clock_st.ck_day = 1
|
||||
clock_st.ck_hours = clock_st.ck_start_hours
|
||||
clock_st.ck_moon = rng_span(clock_st.ck_dice, clock_st.ck_moon_lo, clock_st.ck_moon_hi) % 1.0
|
||||
q_clear(base_st, clock_days(base_st, clock_st))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
# 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
|
||||
# turn toward -x), a bearing is the heading less the viewer's yaw, in -pi .. pi.
|
||||
export function compass_count() -> int {
|
||||
if cmp_list == null { return 0 }
|
||||
return len(cmp_list)
|
||||
export function compass_count(compass_st: CompassState) -> int {
|
||||
if compass_st.cmp_list == null { return 0 }
|
||||
return len(compass_st.cmp_list)
|
||||
}
|
||||
|
||||
# the i-th mark, or an empty one past the end
|
||||
export function compass_at(i: int) -> CompassMark {
|
||||
if i < 0 or i >= compass_count() { return new CompassMark }
|
||||
return cmp_list[i]
|
||||
export function compass_at(compass_st: CompassState, i: int) -> CompassMark {
|
||||
if i < 0 or i >= compass_count(compass_st) { return new CompassMark }
|
||||
return compass_st.cmp_list[i]
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
export function compass_bearing(i: int) -> float {
|
||||
let m = compass_at(i)
|
||||
export function compass_bearing(compass_st: CompassState, i: int) -> float {
|
||||
let m = compass_at(compass_st, i)
|
||||
return compass_bearing_to(m.x, m.z)
|
||||
}
|
||||
|
||||
export function compass_dist(i: int) -> float {
|
||||
let m = compass_at(i)
|
||||
export function compass_dist(compass_st: CompassState, i: int) -> float {
|
||||
let m = compass_at(compass_st, i)
|
||||
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) }
|
||||
|
||||
# the objective: the first tracked mark of the frame, or -1
|
||||
export function compass_tracked() -> int {
|
||||
for i in 0 .. compass_count() { if cmp_list[i].prio >= COMPASS_TRACKED { return i } }
|
||||
export function compass_tracked(compass_st: CompassState) -> int {
|
||||
for i in 0 .. compass_count(compass_st) { if compass_st.cmp_list[i].prio >= COMPASS_TRACKED { return i } }
|
||||
return -1
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,56 +1,56 @@
|
|||
# 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.
|
||||
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
|
||||
export function compass_mark(x: float, z: float, icon: int, colour: int, tier: int, prio: int, label: string) -> void {
|
||||
if cmp_list == null { compass_begin() }
|
||||
export function compass_mark(compass_st: mut CompassState, x: float, z: float, icon: int, colour: int, tier: int, prio: int, label: string) -> void {
|
||||
if compass_st.cmp_list == null { compass_begin(compass_st) }
|
||||
let m = cmp_new(x, z, icon, colour, tier, prio, label)
|
||||
if cmp_capturing {
|
||||
push(cmp_caught, m)
|
||||
if compass_st.cmp_capturing {
|
||||
push(compass_st.cmp_caught, m)
|
||||
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
|
||||
export function compass_end() -> void {
|
||||
if cmp_list == null { compass_begin() }
|
||||
export function compass_end(compass_st: mut CompassState) -> void {
|
||||
if compass_st.cmp_list == null { compass_begin(compass_st) }
|
||||
let out = new []CompassMark
|
||||
for p in 0 .. 3 {
|
||||
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) }
|
||||
|
||||
# once a frame: ask every registered provider, in the registry's order
|
||||
export function compass_gather() -> void {
|
||||
compass_begin()
|
||||
export function compass_gather(compass_st: mut CompassState) -> void {
|
||||
compass_begin(compass_st)
|
||||
if CompassWorld.tier() <= 0 { return }
|
||||
for i in 0 .. COMPASS_FROM_COUNT {
|
||||
let f = CompassProviders[i].gather
|
||||
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
|
||||
# mark is theirs, whatever the tier, and the frame's list is left alone
|
||||
export function compass_capture_begin() -> void {
|
||||
cmp_capturing = true
|
||||
cmp_caught = new []CompassMark
|
||||
export function compass_capture_begin(compass_st: mut CompassState) -> void {
|
||||
compass_st.cmp_capturing = true
|
||||
compass_st.cmp_caught = new []CompassMark
|
||||
}
|
||||
|
||||
export function compass_capture_end() -> []CompassMark {
|
||||
cmp_capturing = false
|
||||
var got = cmp_caught
|
||||
cmp_caught = new []CompassMark
|
||||
export function compass_capture_end(compass_st: mut CompassState) -> []CompassMark {
|
||||
compass_st.cmp_capturing = false
|
||||
var got = compass_st.cmp_caught
|
||||
compass_st.cmp_caught = new []CompassMark
|
||||
if got == null { got = new []CompassMark }
|
||||
return got
|
||||
}
|
||||
|
||||
export function compass_capturing() -> bool { return cmp_capturing }
|
||||
export function compass_capturing(compass_st: CompassState) -> bool { return compass_st.cmp_capturing }
|
||||
|
|
|
|||
|
|
@ -22,15 +22,19 @@ export property CompassProvider {
|
|||
|
||||
export open registry CompassProviders of CompassProvider as COMPASS_FROM
|
||||
|
||||
var cmp_cap: int = 64
|
||||
var cmp_list: []CompassMark = null # this frame's, sorted and culled
|
||||
var cmp_capturing: bool = false
|
||||
var cmp_caught: []CompassMark = null # what providers said while someone was listening
|
||||
export state CompassState {
|
||||
cmp_cap: int = 64
|
||||
cmp_list: []CompassMark = null # this frame's, sorted and culled
|
||||
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)
|
||||
export function compass_config(cap: int) -> void {
|
||||
cmp_cap = Math.max(cap, 1)
|
||||
compass_begin()
|
||||
export function compass_config(compass_st: mut CompassState, cap: int) -> void {
|
||||
compass_st.cmp_cap = Math.max(cap, 1)
|
||||
compass_begin(compass_st)
|
||||
}
|
||||
|
||||
function cmp_new(x: float, z: float, icon: int, colour: int, tier: int, prio: int, label: string) -> CompassMark {
|
||||
|
|
|
|||
|
|
@ -1,40 +1,38 @@
|
|||
# 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
|
||||
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 {
|
||||
cmp_ranges = new []float
|
||||
for i in 0 .. len(ranges) { push(cmp_ranges, ranges[i]) }
|
||||
cmp_radar_from = from_tier
|
||||
export function compass_config_radar(compass_st: mut CompassState, ranges: []float, from_tier: int) -> void {
|
||||
compass_st.cmp_ranges = new []float
|
||||
for i in 0 .. len(ranges) { push(compass_st.cmp_ranges, ranges[i]) }
|
||||
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
|
||||
# the last range
|
||||
export function compass_radar_range() -> float {
|
||||
if cmp_ranges == null or len(cmp_ranges) == 0 { return 0.0 }
|
||||
export function compass_radar_range(compass_st: CompassState) -> float {
|
||||
if compass_st.cmp_ranges == null or len(compass_st.cmp_ranges) == 0 { return 0.0 }
|
||||
let t = CompassWorld.tier()
|
||||
if t < 0 { return 0.0 }
|
||||
if t >= len(cmp_ranges) { return cmp_ranges[len(cmp_ranges) - 1] }
|
||||
return cmp_ranges[t]
|
||||
if t >= len(compass_st.cmp_ranges) { return compass_st.cmp_ranges[len(compass_st.cmp_ranges) - 1] }
|
||||
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
|
||||
export function compass_radar_has(i: int) -> bool {
|
||||
if i < 0 or i >= compass_count() or not compass_radar_on() { return false }
|
||||
let m = cmp_list[i]
|
||||
if m.tier < cmp_radar_from and m.prio < COMPASS_TRACKED { return false }
|
||||
return compass_dist(i) <= compass_radar_range()
|
||||
export function compass_radar_has(compass_st: CompassState, i: int) -> bool {
|
||||
if i < 0 or i >= compass_count(compass_st) or not compass_radar_on(compass_st) { return false }
|
||||
let m = compass_st.cmp_list[i]
|
||||
if m.tier < compass_st.cmp_radar_from and m.prio < COMPASS_TRACKED { return false }
|
||||
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
|
||||
export function compass_radar_x(i: int) -> float { return -Math.sin(compass_bearing(i)) * cmp_radar_frac(i) }
|
||||
export function compass_radar_y(i: int) -> float { return -Math.cos(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(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 {
|
||||
let r = compass_radar_range()
|
||||
function cmp_radar_frac(compass_st: CompassState, i: int) -> float {
|
||||
let r = compass_radar_range(compass_st)
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,120 +6,122 @@ import "ludic.base"
|
|||
program CompassTest {
|
||||
numbers float
|
||||
|
||||
var vx: float = 0.0
|
||||
var vz: float = 0.0
|
||||
var yaw: float = 0.0
|
||||
var tier: int = 4
|
||||
var objective: bool = true
|
||||
state CompasstestState {
|
||||
vx: float = 0.0
|
||||
vz: float = 0.0
|
||||
yaw: float = 0.0
|
||||
tier: int = 4
|
||||
objective: bool = true
|
||||
}
|
||||
|
||||
function fk_x() -> float { return vx }
|
||||
function fk_z() -> float { return vz }
|
||||
function fk_yaw() -> float { return yaw }
|
||||
function fk_tier() -> int { return tier }
|
||||
function fk_x(compasstest_st: CompasstestState) -> float { return compasstest_st.vx }
|
||||
function fk_z(compasstest_st: CompasstestState) -> float { return compasstest_st.vz }
|
||||
function fk_yaw(compasstest_st: CompasstestState) -> float { return compasstest_st.yaw }
|
||||
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 }
|
||||
|
||||
function places() -> void {
|
||||
compass_mark(0.0, -100.0, 1, 10, 1, COMPASS_PLAIN, "camp")
|
||||
compass_mark(-50.0, 0.0, 2, 11, 2, COMPASS_PLAIN, "sign")
|
||||
compass_mark(0.0, 300.0, 3, 12, 4, COMPASS_PLAIN, "legend")
|
||||
compass_mark(10.0, 10.0, 4, 13, 1, COMPASS_NOTE, "my mark")
|
||||
function places(compass_st: mut CompassState) -> void {
|
||||
compass_mark(compass_st, 0.0, -100.0, 1, 10, 1, COMPASS_PLAIN, "camp")
|
||||
compass_mark(compass_st, -50.0, 0.0, 2, 11, 2, COMPASS_PLAIN, "sign")
|
||||
compass_mark(compass_st, 0.0, 300.0, 3, 12, 4, COMPASS_PLAIN, "legend")
|
||||
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 goal { gather: fn goal }
|
||||
|
||||
function fresh() -> void {
|
||||
compass_config(64)
|
||||
compass_config_radar([0.0, 0.0, 0.0, 120.0, 260.0], 2)
|
||||
vx = 0.0
|
||||
vz = 0.0
|
||||
yaw = 0.0
|
||||
tier = 4
|
||||
objective = true
|
||||
function fresh(compass_st: mut CompassState, compasstest_st: mut CompasstestState) -> void {
|
||||
compass_config(compass_st, 64)
|
||||
compass_config_radar(compass_st, [0.0, 0.0, 0.0, 120.0, 260.0], 2)
|
||||
compasstest_st.vx = 0.0
|
||||
compasstest_st.vz = 0.0
|
||||
compasstest_st.yaw = 0.0
|
||||
compasstest_st.tier = 4
|
||||
compasstest_st.objective = true
|
||||
}
|
||||
function labels() -> string {
|
||||
function labels(compass_st: CompassState) -> string {
|
||||
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
|
||||
}
|
||||
|
||||
test "every provider is asked, the tracked mark first, then the player's, then the rest in order" {
|
||||
fresh()
|
||||
compass_gather()
|
||||
expect_eq(compass_count(), 5)
|
||||
expect(labels() == "the dock;my mark;camp;sign;legend;")
|
||||
expect_eq(compass_tracked(), 0)
|
||||
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(compass_st, compasstest_st)
|
||||
compass_gather(compass_st)
|
||||
expect_eq(compass_count(compass_st), 5)
|
||||
expect(labels(compass_st) == "the dock;my mark;camp;sign;legend;")
|
||||
expect_eq(compass_tracked(compass_st), 0)
|
||||
}
|
||||
|
||||
test "a mark shows from its tier up, and without a compass nothing is gathered" {
|
||||
fresh()
|
||||
tier = 1
|
||||
compass_gather()
|
||||
expect(labels() == "my mark;camp;")
|
||||
expect_eq(compass_tracked(), -1)
|
||||
tier = 0
|
||||
compass_gather()
|
||||
expect_eq(compass_count(), 0)
|
||||
test "a mark shows from its tier up, and without a compass nothing is gathered" (compass_st: mut CompassState, compasstest_st: mut CompasstestState) {
|
||||
fresh(compass_st, compasstest_st)
|
||||
compasstest_st.tier = 1
|
||||
compass_gather(compass_st)
|
||||
expect(labels(compass_st) == "my mark;camp;")
|
||||
expect_eq(compass_tracked(compass_st), -1)
|
||||
compasstest_st.tier = 0
|
||||
compass_gather(compass_st)
|
||||
expect_eq(compass_count(compass_st), 0)
|
||||
}
|
||||
|
||||
test "past the cap the lowest priorities go" {
|
||||
fresh()
|
||||
compass_config(2)
|
||||
compass_gather()
|
||||
expect(labels() == "the dock;my mark;")
|
||||
test "past the cap the lowest priorities go" (compass_st: mut CompassState, compasstest_st: mut CompasstestState) {
|
||||
fresh(compass_st, compasstest_st)
|
||||
compass_config(compass_st, 2)
|
||||
compass_gather(compass_st)
|
||||
expect(labels(compass_st) == "the dock;my mark;")
|
||||
}
|
||||
|
||||
test "a bearing is from the viewer's facing, a distance from where it stands" {
|
||||
fresh()
|
||||
compass_gather()
|
||||
expect_near(compass_bearing(2), 0.0, 0.001)
|
||||
expect_near(compass_dist(2), 100.0, 0.01)
|
||||
expect_near(compass_bearing(3), 1.5708, 0.001)
|
||||
expect_near(compass_bearing(0), -1.5708, 0.001)
|
||||
yaw = 1.5708
|
||||
expect_near(compass_bearing(3), 0.0, 0.001)
|
||||
expect_near(compass_bearing(2), -1.5708, 0.001)
|
||||
vx = -50.0
|
||||
vz = 100.0
|
||||
expect_near(compass_dist(3), 100.0, 0.01)
|
||||
test "a bearing is from the viewer's facing, a distance from where it stands" (compass_st: mut CompassState, compasstest_st: mut CompasstestState) {
|
||||
fresh(compass_st, compasstest_st)
|
||||
compass_gather(compass_st)
|
||||
expect_near(compass_bearing(compass_st, 2), 0.0, 0.001)
|
||||
expect_near(compass_dist(compass_st, 2), 100.0, 0.01)
|
||||
expect_near(compass_bearing(compass_st, 3), 1.5708, 0.001)
|
||||
expect_near(compass_bearing(compass_st, 0), -1.5708, 0.001)
|
||||
compasstest_st.yaw = 1.5708
|
||||
expect_near(compass_bearing(compass_st, 3), 0.0, 0.001)
|
||||
expect_near(compass_bearing(compass_st, 2), -1.5708, 0.001)
|
||||
compasstest_st.vx = -50.0
|
||||
compasstest_st.vz = 100.0
|
||||
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_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" {
|
||||
fresh()
|
||||
compass_gather()
|
||||
tier = 1
|
||||
compass_capture_begin()
|
||||
expect(compass_capturing())
|
||||
goal()
|
||||
places()
|
||||
let got = compass_capture_end()
|
||||
expect(not compass_capturing())
|
||||
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(compass_st, compasstest_st)
|
||||
compass_gather(compass_st)
|
||||
compasstest_st.tier = 1
|
||||
compass_capture_begin(compass_st)
|
||||
expect(compass_capturing(compass_st))
|
||||
goal(compass_st, compasstest_st)
|
||||
places(compass_st)
|
||||
let got = compass_capture_end(compass_st)
|
||||
expect(not compass_capturing(compass_st))
|
||||
expect_eq(len(got), 5)
|
||||
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" {
|
||||
fresh()
|
||||
tier = 2
|
||||
compass_gather()
|
||||
expect(not compass_radar_on())
|
||||
expect(not compass_radar_has(0))
|
||||
tier = 3
|
||||
compass_gather()
|
||||
expect(compass_radar_on())
|
||||
expect_near(compass_radar_range(), 120.0, 0.01)
|
||||
expect(compass_radar_has(0))
|
||||
expect(not compass_radar_has(1))
|
||||
expect(not compass_radar_has(2))
|
||||
expect(compass_radar_has(3))
|
||||
expect_near(compass_radar_x(3), -50.0 / 120.0, 0.001)
|
||||
expect_near(compass_radar_y(3), 0.0, 0.001)
|
||||
expect_near(compass_radar_x(0), 80.0 / 120.0, 0.001)
|
||||
tier = 9
|
||||
expect_near(compass_radar_range(), 260.0, 0.01)
|
||||
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(compass_st, compasstest_st)
|
||||
compasstest_st.tier = 2
|
||||
compass_gather(compass_st)
|
||||
expect(not compass_radar_on(compass_st))
|
||||
expect(not compass_radar_has(compass_st, 0))
|
||||
compasstest_st.tier = 3
|
||||
compass_gather(compass_st)
|
||||
expect(compass_radar_on(compass_st))
|
||||
expect_near(compass_radar_range(compass_st), 120.0, 0.01)
|
||||
expect(compass_radar_has(compass_st, 0))
|
||||
expect(not compass_radar_has(compass_st, 1))
|
||||
expect(not compass_radar_has(compass_st, 2))
|
||||
expect(compass_radar_has(compass_st, 3))
|
||||
expect_near(compass_radar_x(compass_st, 3), -50.0 / 120.0, 0.001)
|
||||
expect_near(compass_radar_y(compass_st, 3), 0.0, 0.001)
|
||||
expect_near(compass_radar_x(compass_st, 0), 80.0 / 120.0, 0.001)
|
||||
compasstest_st.tier = 9
|
||||
expect_near(compass_radar_range(compass_st), 260.0, 0.01)
|
||||
}
|
||||
|
||||
test "the eight points go round from the heading 0 toward -x" {
|
||||
|
|
|
|||
|
|
@ -1,42 +1,40 @@
|
|||
# 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
|
||||
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 }
|
||||
|
||||
# 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
|
||||
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 }
|
||||
craft_hold_i = i
|
||||
craft_hold_t = 0.0
|
||||
crafting_st.craft_hold_i = i
|
||||
crafting_st.craft_hold_t = 0.0
|
||||
return true
|
||||
}
|
||||
|
||||
export function craft_hold_stop() -> void {
|
||||
craft_hold_i = -1
|
||||
craft_hold_t = 0.0
|
||||
export function craft_hold_stop(crafting_st: mut CraftingState) -> void {
|
||||
crafting_st.craft_hold_i = -1
|
||||
crafting_st.craft_hold_t = 0.0
|
||||
}
|
||||
|
||||
# how far through the hold on `i` is, 0 to 1
|
||||
export function craft_hold_frac(i: int) -> float {
|
||||
if 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)
|
||||
export function craft_hold_frac(crafting_st: CraftingState, i: int) -> float {
|
||||
if crafting_st.craft_hold_i != i or not craft_holds(i) { return 0.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
|
||||
export function craft_hold_run(dt: float) -> bool {
|
||||
if craft_hold_i < 0 { return false }
|
||||
if not craft_can(craft_hold_i) {
|
||||
craft_hold_stop()
|
||||
export function craft_hold_run(base_st: mut BaseState, crafting_st: mut CraftingState, dt: float) -> bool {
|
||||
if crafting_st.craft_hold_i < 0 { return false }
|
||||
if not craft_can(crafting_st.craft_hold_i) {
|
||||
craft_hold_stop(crafting_st)
|
||||
return false
|
||||
}
|
||||
craft_hold_t = craft_hold_t + dt
|
||||
if craft_hold_t <= CraftRecipes[craft_hold_i].hold { return false }
|
||||
let i = craft_hold_i
|
||||
craft_hold_stop()
|
||||
return craft_make(i)
|
||||
crafting_st.craft_hold_t = crafting_st.craft_hold_t + dt
|
||||
if crafting_st.craft_hold_t <= CraftRecipes[crafting_st.craft_hold_i].hold { return false }
|
||||
let i = crafting_st.craft_hold_i
|
||||
craft_hold_stop(crafting_st)
|
||||
return craft_make(base_st, crafting_st, i)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# 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)
|
||||
export function craft_reset() -> void {
|
||||
craft_hold_stop()
|
||||
q_clear(craft_facts())
|
||||
export function craft_reset(base_st: mut BaseState, crafting_st: mut CraftingState) -> void {
|
||||
craft_hold_stop(crafting_st)
|
||||
q_clear(base_st, craft_facts(base_st, crafting_st))
|
||||
}
|
||||
|
||||
export function craft_system() -> System {
|
||||
|
|
|
|||
|
|
@ -21,49 +21,51 @@ program CraftingTest {
|
|||
def CraftRecipes chair { out: TORCH, ins: [WOOD], ns: [3], station: BENCH }
|
||||
def CraftRecipes secret { out: TORCH, ins: [WOOD], ns: [1], learned: false }
|
||||
|
||||
var pack: []int = null
|
||||
var bench_near: bool = true
|
||||
var pot_takes: bool = true
|
||||
var pot_got: int = -1
|
||||
state CraftingtestState {
|
||||
pack: []int = null
|
||||
bench_near: bool = true
|
||||
pot_takes: bool = true
|
||||
pot_got: int = -1
|
||||
}
|
||||
|
||||
function count(it: int) -> int { return pack[it] }
|
||||
function take(it: int, n: int) -> bool {
|
||||
if pack[it] < n { return false }
|
||||
pack[it] = pack[it] - n
|
||||
function count(craftingtest_st: CraftingtestState, it: int) -> int { return craftingtest_st.pack[it] }
|
||||
function take(craftingtest_st: mut CraftingtestState, it: int, n: int) -> bool {
|
||||
if craftingtest_st.pack[it] < n { return false }
|
||||
craftingtest_st.pack[it] = craftingtest_st.pack[it] - n
|
||||
return true
|
||||
}
|
||||
function add(it: int, n: int) -> int {
|
||||
pack[it] = pack[it] + n
|
||||
function add(craftingtest_st: mut CraftingtestState, it: int, n: int) -> int {
|
||||
craftingtest_st.pack[it] = craftingtest_st.pack[it] + n
|
||||
return n
|
||||
}
|
||||
function room(it: int) -> int {
|
||||
if it == TORCH { return 3 - pack[it] }
|
||||
function room(craftingtest_st: CraftingtestState, it: int) -> int {
|
||||
if it == TORCH { return 3 - craftingtest_st.pack[it] }
|
||||
return 99
|
||||
}
|
||||
function leaves(it: int) -> int {
|
||||
if it == WATER { return BOTTLE }
|
||||
return -1
|
||||
}
|
||||
function ready(st: int) -> bool {
|
||||
if st == BENCH { return bench_near }
|
||||
function ready(craftingtest_st: CraftingtestState, st: int) -> bool {
|
||||
if st == BENCH { return craftingtest_st.bench_near }
|
||||
return true
|
||||
}
|
||||
function start(st: int, recipe: int) -> bool {
|
||||
if not pot_takes { return false }
|
||||
pot_got = recipe
|
||||
function start(craftingtest_st: mut CraftingtestState, st: int, recipe: int) -> bool {
|
||||
if not craftingtest_st.pot_takes { return false }
|
||||
craftingtest_st.pot_got = recipe
|
||||
return true
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
function fresh() -> void {
|
||||
pack = new []int
|
||||
for i in 0 .. 8 { push(pack, 0) }
|
||||
craft_reset()
|
||||
function fresh(base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) -> void {
|
||||
craftingtest_st.pack = new []int
|
||||
for i in 0 .. 8 { push(craftingtest_st.pack, 0) }
|
||||
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" {
|
||||
expect_eq(CRAFT_COUNT, 5)
|
||||
|
|
@ -74,105 +76,105 @@ program CraftingTest {
|
|||
expect_eq(craft_find(99), -1)
|
||||
}
|
||||
|
||||
test "made at once from the makings, and said" {
|
||||
fresh()
|
||||
test "made at once from the makings, and said" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) {
|
||||
fresh(base_st, crafting_st, craftingtest_st)
|
||||
expect(not craft_can(CRAFT_TORCH))
|
||||
pack[WOOD] = 2
|
||||
pack[CLOTH] = 1
|
||||
expect(craft_make(CRAFT_TORCH))
|
||||
expect_eq(pack[WOOD], 1)
|
||||
expect_eq(pack[CLOTH], 0)
|
||||
expect_eq(pack[TORCH], 1)
|
||||
let fs = facts()
|
||||
craftingtest_st.pack[WOOD] = 2
|
||||
craftingtest_st.pack[CLOTH] = 1
|
||||
expect(craft_make(base_st, crafting_st, CRAFT_TORCH))
|
||||
expect_eq(craftingtest_st.pack[WOOD], 1)
|
||||
expect_eq(craftingtest_st.pack[CLOTH], 0)
|
||||
expect_eq(craftingtest_st.pack[TORCH], 1)
|
||||
let fs = facts(base_st, crafting_st)
|
||||
expect_eq(len(fs), 1)
|
||||
expect_eq(fs[0].item, TORCH)
|
||||
expect(not fs[0].queued)
|
||||
}
|
||||
|
||||
test "nothing is taken when it cannot be made" {
|
||||
fresh()
|
||||
pack[WOOD] = 1
|
||||
expect(not craft_make(CRAFT_TORCH))
|
||||
expect_eq(pack[WOOD], 1)
|
||||
expect_eq(len(facts()), 0)
|
||||
test "nothing is taken when it cannot be made" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) {
|
||||
fresh(base_st, crafting_st, craftingtest_st)
|
||||
craftingtest_st.pack[WOOD] = 1
|
||||
expect(not craft_make(base_st, crafting_st, CRAFT_TORCH))
|
||||
expect_eq(craftingtest_st.pack[WOOD], 1)
|
||||
expect_eq(len(facts(base_st, crafting_st)), 0)
|
||||
}
|
||||
|
||||
test "the station decides, and so does the room for what it makes" {
|
||||
fresh()
|
||||
pack[WOOD] = 9
|
||||
bench_near = false
|
||||
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(base_st, crafting_st, craftingtest_st)
|
||||
craftingtest_st.pack[WOOD] = 9
|
||||
craftingtest_st.bench_near = false
|
||||
expect(craft_has(CRAFT_CHAIR))
|
||||
expect(not craft_can(CRAFT_CHAIR))
|
||||
bench_near = true
|
||||
craftingtest_st.bench_near = true
|
||||
expect(craft_can(CRAFT_CHAIR))
|
||||
pack[TORCH] = 3
|
||||
craftingtest_st.pack[TORCH] = 3
|
||||
expect(not craft_can(CRAFT_CHAIR))
|
||||
}
|
||||
|
||||
test "an unlearned recipe is never made" {
|
||||
fresh()
|
||||
pack[WOOD] = 5
|
||||
test "an unlearned recipe is never made" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) {
|
||||
fresh(base_st, crafting_st, craftingtest_st)
|
||||
craftingtest_st.pack[WOOD] = 5
|
||||
expect(not craft_can(CRAFT_SECRET))
|
||||
}
|
||||
|
||||
test "a timed recipe goes into its station, and water leaves its bottle" {
|
||||
fresh()
|
||||
pack[HERB] = 2
|
||||
pack[WATER] = 1
|
||||
expect(craft_make(CRAFT_TEA))
|
||||
expect_eq(pot_got, CRAFT_TEA)
|
||||
expect_eq(pack[TEA], 0)
|
||||
expect_eq(pack[WATER], 0)
|
||||
expect_eq(pack[BOTTLE], 1)
|
||||
let fs = facts()
|
||||
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(base_st, crafting_st, craftingtest_st)
|
||||
craftingtest_st.pack[HERB] = 2
|
||||
craftingtest_st.pack[WATER] = 1
|
||||
expect(craft_make(base_st, crafting_st, CRAFT_TEA))
|
||||
expect_eq(craftingtest_st.pot_got, CRAFT_TEA)
|
||||
expect_eq(craftingtest_st.pack[TEA], 0)
|
||||
expect_eq(craftingtest_st.pack[WATER], 0)
|
||||
expect_eq(craftingtest_st.pack[BOTTLE], 1)
|
||||
let fs = facts(base_st, crafting_st)
|
||||
expect(fs[0].queued)
|
||||
expect_eq(fs[0].station, POT)
|
||||
}
|
||||
|
||||
test "a station that says no gives everything back" {
|
||||
fresh()
|
||||
pot_takes = false
|
||||
pack[HERB] = 2
|
||||
pack[WATER] = 1
|
||||
expect(not craft_make(CRAFT_TEA))
|
||||
expect_eq(pack[HERB], 2)
|
||||
expect_eq(pack[WATER], 1)
|
||||
expect_eq(pack[BOTTLE], 0)
|
||||
expect_eq(len(facts()), 0)
|
||||
test "a station that says no gives everything back" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) {
|
||||
fresh(base_st, crafting_st, craftingtest_st)
|
||||
craftingtest_st.pot_takes = false
|
||||
craftingtest_st.pack[HERB] = 2
|
||||
craftingtest_st.pack[WATER] = 1
|
||||
expect(not craft_make(base_st, crafting_st, CRAFT_TEA))
|
||||
expect_eq(craftingtest_st.pack[HERB], 2)
|
||||
expect_eq(craftingtest_st.pack[WATER], 1)
|
||||
expect_eq(craftingtest_st.pack[BOTTLE], 0)
|
||||
expect_eq(len(facts(base_st, crafting_st)), 0)
|
||||
}
|
||||
|
||||
test "a hold makes it after its seconds, and not before" {
|
||||
fresh()
|
||||
pack[HERB] = 4
|
||||
test "a hold makes it after its seconds, and not before" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) {
|
||||
fresh(base_st, crafting_st, craftingtest_st)
|
||||
craftingtest_st.pack[HERB] = 4
|
||||
expect(craft_holds(CRAFT_ROPE))
|
||||
expect(not craft_holds(CRAFT_TORCH))
|
||||
expect(craft_hold_start(CRAFT_ROPE))
|
||||
expect(not craft_hold_run(1.0))
|
||||
expect(craft_hold_frac(CRAFT_ROPE) == 0.5)
|
||||
expect(not craft_hold_run(0.9))
|
||||
expect(craft_hold_run(0.2))
|
||||
expect_eq(pack[ROPE], 2)
|
||||
expect_eq(craft_holding(), -1)
|
||||
expect(craft_hold_start(crafting_st, CRAFT_ROPE))
|
||||
expect(not craft_hold_run(base_st, crafting_st, 1.0))
|
||||
expect(craft_hold_frac(crafting_st, CRAFT_ROPE) == 0.5)
|
||||
expect(not craft_hold_run(base_st, crafting_st, 0.9))
|
||||
expect(craft_hold_run(base_st, crafting_st, 0.2))
|
||||
expect_eq(craftingtest_st.pack[ROPE], 2)
|
||||
expect_eq(craft_holding(crafting_st), -1)
|
||||
}
|
||||
|
||||
test "letting go wastes nothing, and losing the makings stops the hold" {
|
||||
fresh()
|
||||
pack[HERB] = 4
|
||||
craft_hold_start(CRAFT_ROPE)
|
||||
craft_hold_run(1.5)
|
||||
craft_hold_stop()
|
||||
expect_eq(pack[HERB], 4)
|
||||
craft_hold_start(CRAFT_ROPE)
|
||||
pack[HERB] = 3
|
||||
expect(not craft_hold_run(5.0))
|
||||
expect_eq(craft_holding(), -1)
|
||||
expect_eq(pack[ROPE], 0)
|
||||
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(base_st, crafting_st, craftingtest_st)
|
||||
craftingtest_st.pack[HERB] = 4
|
||||
craft_hold_start(crafting_st, CRAFT_ROPE)
|
||||
craft_hold_run(base_st, crafting_st, 1.5)
|
||||
craft_hold_stop(crafting_st)
|
||||
expect_eq(craftingtest_st.pack[HERB], 4)
|
||||
craft_hold_start(crafting_st, CRAFT_ROPE)
|
||||
craftingtest_st.pack[HERB] = 3
|
||||
expect(not craft_hold_run(base_st, crafting_st, 5.0))
|
||||
expect_eq(craft_holding(crafting_st), -1)
|
||||
expect_eq(craftingtest_st.pack[ROPE], 0)
|
||||
}
|
||||
|
||||
test "a click recipe cannot be held" {
|
||||
fresh()
|
||||
pack[WOOD] = 1
|
||||
pack[CLOTH] = 1
|
||||
expect(not craft_hold_start(CRAFT_TORCH))
|
||||
test "a click recipe cannot be held" (base_st: mut BaseState, crafting_st: mut CraftingState, craftingtest_st: mut CraftingtestState) {
|
||||
fresh(base_st, crafting_st, craftingtest_st)
|
||||
craftingtest_st.pack[WOOD] = 1
|
||||
craftingtest_st.pack[CLOTH] = 1
|
||||
expect(not craft_hold_start(crafting_st, CRAFT_TORCH))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
# 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> {
|
||||
if craft_fact_q == null { craft_fact_q = queue_new("crafting.crafted") }
|
||||
return craft_fact_q
|
||||
export function craft_facts(base_st: mut BaseState, crafting_st: mut CraftingState) -> Queue<Crafted> {
|
||||
if crafting_st.craft_fact_q == null { crafting_st.craft_fact_q = queue_new(base_st, "crafting.crafted") }
|
||||
return crafting_st.craft_fact_q
|
||||
}
|
||||
|
||||
# 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 c = new Crafted
|
||||
c.recipe = i
|
||||
|
|
@ -54,12 +58,12 @@ function craft_fact_push(i: int, queued: bool) -> void {
|
|||
c.n = r.n
|
||||
c.station = r.station
|
||||
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),
|
||||
# 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 }
|
||||
let r = CraftRecipes[i]
|
||||
craft_take(i)
|
||||
|
|
@ -68,10 +72,10 @@ export function craft_make(i: int) -> bool {
|
|||
craft_refund(i)
|
||||
return false
|
||||
}
|
||||
craft_fact_push(i, true)
|
||||
craft_fact_push(base_st, crafting_st, i, true)
|
||||
return true
|
||||
}
|
||||
CraftPack.add(r.out, r.n)
|
||||
craft_fact_push(i, false)
|
||||
craft_fact_push(base_st, crafting_st, i, false)
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,32 +2,32 @@
|
|||
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)
|
||||
export function effects_sum(kind: int) -> float {
|
||||
let all = ef_all()
|
||||
export function effects_sum(effects_st: mut EffectsState, kind: int) -> float {
|
||||
let all = ef_all(effects_st)
|
||||
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 } }
|
||||
return t
|
||||
}
|
||||
|
||||
export function effects_has(kind: int) -> bool {
|
||||
let all = ef_all()
|
||||
export function effects_has(effects_st: mut EffectsState, kind: int) -> bool {
|
||||
let all = ef_all(effects_st)
|
||||
for i in 0 .. len(all) { if all[i].kind == kind and ef_live(all[i]) { return true } }
|
||||
return false
|
||||
}
|
||||
|
||||
export function effects_bonus_count() -> int {
|
||||
let all = ef_all()
|
||||
export function effects_bonus_count(effects_st: mut EffectsState) -> int {
|
||||
let all = ef_all(effects_st)
|
||||
var n = 0
|
||||
for i in 0 .. len(all) { if all[i].bonus { n += 1 } }
|
||||
return n
|
||||
}
|
||||
|
||||
# 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
|
||||
export function effects_at(i: int) -> Effect {
|
||||
let src = ef_all()[i]
|
||||
export function effects_at(effects_st: mut EffectsState, i: int) -> Effect {
|
||||
let src = ef_all(effects_st)[i]
|
||||
let e = new Effect
|
||||
e.kind = src.kind
|
||||
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
|
||||
export function effects_live() -> []int {
|
||||
export function effects_live(effects_st: mut EffectsState) -> []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) } }
|
||||
return out
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,27 +20,29 @@ export property EffectEnded {
|
|||
bonus: bool = false
|
||||
}
|
||||
|
||||
var ef_list: []Effect = null
|
||||
var ef_max: int = 8
|
||||
var ef_bonus_max: int = 3
|
||||
var ef_ended: Queue<EffectEnded> = null
|
||||
export state EffectsState {
|
||||
ef_list: []Effect = null
|
||||
ef_max: int = 8
|
||||
ef_bonus_max: int = 3
|
||||
ef_ended: Queue<EffectEnded> = null
|
||||
}
|
||||
|
||||
function ef_all() -> []Effect {
|
||||
if ef_list == null { ef_list = new []Effect }
|
||||
return ef_list
|
||||
function ef_all(effects_st: mut EffectsState) -> []Effect {
|
||||
if effects_st.ef_list == null { effects_st.ef_list = new []Effect }
|
||||
return effects_st.ef_list
|
||||
}
|
||||
|
||||
# the facts: what ended and why, oldest first; the game drains it
|
||||
export function effects_ended() -> Queue<EffectEnded> {
|
||||
if ef_ended == null { ef_ended = queue_new("effects.ended") }
|
||||
return ef_ended
|
||||
export function effects_ended(base_st: mut BaseState, effects_st: mut EffectsState) -> Queue<EffectEnded> {
|
||||
if effects_st.ef_ended == null { effects_st.ef_ended = queue_new(base_st, "effects.ended") }
|
||||
return effects_st.ef_ended
|
||||
}
|
||||
|
||||
# how many may run at once, and how many of those may be bonuses
|
||||
export function effects_config(max: int, bonus_max: int) -> void {
|
||||
ef_max = max
|
||||
ef_bonus_max = bonus_max
|
||||
export function effects_config(effects_st: mut EffectsState, max: int, bonus_max: int) -> void {
|
||||
effects_st.ef_max = max
|
||||
effects_st.ef_bonus_max = bonus_max
|
||||
}
|
||||
|
||||
export function effects_capacity() -> int { return ef_max }
|
||||
export function effects_bonus_capacity() -> int { return ef_bonus_max }
|
||||
export function effects_capacity(effects_st: EffectsState) -> int { return effects_st.ef_max }
|
||||
export function effects_bonus_capacity(effects_st: EffectsState) -> int { return effects_st.ef_bonus_max }
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
# system.ludic - the effects as a system: reset, a tick in game hours, and its own save section
|
||||
export function effects_reset() -> void {
|
||||
effects_clear()
|
||||
q_clear(effects_ended())
|
||||
export function effects_reset(base_st: mut BaseState, effects_st: mut EffectsState) -> void {
|
||||
effects_clear(effects_st)
|
||||
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 all = ef_all()
|
||||
let all = ef_all(effects_st)
|
||||
for i in 0 .. len(all) {
|
||||
let o = Value.object()
|
||||
sv_put_int(o, "k", all[i].kind)
|
||||
|
|
@ -24,12 +24,12 @@ export function effects_save() -> Val {
|
|||
return v
|
||||
}
|
||||
|
||||
export function effects_load(v: Val, version: int) -> void {
|
||||
effects_clear()
|
||||
export function effects_load(effects_st: mut EffectsState, v: Val, version: int) -> void {
|
||||
effects_clear(effects_st)
|
||||
if Value.has(v, "list") == 0 { return }
|
||||
let l = Value.get(v, "list")
|
||||
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 e = new Effect
|
||||
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.bonus = sv_bool(o, "b", false)
|
||||
e.label = sv_str(o, "l", "")
|
||||
push(ef_all(), e)
|
||||
push(ef_all(effects_st), e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,120 +8,120 @@ program EffectsTest {
|
|||
const WARMTH: int = 1
|
||||
const CARRY: int = 2
|
||||
|
||||
function fresh() -> void {
|
||||
effects_config(8, 3)
|
||||
effects_reset()
|
||||
function fresh(base_st: mut BaseState, effects_st: mut EffectsState) -> void {
|
||||
effects_config(effects_st, 8, 3)
|
||||
effects_reset(base_st, effects_st)
|
||||
}
|
||||
|
||||
function ended() -> []EffectEnded { return q_drain(effects_ended()) }
|
||||
function waiting() -> int { return q_len(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(base_st: mut BaseState, effects_st: mut EffectsState) -> int { return q_len(effects_ended(base_st, effects_st)) }
|
||||
|
||||
function tick_minutes(m: float) -> void {
|
||||
let s = effects_system()
|
||||
s.tick(tick_new(0.016, 0, m / 60.0))
|
||||
}
|
||||
|
||||
test "the sum is per kind, and a kind with nothing running is zero" {
|
||||
fresh()
|
||||
effects_add(LEGS, 8.0, 20.0, false, "Fizz")
|
||||
effects_add(LEGS, 6.0, 45.0, false, "Trail mix")
|
||||
effects_add(WARMTH, 12.0, 50.0, false, "Stew")
|
||||
expect(effects_sum(LEGS) == 14.0)
|
||||
expect(effects_sum(WARMTH) == 12.0)
|
||||
expect(effects_sum(CARRY) == 0.0)
|
||||
expect(effects_has(WARMTH))
|
||||
expect(not effects_has(CARRY))
|
||||
test "the sum is per kind, and a kind with nothing running is zero" (base_st: mut BaseState, effects_st: mut EffectsState) {
|
||||
fresh(base_st, effects_st)
|
||||
effects_add(base_st, effects_st, LEGS, 8.0, 20.0, false, "Fizz")
|
||||
effects_add(base_st, effects_st, LEGS, 6.0, 45.0, false, "Trail mix")
|
||||
effects_add(base_st, effects_st, WARMTH, 12.0, 50.0, false, "Stew")
|
||||
expect(effects_sum(effects_st, LEGS) == 14.0)
|
||||
expect(effects_sum(effects_st, WARMTH) == 12.0)
|
||||
expect(effects_sum(effects_st, CARRY) == 0.0)
|
||||
expect(effects_has(effects_st, WARMTH))
|
||||
expect(not effects_has(effects_st, CARRY))
|
||||
}
|
||||
|
||||
test "the same kind from the same label replaces itself rather than stacking" {
|
||||
fresh()
|
||||
effects_add(LEGS, 8.0, 20.0, false, "Fizz")
|
||||
effects_add(LEGS, 8.0, 20.0, false, "Fizz")
|
||||
expect_eq(effects_count(), 1)
|
||||
expect(effects_sum(LEGS) == 8.0)
|
||||
test "the same kind from the same label replaces itself rather than stacking" (base_st: mut BaseState, effects_st: mut EffectsState) {
|
||||
fresh(base_st, effects_st)
|
||||
effects_add(base_st, effects_st, LEGS, 8.0, 20.0, false, "Fizz")
|
||||
effects_add(base_st, effects_st, LEGS, 8.0, 20.0, false, "Fizz")
|
||||
expect_eq(effects_count(effects_st), 1)
|
||||
expect(effects_sum(effects_st, LEGS) == 8.0)
|
||||
}
|
||||
|
||||
test "a fourth bonus pushes the oldest out and says so" {
|
||||
fresh()
|
||||
effects_add(LEGS, 1.0, 10.0, true, "a")
|
||||
effects_add(LEGS, 1.0, 30.0, true, "b")
|
||||
effects_add(WARMTH, 1.0, 40.0, true, "c")
|
||||
effects_add(CARRY, 1.0, 50.0, true, "d")
|
||||
expect_eq(effects_bonus_count(), 3)
|
||||
let gone = ended()
|
||||
test "a fourth bonus pushes the oldest out and says so" (base_st: mut BaseState, effects_st: mut EffectsState) {
|
||||
fresh(base_st, effects_st)
|
||||
effects_add(base_st, effects_st, LEGS, 1.0, 10.0, true, "a")
|
||||
effects_add(base_st, effects_st, LEGS, 1.0, 30.0, true, "b")
|
||||
effects_add(base_st, effects_st, WARMTH, 1.0, 40.0, true, "c")
|
||||
effects_add(base_st, effects_st, CARRY, 1.0, 50.0, true, "d")
|
||||
expect_eq(effects_bonus_count(effects_st), 3)
|
||||
let gone = ended(base_st, effects_st)
|
||||
expect_eq(len(gone), 1)
|
||||
expect(gone[0].label == "a")
|
||||
expect_eq(gone[0].why, EFFECT_PUSHED_OUT)
|
||||
}
|
||||
|
||||
test "a full ring drops the one nearest its end" {
|
||||
fresh()
|
||||
effects_config(2, 3)
|
||||
effects_add(LEGS, 1.0, 10.0, false, "short")
|
||||
effects_add(WARMTH, 1.0, 90.0, false, "long")
|
||||
effects_add(CARRY, 1.0, 50.0, false, "new")
|
||||
expect_eq(effects_count(), 2)
|
||||
expect(not effects_has(LEGS))
|
||||
expect_eq(ended()[0].why, EFFECT_CROWDED_OUT)
|
||||
test "a full ring drops the one nearest its end" (base_st: mut BaseState, effects_st: mut EffectsState) {
|
||||
fresh(base_st, effects_st)
|
||||
effects_config(effects_st, 2, 3)
|
||||
effects_add(base_st, effects_st, LEGS, 1.0, 10.0, false, "short")
|
||||
effects_add(base_st, effects_st, WARMTH, 1.0, 90.0, false, "long")
|
||||
effects_add(base_st, effects_st, CARRY, 1.0, 50.0, false, "new")
|
||||
expect_eq(effects_count(effects_st), 2)
|
||||
expect(not effects_has(effects_st, LEGS))
|
||||
expect_eq(ended(base_st, effects_st)[0].why, EFFECT_CROWDED_OUT)
|
||||
}
|
||||
|
||||
test "a delayed one counts only once it has begun" {
|
||||
fresh()
|
||||
effects_add(LEGS, 8.0, 20.0, false, "Fizz")
|
||||
effects_add_after(20.0, LEGS, -5.0, 15.0, false, "Sugar crash")
|
||||
expect(effects_sum(LEGS) == 8.0)
|
||||
expect_eq(len(effects_live()), 1)
|
||||
test "a delayed one counts only once it has begun" (base_st: mut BaseState, effects_st: mut EffectsState) {
|
||||
fresh(base_st, effects_st)
|
||||
effects_add(base_st, effects_st, LEGS, 8.0, 20.0, false, "Fizz")
|
||||
effects_add_after(base_st, effects_st, 20.0, LEGS, -5.0, 15.0, false, "Sugar crash")
|
||||
expect(effects_sum(effects_st, LEGS) == 8.0)
|
||||
expect_eq(len(effects_live(effects_st)), 1)
|
||||
tick_minutes(25.0)
|
||||
expect(effects_sum(LEGS) == -5.0)
|
||||
expect(effects_sum(effects_st, LEGS) == -5.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" {
|
||||
fresh()
|
||||
effects_add(WARMTH, -10.0, 10.0, false, "Wet through")
|
||||
test "running down in game hours reports each ending" (base_st: mut BaseState, effects_st: mut EffectsState) {
|
||||
fresh(base_st, effects_st)
|
||||
effects_add(base_st, effects_st, WARMTH, -10.0, 10.0, false, "Wet through")
|
||||
tick_minutes(5.0)
|
||||
expect_eq(waiting(), 0)
|
||||
expect_eq(waiting(base_st, effects_st), 0)
|
||||
tick_minutes(6.0)
|
||||
expect_eq(effects_count(), 0)
|
||||
let gone = ended()
|
||||
expect_eq(effects_count(effects_st), 0)
|
||||
let gone = ended(base_st, effects_st)
|
||||
expect_eq(gone[0].why, EFFECT_RAN_OUT)
|
||||
expect(gone[0].label == "Wet through")
|
||||
}
|
||||
|
||||
test "clear stops everything and says nothing" {
|
||||
fresh()
|
||||
effects_add(LEGS, 1.0, 10.0, false, "a")
|
||||
effects_clear()
|
||||
expect_eq(effects_count(), 0)
|
||||
expect_eq(waiting(), 0)
|
||||
test "clear stops everything and says nothing" (base_st: mut BaseState, effects_st: mut EffectsState) {
|
||||
fresh(base_st, effects_st)
|
||||
effects_add(base_st, effects_st, LEGS, 1.0, 10.0, false, "a")
|
||||
effects_clear(effects_st)
|
||||
expect_eq(effects_count(effects_st), 0)
|
||||
expect_eq(waiting(base_st, effects_st), 0)
|
||||
}
|
||||
|
||||
test "a save section reads back the same ring" {
|
||||
fresh()
|
||||
effects_add(LEGS, 8.5, 20.0, true, "Fizz")
|
||||
effects_add_after(20.0, LEGS, -5.0, 15.0, false, "Sugar crash")
|
||||
test "a save section reads back the same ring" (base_st: mut BaseState, effects_st: mut EffectsState) {
|
||||
fresh(base_st, effects_st)
|
||||
effects_add(base_st, effects_st, LEGS, 8.5, 20.0, true, "Fizz")
|
||||
effects_add_after(base_st, effects_st, 20.0, LEGS, -5.0, 15.0, false, "Sugar crash")
|
||||
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)
|
||||
effects_clear()
|
||||
effects_clear(effects_st)
|
||||
let n = load_section(save_decode(text), "effects")
|
||||
expect(n.found)
|
||||
effects_load(n.data, n.version)
|
||||
expect_eq(effects_count(), 2)
|
||||
let a = effects_at(0)
|
||||
effects_load(effects_st, n.data, n.version)
|
||||
expect_eq(effects_count(effects_st), 2)
|
||||
let a = effects_at(effects_st, 0)
|
||||
expect(a.mag == 8.5)
|
||||
expect(a.bonus)
|
||||
let b = effects_at(1)
|
||||
let b = effects_at(effects_st, 1)
|
||||
expect(b.delay == 20.0)
|
||||
expect(b.label == "Sugar crash")
|
||||
}
|
||||
|
||||
test "the copy a reader gets does not change the ring" {
|
||||
fresh()
|
||||
effects_add(LEGS, 3.0, 10.0, false, "a")
|
||||
let e = effects_at(0)
|
||||
test "the copy a reader gets does not change the ring" (base_st: mut BaseState, effects_st: mut EffectsState) {
|
||||
fresh(base_st, effects_st)
|
||||
effects_add(base_st, effects_st, LEGS, 3.0, 10.0, false, "a")
|
||||
let e = effects_at(effects_st, 0)
|
||||
e.mag = 100.0
|
||||
expect(effects_sum(LEGS) == 3.0)
|
||||
expect(effects_sum(effects_st, LEGS) == 3.0)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
# 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.
|
||||
function ef_end(i: int, why: int) -> void {
|
||||
let all = ef_all()
|
||||
function ef_end(base_st: mut BaseState, effects_st: mut EffectsState, i: int, why: int) -> void {
|
||||
let all = ef_all(effects_st)
|
||||
let e = new EffectEnded
|
||||
e.kind = all[i].kind
|
||||
e.label = all[i].label
|
||||
e.why = why
|
||||
e.bonus = all[i].bonus
|
||||
q_push(effects_ended(), e)
|
||||
ef_drop(i)
|
||||
q_push(base_st, effects_ended(base_st, effects_st), e)
|
||||
ef_drop(effects_st, i)
|
||||
}
|
||||
|
||||
function ef_drop(i: int) -> void {
|
||||
let all = ef_all()
|
||||
function ef_drop(effects_st: mut EffectsState, i: int) -> void {
|
||||
let all = ef_all(effects_st)
|
||||
let rest = new []Effect
|
||||
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
|
||||
function ef_nearest_end(bonus_only: bool) -> int {
|
||||
let all = ef_all()
|
||||
function ef_nearest_end(effects_st: mut EffectsState, bonus_only: bool) -> int {
|
||||
let all = ef_all(effects_st)
|
||||
var at = -1
|
||||
for i in 0 .. len(all) {
|
||||
if bonus_only and not all[i].bonus { continue }
|
||||
|
|
@ -29,16 +29,16 @@ function ef_nearest_end(bonus_only: bool) -> int {
|
|||
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 {
|
||||
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
|
||||
export function effects_add_after(after: float, kind: int, mag: float, minutes: float, bonus: bool, label: string) -> void {
|
||||
let all = ef_all()
|
||||
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(effects_st)
|
||||
for i in 0 .. len(all) {
|
||||
if all[i].kind == kind and all[i].label == label {
|
||||
all[i].mag = mag
|
||||
|
|
@ -46,7 +46,7 @@ export function effects_add_after(after: float, kind: int, mag: float, minutes:
|
|||
return
|
||||
}
|
||||
}
|
||||
ef_make_room(bonus)
|
||||
ef_make_room(base_st, effects_st, bonus)
|
||||
let e = new Effect
|
||||
e.kind = kind
|
||||
e.mag = mag
|
||||
|
|
@ -54,27 +54,27 @@ export function effects_add_after(after: float, kind: int, mag: float, minutes:
|
|||
e.delay = after
|
||||
e.bonus = bonus
|
||||
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 {
|
||||
effects_add_after(0.0, kind, mag, minutes, bonus, label)
|
||||
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(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
|
||||
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
|
||||
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
|
||||
while i < len(ef_all()) {
|
||||
let e = ef_all()[i]
|
||||
while i < len(ef_all(effects_st)) {
|
||||
let e = ef_all(effects_st)[i]
|
||||
if e.delay > 0.0 {
|
||||
e.delay = e.delay - minutes
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,31 @@
|
|||
# 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
|
||||
# (fire_cost), and doing it (fire_apply - only ever on the machine that owns the world)
|
||||
export function fire_decide(wood: int) -> int {
|
||||
if fr_lit {
|
||||
export function fire_decide(fire_st: FireState, wood: int) -> int {
|
||||
if fire_st.fr_lit {
|
||||
if wood < 1 { return FIRE_NO_LOG }
|
||||
return FIRE_FEED
|
||||
}
|
||||
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 }
|
||||
return FIRE_LIGHT
|
||||
}
|
||||
|
||||
export function fire_cost(how: int) -> int {
|
||||
if how == FIRE_LIGHT { return fr_light_wood }
|
||||
export function fire_cost(fire_st: FireState, how: int) -> int {
|
||||
if how == FIRE_LIGHT { return fire_st.fr_light_wood }
|
||||
if how == FIRE_FEED { return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
export function fire_apply(how: int) -> void {
|
||||
if how == FIRE_FEED { fr_fuel = fr_fuel + fr_log_minutes }
|
||||
export function fire_apply(base_st: mut BaseState, fire_st: mut FireState, how: int) -> void {
|
||||
if how == FIRE_FEED { fire_st.fr_fuel = fire_st.fr_fuel + fire_st.fr_log_minutes }
|
||||
if how == FIRE_LIGHT {
|
||||
fr_lit = true
|
||||
fr_fuel = fr_light_minutes
|
||||
fr_fact(FIRE_LIT)
|
||||
fire_st.fr_lit = true
|
||||
fire_st.fr_fuel = fire_st.fr_light_minutes
|
||||
fr_fact(base_st, fire_st, FIRE_LIT)
|
||||
}
|
||||
}
|
||||
|
||||
# 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 }
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
# 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
|
||||
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
|
||||
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?
|
||||
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)
|
||||
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 }
|
||||
|
|
|
|||
|
|
@ -10,12 +10,18 @@ export port FireWorld {
|
|||
function fr_yes() -> bool { return true }
|
||||
function fr_no() -> bool { return false }
|
||||
|
||||
var fr_light_wood: int = 3 # logs to light one
|
||||
var fr_light_minutes: float = 120.0 # game minutes of fuel a new fire has
|
||||
var fr_log_minutes: float = 60.0 # game minutes a log adds
|
||||
var fr_low_minutes: float = 15.0 # under this, the fire is burning low
|
||||
var fr_warm_radius: float = 3.6 # metres it warms a body
|
||||
var fr_cook_radius: float = 3.5 # metres it cooks from
|
||||
export state FireState {
|
||||
fr_light_wood: int = 3 # logs to light one
|
||||
fr_light_minutes: float = 120.0 # game minutes of fuel a new fire has
|
||||
fr_log_minutes: float = 60.0 # game minutes a log adds
|
||||
fr_low_minutes: float = 15.0 # under this, the fire is burning low
|
||||
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
|
||||
export const FIRE_NO_WOOD: int = 0 # not enough to light one
|
||||
|
|
@ -35,34 +41,30 @@ export property FireFact {
|
|||
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
|
||||
export function fire_facts() -> Queue<FireFact> {
|
||||
if fr_facts == null { fr_facts = queue_new("fire.facts") }
|
||||
return fr_facts
|
||||
export function fire_facts(base_st: mut BaseState, fire_st: mut FireState) -> Queue<FireFact> {
|
||||
if fire_st.fr_facts == null { fire_st.fr_facts = queue_new(base_st, "fire.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
|
||||
f.what = what
|
||||
f.fuel = fr_fuel
|
||||
q_push(fire_facts(), f)
|
||||
f.fuel = fire_st.fr_fuel
|
||||
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
|
||||
export function fire_config(light_wood: int, light_minutes: float, log_minutes: float, low_minutes: float) -> void {
|
||||
fr_light_wood = light_wood
|
||||
fr_light_minutes = light_minutes
|
||||
fr_log_minutes = log_minutes
|
||||
fr_low_minutes = low_minutes
|
||||
export function fire_config(fire_st: mut FireState, light_wood: int, light_minutes: float, log_minutes: float, low_minutes: float) -> void {
|
||||
fire_st.fr_light_wood = light_wood
|
||||
fire_st.fr_light_minutes = light_minutes
|
||||
fire_st.fr_log_minutes = log_minutes
|
||||
fire_st.fr_low_minutes = low_minutes
|
||||
}
|
||||
|
||||
# how far it warms a body and how far it cooks from, in metres
|
||||
export function fire_config_reach(warm: float, cook: float) -> void {
|
||||
fr_warm_radius = warm
|
||||
fr_cook_radius = cook
|
||||
export function fire_config_reach(fire_st: mut FireState, warm: float, cook: float) -> void {
|
||||
fire_st.fr_warm_radius = warm
|
||||
fire_st.fr_cook_radius = cook
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
# 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()
|
||||
sv_put_bool(v, "lit", fr_lit)
|
||||
sv_put_float(v, "fuel", fr_fuel)
|
||||
sv_put_float(v, "rain_h", fr_rain_h)
|
||||
sv_put_bool(v, "lit", fire_st.fr_lit)
|
||||
sv_put_float(v, "fuel", fire_st.fr_fuel)
|
||||
sv_put_float(v, "rain_h", fire_st.fr_rain_h)
|
||||
return v
|
||||
}
|
||||
|
||||
export function fire_load(v: Val, version: int) -> void {
|
||||
fire_reset()
|
||||
fr_lit = sv_bool(v, "lit", false)
|
||||
fr_fuel = sv_float(v, "fuel", 0.0)
|
||||
fr_rain_h = sv_float(v, "rain_h", 0.0)
|
||||
export function fire_load(base_st: mut BaseState, fire_st: mut FireState, v: Val, version: int) -> void {
|
||||
fire_reset(base_st, fire_st)
|
||||
fire_st.fr_lit = sv_bool(v, "lit", false)
|
||||
fire_st.fr_fuel = sv_float(v, "fuel", 0.0)
|
||||
fire_st.fr_rain_h = sv_float(v, "rain_h", 0.0)
|
||||
}
|
||||
|
||||
export function fire_system() -> System {
|
||||
|
|
|
|||
|
|
@ -4,15 +4,17 @@ import "ludic.fire"
|
|||
import "ludic.base"
|
||||
program FireTest {
|
||||
numbers float
|
||||
var owns: bool = true
|
||||
var wet: bool = false
|
||||
var tarp: bool = false
|
||||
var ban: bool = false
|
||||
state FiretestState {
|
||||
owns: bool = true
|
||||
wet: bool = false
|
||||
tarp: bool = false
|
||||
ban: bool = false
|
||||
}
|
||||
|
||||
function fake_owns() -> bool { return owns }
|
||||
function fake_wet() -> bool { return wet }
|
||||
function fake_tarp() -> bool { return tarp }
|
||||
function fake_allowed() -> bool { return not ban }
|
||||
function fake_owns(firetest_st: FiretestState) -> bool { return firetest_st.owns }
|
||||
function fake_wet(firetest_st: FiretestState) -> bool { return firetest_st.wet }
|
||||
function fake_tarp(firetest_st: FiretestState) -> bool { return firetest_st.tarp }
|
||||
function fake_allowed(firetest_st: FiretestState) -> bool { return not firetest_st.ban }
|
||||
|
||||
bind FireWorld {
|
||||
owns_world: fn fake_owns
|
||||
|
|
@ -21,125 +23,125 @@ program FireTest {
|
|||
allowed: fn fake_allowed
|
||||
}
|
||||
|
||||
function fresh() -> void {
|
||||
fire_config(3, 120.0, 60.0, 15.0)
|
||||
fire_config_reach(3.6, 3.5)
|
||||
fire_reset()
|
||||
function fresh(base_st: mut BaseState, fire_st: mut FireState) -> void {
|
||||
fire_config(fire_st, 3, 120.0, 60.0, 15.0)
|
||||
fire_config_reach(fire_st, 3.6, 3.5)
|
||||
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 {
|
||||
var n = 0
|
||||
for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } }
|
||||
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" {
|
||||
fresh()
|
||||
expect_eq(fire_decide(2), FIRE_NO_WOOD)
|
||||
expect_eq(fire_decide(3), FIRE_LIGHT)
|
||||
expect_eq(fire_cost(FIRE_LIGHT), 3)
|
||||
light()
|
||||
expect(fire_lit())
|
||||
expect(fire_fuel() == 120.0)
|
||||
expect_eq(count(facts(), FIRE_LIT), 1)
|
||||
test "three logs light it, and that is a fact" (base_st: mut BaseState, fire_st: mut FireState) {
|
||||
fresh(base_st, fire_st)
|
||||
expect_eq(fire_decide(fire_st, 2), FIRE_NO_WOOD)
|
||||
expect_eq(fire_decide(fire_st, 3), FIRE_LIGHT)
|
||||
expect_eq(fire_cost(fire_st, FIRE_LIGHT), 3)
|
||||
light(base_st, fire_st)
|
||||
expect(fire_lit(fire_st))
|
||||
expect(fire_fuel(fire_st) == 120.0)
|
||||
expect_eq(count(facts(base_st, fire_st), FIRE_LIT), 1)
|
||||
}
|
||||
|
||||
test "a lit fire takes a log for an hour" {
|
||||
fresh()
|
||||
light()
|
||||
expect_eq(fire_decide(0), FIRE_NO_LOG)
|
||||
expect_eq(fire_decide(1), FIRE_FEED)
|
||||
expect_eq(fire_cost(FIRE_FEED), 1)
|
||||
fire_apply(FIRE_FEED)
|
||||
expect(fire_fuel() == 180.0)
|
||||
test "a lit fire takes a log for an hour" (base_st: mut BaseState, fire_st: mut FireState) {
|
||||
fresh(base_st, fire_st)
|
||||
light(base_st, fire_st)
|
||||
expect_eq(fire_decide(fire_st, 0), FIRE_NO_LOG)
|
||||
expect_eq(fire_decide(fire_st, 1), FIRE_FEED)
|
||||
expect_eq(fire_cost(fire_st, FIRE_FEED), 1)
|
||||
fire_apply(base_st, fire_st, FIRE_FEED)
|
||||
expect(fire_fuel(fire_st) == 180.0)
|
||||
}
|
||||
|
||||
test "a ban and a soaked ring refuse, a tarp keeps it dry" {
|
||||
fresh()
|
||||
ban = true
|
||||
expect_eq(fire_decide(5), FIRE_BANNED)
|
||||
ban = false
|
||||
wet = true
|
||||
expect_eq(fire_decide(5), FIRE_SOAKED)
|
||||
expect_eq(fire_cost(FIRE_SOAKED), 0)
|
||||
tarp = true
|
||||
expect_eq(fire_decide(5), FIRE_LIGHT)
|
||||
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(base_st, fire_st)
|
||||
firetest_st.ban = true
|
||||
expect_eq(fire_decide(fire_st, 5), FIRE_BANNED)
|
||||
firetest_st.ban = false
|
||||
firetest_st.wet = true
|
||||
expect_eq(fire_decide(fire_st, 5), FIRE_SOAKED)
|
||||
expect_eq(fire_cost(fire_st, FIRE_SOAKED), 0)
|
||||
firetest_st.tarp = true
|
||||
expect_eq(fire_decide(fire_st, 5), FIRE_LIGHT)
|
||||
}
|
||||
|
||||
test "it burns a minute a minute, runs low, and goes out as facts" {
|
||||
fresh()
|
||||
light()
|
||||
facts()
|
||||
fire_run(1.5)
|
||||
expect(fire_fuel() == 30.0)
|
||||
expect(not fire_low())
|
||||
fire_run(0.3)
|
||||
expect(fire_low())
|
||||
expect_eq(count(facts(), FIRE_LOW_FUEL), 1)
|
||||
fire_run(0.5)
|
||||
expect(not fire_lit())
|
||||
expect(fire_fuel() == 0.0)
|
||||
expect_eq(count(facts(), FIRE_WENT_OUT), 1)
|
||||
test "it burns a minute a minute, runs low, and goes out as facts" (base_st: mut BaseState, fire_st: mut FireState) {
|
||||
fresh(base_st, fire_st)
|
||||
light(base_st, fire_st)
|
||||
facts(base_st, fire_st)
|
||||
fire_run(base_st, fire_st, 1.5)
|
||||
expect(fire_fuel(fire_st) == 30.0)
|
||||
expect(not fire_low(fire_st))
|
||||
fire_run(base_st, fire_st, 0.3)
|
||||
expect(fire_low(fire_st))
|
||||
expect_eq(count(facts(base_st, fire_st), FIRE_LOW_FUEL), 1)
|
||||
fire_run(base_st, fire_st, 0.5)
|
||||
expect(not fire_lit(fire_st))
|
||||
expect(fire_fuel(fire_st) == 0.0)
|
||||
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" {
|
||||
fresh()
|
||||
light()
|
||||
wet = true
|
||||
fire_run(0.5)
|
||||
expect(fire_fuel() == 60.0)
|
||||
tarp = true
|
||||
fire_run(0.5)
|
||||
expect(fire_fuel() == 30.0)
|
||||
expect(fire_rain_hours() == 0.5)
|
||||
fire_morning()
|
||||
expect(fire_rain_hours() == 0.0)
|
||||
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(base_st, fire_st)
|
||||
light(base_st, fire_st)
|
||||
firetest_st.wet = true
|
||||
fire_run(base_st, fire_st, 0.5)
|
||||
expect(fire_fuel(fire_st) == 60.0)
|
||||
firetest_st.tarp = true
|
||||
fire_run(base_st, fire_st, 0.5)
|
||||
expect(fire_fuel(fire_st) == 30.0)
|
||||
expect(fire_rain_hours(fire_st) == 0.5)
|
||||
fire_morning(fire_st)
|
||||
expect(fire_rain_hours(fire_st) == 0.0)
|
||||
}
|
||||
|
||||
test "a machine that does not own the world burns nothing, but is told" {
|
||||
fresh()
|
||||
light()
|
||||
owns = false
|
||||
fire_run(5.0)
|
||||
expect(fire_fuel() == 120.0)
|
||||
facts()
|
||||
fire_told(false, 0.0, true)
|
||||
expect(not fire_lit())
|
||||
expect_eq(count(facts(), FIRE_WENT_OUT), 1)
|
||||
fire_told(true, 50.0, true)
|
||||
fire_told(false, 0.0, false)
|
||||
expect_eq(q_len(fire_facts()), 0)
|
||||
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(base_st, fire_st)
|
||||
light(base_st, fire_st)
|
||||
firetest_st.owns = false
|
||||
fire_run(base_st, fire_st, 5.0)
|
||||
expect(fire_fuel(fire_st) == 120.0)
|
||||
facts(base_st, fire_st)
|
||||
fire_told(base_st, fire_st, false, 0.0, true)
|
||||
expect(not fire_lit(fire_st))
|
||||
expect_eq(count(facts(base_st, fire_st), FIRE_WENT_OUT), 1)
|
||||
fire_told(base_st, fire_st, true, 50.0, true)
|
||||
fire_told(base_st, fire_st, false, 0.0, false)
|
||||
expect_eq(q_len(fire_facts(base_st, fire_st)), 0)
|
||||
}
|
||||
|
||||
test "it warms and cooks close, and only while lit" {
|
||||
fresh()
|
||||
expect(not fire_warms(1.0))
|
||||
light()
|
||||
expect(fire_warms(3.5))
|
||||
expect(not fire_warms(3.7))
|
||||
expect(fire_cooks(3.4))
|
||||
expect(not fire_cooks(3.55))
|
||||
fire_out()
|
||||
expect(not fire_cooks(0.0))
|
||||
test "it warms and cooks close, and only while lit" (base_st: mut BaseState, fire_st: mut FireState) {
|
||||
fresh(base_st, fire_st)
|
||||
expect(not fire_warms(fire_st, 1.0))
|
||||
light(base_st, fire_st)
|
||||
expect(fire_warms(fire_st, 3.5))
|
||||
expect(not fire_warms(fire_st, 3.7))
|
||||
expect(fire_cooks(fire_st, 3.4))
|
||||
expect(not fire_cooks(fire_st, 3.55))
|
||||
fire_out(fire_st)
|
||||
expect(not fire_cooks(fire_st, 0.0))
|
||||
}
|
||||
|
||||
test "a save section reads back the same fire" {
|
||||
fresh()
|
||||
light()
|
||||
wet = true
|
||||
tarp = true
|
||||
fire_run(0.25)
|
||||
test "a save section reads back the same fire" (base_st: mut BaseState, fire_st: mut FireState, firetest_st: mut FiretestState) {
|
||||
fresh(base_st, fire_st)
|
||||
light(base_st, fire_st)
|
||||
firetest_st.wet = true
|
||||
firetest_st.tarp = true
|
||||
fire_run(base_st, fire_st, 0.25)
|
||||
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)
|
||||
fire_reset()
|
||||
fire_reset(base_st, fire_st)
|
||||
let n = load_section(save_decode(text), "fire")
|
||||
fire_load(n.data, n.version)
|
||||
expect(fire_lit())
|
||||
expect(fire_fuel() == 105.0)
|
||||
expect(fire_rain_hours() == 0.25)
|
||||
fire_load(base_st, fire_st, n.data, n.version)
|
||||
expect(fire_lit(fire_st))
|
||||
expect(fire_fuel(fire_st) == 105.0)
|
||||
expect(fire_rain_hours(fire_st) == 0.25)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,55 +1,55 @@
|
|||
# verbs.ludic - the fire burning down, and the only other ways it changes
|
||||
function fr_out() -> void {
|
||||
fr_lit = false
|
||||
fr_fuel = 0.0
|
||||
fr_fact(FIRE_WENT_OUT)
|
||||
function fr_out(base_st: mut BaseState, fire_st: mut FireState) -> void {
|
||||
fire_st.fr_lit = false
|
||||
fire_st.fr_fuel = 0.0
|
||||
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,
|
||||
# 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 {
|
||||
if not fr_lit or not FireWorld.owns_world() { return }
|
||||
let was = fr_fuel
|
||||
fr_fuel = fr_fuel - gh * 60.0
|
||||
export function fire_run(base_st: mut BaseState, fire_st: mut FireState, gh: float) -> void {
|
||||
if not fire_st.fr_lit or not FireWorld.owns_world() { return }
|
||||
let was = fire_st.fr_fuel
|
||||
fire_st.fr_fuel = fire_st.fr_fuel - gh * 60.0
|
||||
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 {
|
||||
fr_out()
|
||||
if fire_st.fr_fuel < 0.0 {
|
||||
fr_out(base_st, fire_st)
|
||||
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)
|
||||
export function fire_out() -> void {
|
||||
fr_lit = false
|
||||
fr_fuel = 0.0
|
||||
export function fire_out(fire_st: mut FireState) -> void {
|
||||
fire_st.fr_lit = false
|
||||
fire_st.fr_fuel = 0.0
|
||||
}
|
||||
|
||||
# burning with `fuel` minutes, and nothing said (staging, a test)
|
||||
export function fire_set_burning(fuel: float) -> void {
|
||||
fr_lit = true
|
||||
fr_fuel = fuel
|
||||
export function fire_set_burning(fire_st: mut FireState, fuel: float) -> void {
|
||||
fire_st.fr_lit = true
|
||||
fire_st.fr_fuel = fuel
|
||||
}
|
||||
|
||||
# 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 {
|
||||
let was = fr_lit
|
||||
fr_lit = lit
|
||||
fr_fuel = fuel
|
||||
if was and not lit and say { fr_fact(FIRE_WENT_OUT) }
|
||||
export function fire_told(base_st: mut BaseState, fire_st: mut FireState, lit: bool, fuel: float, say: bool) -> void {
|
||||
let was = fire_st.fr_lit
|
||||
fire_st.fr_lit = lit
|
||||
fire_st.fr_fuel = fuel
|
||||
if was and not lit and say { fr_fact(base_st, fire_st, FIRE_WENT_OUT) }
|
||||
}
|
||||
|
||||
# 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
|
||||
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 {
|
||||
fr_lit = false
|
||||
fr_fuel = 0.0
|
||||
fr_rain_h = 0.0
|
||||
q_clear(fire_facts())
|
||||
export function fire_reset(base_st: mut BaseState, fire_st: mut FireState) -> void {
|
||||
fire_st.fr_lit = false
|
||||
fire_st.fr_fuel = 0.0
|
||||
fire_st.fr_rain_h = 0.0
|
||||
q_clear(base_st, fire_facts(base_st, fire_st))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
# 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
|
||||
function fishing_pick() -> int {
|
||||
function fishing_pick(fishing_st: mut FishingState) -> int {
|
||||
let leg = fishing_legend()
|
||||
if leg >= 0 and fishing_lure_on {
|
||||
if leg >= 0 and fishing_st.fishing_lure_on {
|
||||
let k = FishSpecies[leg]
|
||||
var p = k.chance
|
||||
if FishingWorld.afloat() or FishingWorld.depth(fishing_bx, fishing_bz) >= fishing_deep { p = k.chance_deep }
|
||||
if fishing_rand() < p + FishingWorld.skill() { return leg }
|
||||
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(fishing_st) < p + FishingWorld.skill() { return leg }
|
||||
}
|
||||
var total = 0.0
|
||||
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
|
||||
for s in 0 .. FISH_COUNT {
|
||||
if FishSpecies[s].legend { continue }
|
||||
|
|
@ -21,37 +21,37 @@ function fishing_pick() -> int {
|
|||
return last
|
||||
}
|
||||
|
||||
function fishing_size(s: int) -> int {
|
||||
function fishing_size(fishing_st: mut FishingState, s: int) -> int {
|
||||
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)
|
||||
}
|
||||
|
||||
# the green's half width this crossing: a fish that closes it does so at every rung, and Hard
|
||||
# closes it for everything else
|
||||
export function fishing_window() -> float {
|
||||
if not fishing_kind_ok(fishing_sp) { return 0.12 }
|
||||
let k = FishSpecies[fishing_sp]
|
||||
let hw = FishingWorld.window(fishing_sp)
|
||||
if k.closes > 0.0 { return Math.max(hw * (1.0 - k.closes * float(fishing_won)), hw * k.floor) }
|
||||
if FishingWorld.level() == FISHING_HARD { return Math.max(hw * (1.0 - 0.10 * float(fishing_won)), hw * 0.55) }
|
||||
export function fishing_window(fishing_st: FishingState) -> float {
|
||||
if not fishing_kind_ok(fishing_st.fishing_sp) { return 0.12 }
|
||||
let k = FishSpecies[fishing_st.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_st.fishing_won)), hw * k.floor) }
|
||||
if FishingWorld.level() == FISHING_HARD { return Math.max(hw * (1.0 - 0.10 * float(fishing_st.fishing_won)), hw * 0.55) }
|
||||
return hw
|
||||
}
|
||||
|
||||
export function fishing_window_lo() -> float { return Math.clamp(fishing_win_c - fishing_window(), 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_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(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
|
||||
function fishing_new_window() -> void {
|
||||
function fishing_new_window(fishing_st: mut FishingState) -> void {
|
||||
let lv = FishingWorld.level()
|
||||
if lv == FISHING_RELAXED or fishing_gentle_on {
|
||||
fishing_win_c = 0.5
|
||||
if lv == FISHING_RELAXED or fishing_st.fishing_gentle_on {
|
||||
fishing_st.fishing_win_c = 0.5
|
||||
return
|
||||
}
|
||||
let hw = fishing_window() + 0.05
|
||||
fishing_win_c = hw + fishing_rand() * (1.0 - hw - hw)
|
||||
let hw = fishing_window(fishing_st) + 0.05
|
||||
fishing_st.fishing_win_c = hw + fishing_rand(fishing_st) * (1.0 - hw - hw)
|
||||
}
|
||||
|
||||
function fishing_in_window() -> bool {
|
||||
return fishing_mark > fishing_win_c - fishing_window() and fishing_mark < fishing_win_c + fishing_window()
|
||||
function fishing_in_window(fishing_st: FishingState) -> bool {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,64 +1,64 @@
|
|||
# run.ludic - `dt` real seconds of a line in the water: the wait, the bite, and the fight
|
||||
function fishing_run_wait() -> void {
|
||||
if fishing_t >= 0.0 { return }
|
||||
fishing_st = FISHING_BITE_ON
|
||||
fishing_t = 1.6
|
||||
fishing_sp = fishing_pick()
|
||||
fishing_say(FISHING_BITE)
|
||||
function fishing_run_wait(base_st: mut BaseState, fishing_st: mut FishingState) -> void {
|
||||
if fishing_st.fishing_t >= 0.0 { return }
|
||||
fishing_st.fishing_st = FISHING_BITE_ON
|
||||
fishing_st.fishing_t = 1.6
|
||||
fishing_st.fishing_sp = fishing_pick(fishing_st)
|
||||
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
|
||||
function fishing_run_runs(dt: float) -> float {
|
||||
let k = FishSpecies[fishing_sp]
|
||||
if fishing_run_t > 0.0 {
|
||||
fishing_run_t = fishing_run_t - dt
|
||||
if fishing_run_t <= 0.0 {
|
||||
fishing_run_t = 0.0
|
||||
fishing_say(FISHING_TIRED)
|
||||
function fishing_run_runs(base_st: mut BaseState, fishing_st: mut FishingState, dt: float) -> float {
|
||||
let k = FishSpecies[fishing_st.fishing_sp]
|
||||
if fishing_st.fishing_run_t > 0.0 {
|
||||
fishing_st.fishing_run_t = fishing_st.fishing_run_t - dt
|
||||
if fishing_st.fishing_run_t <= 0.0 {
|
||||
fishing_st.fishing_run_t = 0.0
|
||||
fishing_say(base_st, fishing_st, FISHING_TIRED)
|
||||
}
|
||||
return 4.2
|
||||
}
|
||||
if fishing_run_left > 0 {
|
||||
fishing_run_next = fishing_run_next - dt
|
||||
if fishing_run_next <= 0.0 {
|
||||
fishing_run_left -= 1
|
||||
fishing_run_t = 2.2
|
||||
fishing_run_next = 4.0 + fishing_rand() * 4.0
|
||||
fishing_say(FISHING_RUNS)
|
||||
if fishing_st.fishing_run_left > 0 {
|
||||
fishing_st.fishing_run_next = fishing_st.fishing_run_next - dt
|
||||
if fishing_st.fishing_run_next <= 0.0 {
|
||||
fishing_st.fishing_run_left -= 1
|
||||
fishing_st.fishing_run_t = 2.2
|
||||
fishing_st.fishing_run_next = 4.0 + fishing_rand(fishing_st) * 4.0
|
||||
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 {
|
||||
let sp = fishing_run_runs(dt)
|
||||
if fishing_gentle_on {
|
||||
fishing_ring_t = fishing_ring_t - dt
|
||||
if fishing_ring_t <= 0.0 { fishing_lost(FISHING_LINE_SNAPPED) }
|
||||
function fishing_run_reel(base_st: mut BaseState, fishing_st: mut FishingState, dt: float) -> void {
|
||||
let sp = fishing_run_runs(base_st, fishing_st, dt)
|
||||
if fishing_st.fishing_gentle_on {
|
||||
fishing_st.fishing_ring_t = fishing_st.fishing_ring_t - dt
|
||||
if fishing_st.fishing_ring_t <= 0.0 { fishing_lost(base_st, fishing_st, FISHING_LINE_SNAPPED) }
|
||||
return
|
||||
}
|
||||
fishing_mark = fishing_mark + sp * fishing_dir * dt
|
||||
if fishing_mark > 1.0 {
|
||||
fishing_mark = 1.0
|
||||
fishing_dir = -1.0
|
||||
fishing_st.fishing_mark = fishing_st.fishing_mark + sp * fishing_st.fishing_dir * dt
|
||||
if fishing_st.fishing_mark > 1.0 {
|
||||
fishing_st.fishing_mark = 1.0
|
||||
fishing_st.fishing_dir = -1.0
|
||||
}
|
||||
if fishing_mark < 0.0 {
|
||||
fishing_mark = 0.0
|
||||
fishing_dir = 1.0
|
||||
if fishing_st.fishing_mark < 0.0 {
|
||||
fishing_st.fishing_mark = 0.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 {
|
||||
if fishing_st == FISHING_OFF { return }
|
||||
fishing_t = fishing_t - dt
|
||||
if fishing_st == FISHING_WAIT {
|
||||
fishing_run_wait()
|
||||
export function fishing_run(base_st: mut BaseState, fishing_st: mut FishingState, dt: float) -> void {
|
||||
if fishing_st.fishing_st == FISHING_OFF { return }
|
||||
fishing_st.fishing_t = fishing_st.fishing_t - dt
|
||||
if fishing_st.fishing_st == FISHING_WAIT {
|
||||
fishing_run_wait(base_st, fishing_st)
|
||||
return
|
||||
}
|
||||
if fishing_st == FISHING_BITE_ON {
|
||||
if fishing_t < 0.0 { fishing_lost(FISHING_MISSED) }
|
||||
if fishing_st.fishing_st == FISHING_BITE_ON {
|
||||
if fishing_st.fishing_t < 0.0 { fishing_lost(base_st, fishing_st, FISHING_MISSED) }
|
||||
return
|
||||
}
|
||||
fishing_run_reel(dt)
|
||||
fishing_run_reel(base_st, fishing_st, dt)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,86 +4,88 @@ export const FISHING_WAIT: int = 1
|
|||
export const FISHING_BITE_ON: int = 2
|
||||
export const FISHING_REEL: int = 3
|
||||
|
||||
var fishing_st: int = 0
|
||||
var fishing_t: float = 0.0 # seconds: to the bite, of the bite, of the fight
|
||||
var fishing_bx: float = 0.0 # where the line is in the water
|
||||
var fishing_bz: float = 0.0
|
||||
var fishing_sp: int = -1 # what is on it
|
||||
var fishing_len: int = 0 # cm
|
||||
var fishing_bait_on: bool = false
|
||||
var fishing_lure_on: bool = false
|
||||
var fishing_mark: float = 0.0 # 0..1 across the bar
|
||||
var fishing_dir: float = 1.0
|
||||
var fishing_win_c: float = 0.5 # where the green sits this crossing
|
||||
var fishing_ring_t: float = 0.0 # Gentle: seconds left to press
|
||||
var fishing_gentle_on: bool = false
|
||||
var fishing_won: int = 0 # crossings won
|
||||
var fishing_need_n: int = 3
|
||||
var fishing_run_t: float = 0.0 # seconds left of a run
|
||||
var fishing_run_left: int = 0
|
||||
var fishing_run_next: float = 0.0
|
||||
var fishing_dice: Rng = null
|
||||
var fishing_fact_q: Queue<FishingFact> = null
|
||||
var fishing_n: int = 0 # the record: caught, the biggest (cm), a legend landed
|
||||
var fishing_big: int = 0
|
||||
var fishing_leg: bool = false
|
||||
export state FishingState {
|
||||
fishing_st: int = 0
|
||||
fishing_t: float = 0.0 # seconds: to the bite, of the bite, of the fight
|
||||
fishing_bx: float = 0.0 # where the line is in the water
|
||||
fishing_bz: float = 0.0
|
||||
fishing_sp: int = -1 # what is on it
|
||||
fishing_len: int = 0 # cm
|
||||
fishing_bait_on: bool = false
|
||||
fishing_lure_on: bool = false
|
||||
fishing_mark: float = 0.0 # 0..1 across the bar
|
||||
fishing_dir: float = 1.0
|
||||
fishing_win_c: float = 0.5 # where the green sits this crossing
|
||||
fishing_ring_t: float = 0.0 # Gentle: seconds left to press
|
||||
fishing_gentle_on: bool = false
|
||||
fishing_won: int = 0 # crossings won
|
||||
fishing_need_n: int = 3
|
||||
fishing_run_t: float = 0.0 # seconds left of a run
|
||||
fishing_run_left: int = 0
|
||||
fishing_run_next: float = 0.0
|
||||
fishing_dice: Rng = null
|
||||
fishing_fact_q: Queue<FishingFact> = null
|
||||
fishing_n: int = 0 # the record: caught, the biggest (cm), a legend landed
|
||||
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
|
||||
export function fishing_config(min_depth: float, deep: float, seed: int) -> void {
|
||||
fishing_min_depth = min_depth
|
||||
fishing_deep = deep
|
||||
fishing_seed_n = seed
|
||||
fishing_dice = rng_new(seed)
|
||||
export function fishing_config(fishing_st: mut FishingState, min_depth: float, deep: float, seed: int) -> void {
|
||||
fishing_st.fishing_min_depth = min_depth
|
||||
fishing_st.fishing_deep = deep
|
||||
fishing_st.fishing_seed_n = seed
|
||||
fishing_st.fishing_dice = rng_new(seed)
|
||||
}
|
||||
|
||||
export function fishing_facts() -> Queue<FishingFact> {
|
||||
if fishing_fact_q == null { fishing_fact_q = queue_new("fishing.facts") }
|
||||
return fishing_fact_q
|
||||
export function fishing_facts(base_st: mut BaseState, fishing_st: mut FishingState) -> Queue<FishingFact> {
|
||||
if fishing_st.fishing_fact_q == null { fishing_st.fishing_fact_q = queue_new(base_st, "fishing.facts") }
|
||||
return fishing_st.fishing_fact_q
|
||||
}
|
||||
|
||||
function fishing_rand() -> float {
|
||||
if fishing_dice == null { fishing_dice = rng_new(fishing_seed_n) }
|
||||
return rng_float(fishing_dice)
|
||||
function fishing_rand(fishing_st: mut FishingState) -> float {
|
||||
if fishing_st.fishing_dice == null { fishing_st.fishing_dice = rng_new(fishing_st.fishing_seed_n) }
|
||||
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
|
||||
f.what = what
|
||||
f.species = fishing_sp
|
||||
f.cm = fishing_len
|
||||
f.kg = fishing_weight_of(fishing_sp, fishing_len)
|
||||
f.hits = fishing_won
|
||||
f.need = fishing_need_n
|
||||
f.bait = fishing_bait_on
|
||||
f.lure = fishing_lure_on
|
||||
f.x = fishing_bx
|
||||
f.z = fishing_bz
|
||||
q_push(fishing_facts(), f)
|
||||
f.species = fishing_st.fishing_sp
|
||||
f.cm = fishing_st.fishing_len
|
||||
f.kg = fishing_weight_of(fishing_st.fishing_sp, fishing_st.fishing_len)
|
||||
f.hits = fishing_st.fishing_won
|
||||
f.need = fishing_st.fishing_need_n
|
||||
f.bait = fishing_st.fishing_bait_on
|
||||
f.lure = fishing_st.fishing_lure_on
|
||||
f.x = fishing_st.fishing_bx
|
||||
f.z = fishing_st.fishing_bz
|
||||
q_push(base_st, fishing_facts(base_st, fishing_st), f)
|
||||
}
|
||||
|
||||
export function fishing_state() -> int { return fishing_st }
|
||||
export function fishing_active() -> bool { return fishing_st != FISHING_OFF }
|
||||
export function fishing_species() -> int { return fishing_sp }
|
||||
export function fishing_cm() -> int { return fishing_len }
|
||||
export function fishing_line_x() -> float { return fishing_bx }
|
||||
export function fishing_line_z() -> float { return fishing_bz }
|
||||
export function fishing_marker() -> float { return fishing_mark }
|
||||
export function fishing_hits() -> int { return fishing_won }
|
||||
export function fishing_need() -> int { return fishing_need_n }
|
||||
export function fishing_running() -> bool { return fishing_run_t > 0.0 }
|
||||
export function fishing_gentle() -> bool { return fishing_gentle_on }
|
||||
export function fishing_ring() -> float { return fishing_ring_t }
|
||||
export function fishing_baited() -> bool { return fishing_bait_on }
|
||||
export function fishing_lured() -> bool { return fishing_lure_on }
|
||||
export function fishing_caught() -> int { return fishing_n }
|
||||
export function fishing_biggest() -> int { return fishing_big }
|
||||
export function fishing_legend_caught() -> bool { return fishing_leg }
|
||||
export function fishing_state(fishing_st: FishingState) -> int { return fishing_st.fishing_st }
|
||||
export function fishing_active(fishing_st: FishingState) -> bool { return fishing_st.fishing_st != FISHING_OFF }
|
||||
export function fishing_species(fishing_st: FishingState) -> int { return fishing_st.fishing_sp }
|
||||
export function fishing_cm(fishing_st: FishingState) -> int { return fishing_st.fishing_len }
|
||||
export function fishing_line_x(fishing_st: FishingState) -> float { return fishing_st.fishing_bx }
|
||||
export function fishing_line_z(fishing_st: FishingState) -> float { return fishing_st.fishing_bz }
|
||||
export function fishing_marker(fishing_st: FishingState) -> float { return fishing_st.fishing_mark }
|
||||
export function fishing_hits(fishing_st: FishingState) -> int { return fishing_st.fishing_won }
|
||||
export function fishing_need(fishing_st: FishingState) -> int { return fishing_st.fishing_need_n }
|
||||
export function fishing_running(fishing_st: FishingState) -> bool { return fishing_st.fishing_run_t > 0.0 }
|
||||
export function fishing_gentle(fishing_st: FishingState) -> bool { return fishing_st.fishing_gentle_on }
|
||||
export function fishing_ring(fishing_st: FishingState) -> float { return fishing_st.fishing_ring_t }
|
||||
export function fishing_baited(fishing_st: FishingState) -> bool { return fishing_st.fishing_bait_on }
|
||||
export function fishing_lured(fishing_st: FishingState) -> bool { return fishing_st.fishing_lure_on }
|
||||
export function fishing_caught(fishing_st: FishingState) -> int { return fishing_st.fishing_n }
|
||||
export function fishing_biggest(fishing_st: FishingState) -> int { return fishing_st.fishing_big }
|
||||
export function fishing_legend_caught(fishing_st: FishingState) -> bool { return fishing_st.fishing_leg }
|
||||
|
||||
# can a line land at (x, z)?
|
||||
export function fishing_water_at(x: float, z: float) -> bool {
|
||||
return FishingWorld.afloat() or FishingWorld.depth(x, z) >= fishing_min_depth
|
||||
export function fishing_water_at(fishing_st: FishingState, x: float, z: float) -> bool {
|
||||
return FishingWorld.afloat() or FishingWorld.depth(x, z) >= fishing_st.fishing_min_depth
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +1,35 @@
|
|||
# system.ludic - fishing as a system: its tick, and the record in its own save section (a line in
|
||||
# the water is never saved)
|
||||
export function fishing_reset() -> void {
|
||||
fishing_stop()
|
||||
fishing_n = 0
|
||||
fishing_big = 0
|
||||
fishing_leg = false
|
||||
fishing_dice = rng_new(fishing_seed_n)
|
||||
q_clear(fishing_facts())
|
||||
export function fishing_reset(base_st: mut BaseState, fishing_st: mut FishingState) -> void {
|
||||
fishing_stop(fishing_st)
|
||||
fishing_st.fishing_n = 0
|
||||
fishing_st.fishing_big = 0
|
||||
fishing_st.fishing_leg = false
|
||||
fishing_st.fishing_dice = rng_new(fishing_st.fishing_seed_n)
|
||||
q_clear(base_st, fishing_facts(base_st, fishing_st))
|
||||
}
|
||||
|
||||
# the record outright (staging, a test, the network's word)
|
||||
export function fishing_record_set(caught: int, biggest: int, legend: bool) -> void {
|
||||
fishing_n = caught
|
||||
fishing_big = biggest
|
||||
fishing_leg = legend
|
||||
export function fishing_record_set(fishing_st: mut FishingState, caught: int, biggest: int, legend: bool) -> void {
|
||||
fishing_st.fishing_n = caught
|
||||
fishing_st.fishing_big = biggest
|
||||
fishing_st.fishing_leg = legend
|
||||
}
|
||||
|
||||
export function fishing_save() -> Val {
|
||||
export function fishing_save(fishing_st: FishingState) -> Val {
|
||||
let v = Value.object()
|
||||
sv_put_int(v, "caught", fishing_n)
|
||||
sv_put_int(v, "biggest", fishing_big)
|
||||
sv_put_bool(v, "legend", fishing_leg)
|
||||
sv_put_int(v, "caught", fishing_st.fishing_n)
|
||||
sv_put_int(v, "biggest", fishing_st.fishing_big)
|
||||
sv_put_bool(v, "legend", fishing_st.fishing_leg)
|
||||
return v
|
||||
}
|
||||
|
||||
export function fishing_load(v: Val, version: int) -> void {
|
||||
fishing_reset()
|
||||
fishing_record_set(sv_int(v, "caught", 0), sv_int(v, "biggest", 0), sv_bool(v, "legend", false))
|
||||
export function fishing_load(base_st: mut BaseState, fishing_st: mut FishingState, v: Val, version: int) -> void {
|
||||
fishing_reset(base_st, fishing_st)
|
||||
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 {
|
||||
let s = system_new("fishing", PH_SIMULATE)
|
||||
|
|
|
|||
|
|
@ -10,246 +10,248 @@ program FishingTest {
|
|||
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 }
|
||||
|
||||
var bait: bool = false
|
||||
var lure: bool = false
|
||||
var boat: bool = false
|
||||
var level: int = 0
|
||||
var skill: float = 0.0
|
||||
state FishingtestState {
|
||||
bait: bool = false
|
||||
lure: bool = false
|
||||
boat: bool = false
|
||||
level: int = 0
|
||||
skill: float = 0.0
|
||||
}
|
||||
|
||||
# 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 afloat() -> bool { return boat }
|
||||
function has_bait() -> bool { return bait }
|
||||
function has_lure() -> bool { return lure }
|
||||
function fake_skill() -> float { return skill }
|
||||
function fake_level() -> int { return level }
|
||||
function afloat(fishingtest_st: FishingtestState) -> bool { return fishingtest_st.boat }
|
||||
function has_bait(fishingtest_st: FishingtestState) -> bool { return fishingtest_st.bait }
|
||||
function has_lure(fishingtest_st: FishingtestState) -> bool { return fishingtest_st.lure }
|
||||
function fake_skill(fishingtest_st: FishingtestState) -> float { return fishingtest_st.skill }
|
||||
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 }
|
||||
|
||||
function fresh() -> void {
|
||||
fishing_config(0.15, 2.5, 99)
|
||||
fishing_reset()
|
||||
bait = false
|
||||
lure = false
|
||||
boat = false
|
||||
level = FISHING_RELAXED
|
||||
skill = 0.0
|
||||
function fresh(base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) -> void {
|
||||
fishing_config(fishing_st, 0.15, 2.5, 99)
|
||||
fishing_reset(base_st, fishing_st)
|
||||
fishingtest_st.bait = false
|
||||
fishingtest_st.lure = false
|
||||
fishingtest_st.boat = false
|
||||
fishingtest_st.level = FISHING_RELAXED
|
||||
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 {
|
||||
for i in 0 .. len(fs) { if fs[i].what == what { return true } }
|
||||
return false
|
||||
}
|
||||
|
||||
# run until it bites, then strike
|
||||
function hook() -> void {
|
||||
fishing_set_wait(0.0)
|
||||
fishing_run(0.02)
|
||||
fishing_press()
|
||||
function hook(base_st: mut BaseState, fishing_st: mut FishingState) -> void {
|
||||
fishing_set_wait(fishing_st, 0.0)
|
||||
fishing_run(base_st, fishing_st, 0.02)
|
||||
fishing_press(base_st, fishing_st)
|
||||
}
|
||||
|
||||
# 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 {
|
||||
if fishing_state() != FISHING_REEL { return }
|
||||
if fishing_state(fishing_st) != FISHING_REEL { return }
|
||||
var guard = 0
|
||||
while (fishing_running() or fishing_marker() <= fishing_window_lo() or fishing_marker() >= fishing_window_hi()) and guard < 4000 {
|
||||
fishing_run(0.005)
|
||||
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(base_st, fishing_st, 0.005)
|
||||
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" {
|
||||
fresh()
|
||||
expect_eq(fishing_cast(5.0, 0.0), FISHING_NOT_WATER)
|
||||
expect(not fishing_active())
|
||||
expect_eq(fishing_cast(20.0, 0.0), FISHING_CAST_OK)
|
||||
expect_eq(fishing_state(), FISHING_WAIT)
|
||||
expect_eq(fishing_cast(20.0, 0.0), FISHING_BUSY)
|
||||
boat = true
|
||||
fishing_stop()
|
||||
expect_eq(fishing_cast(0.0, 0.0), FISHING_CAST_OK)
|
||||
test "a cast lands only in water, and never twice" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) {
|
||||
fresh(base_st, fishing_st, fishingtest_st)
|
||||
expect_eq(fishing_cast(base_st, fishing_st, 5.0, 0.0), FISHING_NOT_WATER)
|
||||
expect(not fishing_active(fishing_st))
|
||||
expect_eq(fishing_cast(base_st, fishing_st, 20.0, 0.0), FISHING_CAST_OK)
|
||||
expect_eq(fishing_state(fishing_st), FISHING_WAIT)
|
||||
expect_eq(fishing_cast(base_st, fishing_st, 20.0, 0.0), FISHING_BUSY)
|
||||
fishingtest_st.boat = true
|
||||
fishing_stop(fishing_st)
|
||||
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" {
|
||||
fresh()
|
||||
fishing_cast(20.0, 0.0)
|
||||
expect(not saw(facts(), FISHING_BAIT_USED))
|
||||
test "bait is used and said, and halves the wait" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) {
|
||||
fresh(base_st, fishing_st, fishingtest_st)
|
||||
fishing_cast(base_st, fishing_st, 20.0, 0.0)
|
||||
expect(not saw(facts(base_st, fishing_st), FISHING_BAIT_USED))
|
||||
var plain = 0
|
||||
while fishing_state() == FISHING_WAIT {
|
||||
fishing_run(0.1)
|
||||
while fishing_state(fishing_st) == FISHING_WAIT {
|
||||
fishing_run(base_st, fishing_st, 0.1)
|
||||
plain += 1
|
||||
}
|
||||
expect(plain <= 201)
|
||||
fresh()
|
||||
bait = true
|
||||
fishing_cast(20.0, 0.0)
|
||||
let fs = facts()
|
||||
fresh(base_st, fishing_st, fishingtest_st)
|
||||
fishingtest_st.bait = true
|
||||
fishing_cast(base_st, fishing_st, 20.0, 0.0)
|
||||
let fs = facts(base_st, fishing_st)
|
||||
expect(saw(fs, FISHING_BAIT_USED))
|
||||
expect(fs[0].bait)
|
||||
var baited = 0
|
||||
while fishing_state() == FISHING_WAIT {
|
||||
fishing_run(0.1)
|
||||
while fishing_state(fishing_st) == FISHING_WAIT {
|
||||
fishing_run(base_st, fishing_st, 0.1)
|
||||
baited += 1
|
||||
}
|
||||
expect(baited <= 101)
|
||||
}
|
||||
|
||||
test "a bite missed is gone, a bite struck is hooked" {
|
||||
fresh()
|
||||
fishing_cast(20.0, 0.0)
|
||||
fishing_set_wait(0.0)
|
||||
fishing_run(0.02)
|
||||
expect_eq(fishing_state(), FISHING_BITE_ON)
|
||||
expect(fishing_species() >= 0)
|
||||
fishing_run(2.0)
|
||||
expect(not fishing_active())
|
||||
expect(saw(facts(), FISHING_MISSED))
|
||||
fishing_cast(20.0, 0.0)
|
||||
hook()
|
||||
expect_eq(fishing_state(), FISHING_REEL)
|
||||
expect(saw(facts(), FISHING_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(base_st, fishing_st, fishingtest_st)
|
||||
fishing_cast(base_st, fishing_st, 20.0, 0.0)
|
||||
fishing_set_wait(fishing_st, 0.0)
|
||||
fishing_run(base_st, fishing_st, 0.02)
|
||||
expect_eq(fishing_state(fishing_st), FISHING_BITE_ON)
|
||||
expect(fishing_species(fishing_st) >= 0)
|
||||
fishing_run(base_st, fishing_st, 2.0)
|
||||
expect(not fishing_active(fishing_st))
|
||||
expect(saw(facts(base_st, fishing_st), FISHING_MISSED))
|
||||
fishing_cast(base_st, fishing_st, 20.0, 0.0)
|
||||
hook(base_st, fishing_st)
|
||||
expect_eq(fishing_state(fishing_st), FISHING_REEL)
|
||||
expect(saw(facts(base_st, fishing_st), FISHING_HOOKED))
|
||||
}
|
||||
|
||||
test "everyday fish only without a lure, sized and weighed from the data" {
|
||||
fresh()
|
||||
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(base_st, fishing_st, fishingtest_st)
|
||||
for i in 0 .. 40 {
|
||||
fishing_cast(20.0, 0.0)
|
||||
hook()
|
||||
let s = fishing_species()
|
||||
fishing_cast(base_st, fishing_st, 20.0, 0.0)
|
||||
hook(base_st, fishing_st)
|
||||
let s = fishing_species(fishing_st)
|
||||
expect(s != FISH_OLD_MAROON)
|
||||
let k = FishSpecies[s]
|
||||
expect(fishing_cm() >= k.cm_min and fishing_cm() <= k.cm_min + k.cm_spread)
|
||||
fishing_stop()
|
||||
expect(fishing_cm(fishing_st) >= k.cm_min and fishing_cm(fishing_st) <= k.cm_min + k.cm_spread)
|
||||
fishing_stop(fishing_st)
|
||||
}
|
||||
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" {
|
||||
fresh()
|
||||
fishing_cast(20.0, 0.0)
|
||||
hook()
|
||||
facts()
|
||||
play(3)
|
||||
let fs = facts()
|
||||
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(base_st, fishing_st, fishingtest_st)
|
||||
fishing_cast(base_st, fishing_st, 20.0, 0.0)
|
||||
hook(base_st, fishing_st)
|
||||
facts(base_st, fishing_st)
|
||||
play(base_st, fishing_st, 3)
|
||||
let fs = facts(base_st, fishing_st)
|
||||
expect(saw(fs, FISHING_CAUGHT))
|
||||
expect_eq(fishing_caught(), 1)
|
||||
expect(fishing_biggest() > 0)
|
||||
expect(not fishing_active())
|
||||
expect_eq(fishing_caught(fishing_st), 1)
|
||||
expect(fishing_biggest(fishing_st) > 0)
|
||||
expect(not fishing_active(fishing_st))
|
||||
let last = fs[len(fs) - 1]
|
||||
expect(last.kg > 0.0)
|
||||
expect(last.x == 20.0)
|
||||
}
|
||||
|
||||
test "a press outside the green throws the hook" {
|
||||
fresh()
|
||||
fishing_cast(20.0, 0.0)
|
||||
hook()
|
||||
while fishing_marker() < fishing_window_hi() + 0.01 { fishing_run(0.005) }
|
||||
fishing_press()
|
||||
expect(saw(facts(), FISHING_LINE_SNAPPED))
|
||||
expect(not fishing_active())
|
||||
test "a press outside the green throws the hook" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) {
|
||||
fresh(base_st, fishing_st, fishingtest_st)
|
||||
fishing_cast(base_st, fishing_st, 20.0, 0.0)
|
||||
hook(base_st, fishing_st)
|
||||
while fishing_marker(fishing_st) < fishing_window_hi(fishing_st) + 0.01 { fishing_run(base_st, fishing_st, 0.005) }
|
||||
fishing_press(base_st, fishing_st)
|
||||
expect(saw(facts(base_st, fishing_st), FISHING_LINE_SNAPPED))
|
||||
expect(not fishing_active(fishing_st))
|
||||
}
|
||||
|
||||
test "a fight left alone goes slack" {
|
||||
fresh()
|
||||
fishing_cast(20.0, 0.0)
|
||||
hook()
|
||||
for i in 0 .. 700 { fishing_run(0.02) }
|
||||
expect(saw(facts(), FISHING_SLACK))
|
||||
expect_eq(fishing_caught(), 0)
|
||||
test "a fight left alone goes slack" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) {
|
||||
fresh(base_st, fishing_st, fishingtest_st)
|
||||
fishing_cast(base_st, fishing_st, 20.0, 0.0)
|
||||
hook(base_st, fishing_st)
|
||||
for i in 0 .. 700 { fishing_run(base_st, fishing_st, 0.02) }
|
||||
expect(saw(facts(base_st, fishing_st), FISHING_SLACK))
|
||||
expect_eq(fishing_caught(fishing_st), 0)
|
||||
}
|
||||
|
||||
test "a legend takes the lure, runs, turns, and is landed at five" {
|
||||
fresh()
|
||||
lure = true
|
||||
fishing_cast(20.0, 0.0)
|
||||
hook()
|
||||
expect_eq(fishing_species(), FISH_OLD_MAROON)
|
||||
expect_eq(fishing_need(), 5)
|
||||
let w0 = fishing_window()
|
||||
play(2)
|
||||
expect(fishing_window() < w0)
|
||||
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(base_st, fishing_st, fishingtest_st)
|
||||
fishingtest_st.lure = true
|
||||
fishing_cast(base_st, fishing_st, 20.0, 0.0)
|
||||
hook(base_st, fishing_st)
|
||||
expect_eq(fishing_species(fishing_st), FISH_OLD_MAROON)
|
||||
expect_eq(fishing_need(fishing_st), 5)
|
||||
let w0 = fishing_window(fishing_st)
|
||||
play(base_st, fishing_st, 2)
|
||||
expect(fishing_window(fishing_st) < w0)
|
||||
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))
|
||||
if fishing_running() {
|
||||
let before = fishing_hits()
|
||||
fishing_press()
|
||||
expect_eq(fishing_hits(), before - 1)
|
||||
if fishing_running(fishing_st) {
|
||||
let before = fishing_hits(fishing_st)
|
||||
fishing_press(base_st, fishing_st)
|
||||
expect_eq(fishing_hits(fishing_st), before - 1)
|
||||
}
|
||||
play(40)
|
||||
expect(fishing_legend_caught())
|
||||
let last = facts()
|
||||
play(base_st, fishing_st, 40)
|
||||
expect(fishing_legend_caught(fishing_st))
|
||||
let last = facts(base_st, fishing_st)
|
||||
expect(last[len(last) - 1].lure)
|
||||
}
|
||||
|
||||
test "gentle: one press inside the ring, and the ring closes" {
|
||||
fresh()
|
||||
level = FISHING_GENTLE
|
||||
fishing_cast(20.0, 0.0)
|
||||
hook()
|
||||
expect(fishing_gentle())
|
||||
expect_eq(fishing_need(), 1)
|
||||
fishing_press()
|
||||
expect_eq(fishing_caught(), 1)
|
||||
fishing_cast(20.0, 0.0)
|
||||
hook()
|
||||
fishing_run(2.1)
|
||||
expect(saw(facts(), FISHING_LINE_SNAPPED))
|
||||
test "gentle: one press inside the ring, and the ring closes" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) {
|
||||
fresh(base_st, fishing_st, fishingtest_st)
|
||||
fishingtest_st.level = FISHING_GENTLE
|
||||
fishing_cast(base_st, fishing_st, 20.0, 0.0)
|
||||
hook(base_st, fishing_st)
|
||||
expect(fishing_gentle(fishing_st))
|
||||
expect_eq(fishing_need(fishing_st), 1)
|
||||
fishing_press(base_st, fishing_st)
|
||||
expect_eq(fishing_caught(fishing_st), 1)
|
||||
fishing_cast(base_st, fishing_st, 20.0, 0.0)
|
||||
hook(base_st, fishing_st)
|
||||
fishing_run(base_st, fishing_st, 2.1)
|
||||
expect(saw(facts(base_st, fishing_st), FISHING_LINE_SNAPPED))
|
||||
}
|
||||
|
||||
test "a legend fights on gentle too" {
|
||||
fresh()
|
||||
level = FISHING_GENTLE
|
||||
lure = true
|
||||
fishing_cast(20.0, 0.0)
|
||||
hook()
|
||||
expect(not fishing_gentle())
|
||||
expect_eq(fishing_need(), 5)
|
||||
test "a legend fights on gentle too" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) {
|
||||
fresh(base_st, fishing_st, fishingtest_st)
|
||||
fishingtest_st.level = FISHING_GENTLE
|
||||
fishingtest_st.lure = true
|
||||
fishing_cast(base_st, fishing_st, 20.0, 0.0)
|
||||
hook(base_st, fishing_st)
|
||||
expect(not fishing_gentle(fishing_st))
|
||||
expect_eq(fishing_need(fishing_st), 5)
|
||||
}
|
||||
|
||||
test "hard closes the window for everyday fish, relaxed keeps it centred" {
|
||||
fresh()
|
||||
fishing_cast(20.0, 0.0)
|
||||
hook()
|
||||
expect(fishing_window_lo() + fishing_window_hi() > 0.999 and fishing_window_lo() + fishing_window_hi() < 1.001)
|
||||
fishing_stop()
|
||||
level = FISHING_HARD
|
||||
fishing_cast(20.0, 0.0)
|
||||
hook()
|
||||
let w0 = fishing_window()
|
||||
play(1)
|
||||
expect(fishing_window() < w0)
|
||||
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(base_st, fishing_st, fishingtest_st)
|
||||
fishing_cast(base_st, fishing_st, 20.0, 0.0)
|
||||
hook(base_st, fishing_st)
|
||||
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_st)
|
||||
fishingtest_st.level = FISHING_HARD
|
||||
fishing_cast(base_st, fishing_st, 20.0, 0.0)
|
||||
hook(base_st, fishing_st)
|
||||
let w0 = fishing_window(fishing_st)
|
||||
play(base_st, fishing_st, 1)
|
||||
expect(fishing_window(fishing_st) < w0)
|
||||
}
|
||||
|
||||
test "skill makes a fish bigger" {
|
||||
fresh()
|
||||
skill = 1.0
|
||||
fishing_cast(20.0, 0.0)
|
||||
hook()
|
||||
let k = FishSpecies[fishing_species()]
|
||||
expect(fishing_cm() >= k.cm_min + 60)
|
||||
test "skill makes a fish bigger" (base_st: mut BaseState, fishing_st: mut FishingState, fishingtest_st: mut FishingtestState) {
|
||||
fresh(base_st, fishing_st, fishingtest_st)
|
||||
fishingtest_st.skill = 1.0
|
||||
fishing_cast(base_st, fishing_st, 20.0, 0.0)
|
||||
hook(base_st, fishing_st)
|
||||
let k = FishSpecies[fishing_species(fishing_st)]
|
||||
expect(fishing_cm(fishing_st) >= k.cm_min + 60)
|
||||
}
|
||||
|
||||
test "the record is saved, and a line in the water is not" {
|
||||
fresh()
|
||||
fishing_record_set(7, 52, true)
|
||||
fishing_cast(20.0, 0.0)
|
||||
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(base_st, fishing_st, fishingtest_st)
|
||||
fishing_record_set(fishing_st, 7, 52, true)
|
||||
fishing_cast(base_st, fishing_st, 20.0, 0.0)
|
||||
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)
|
||||
fishing_reset()
|
||||
fishing_reset(base_st, fishing_st)
|
||||
let n = load_section(save_decode(text), "fishing")
|
||||
fishing_load(n.data, n.version)
|
||||
expect_eq(fishing_caught(), 7)
|
||||
expect_eq(fishing_biggest(), 52)
|
||||
expect(fishing_legend_caught())
|
||||
expect(not fishing_active())
|
||||
fishing_load(base_st, fishing_st, n.data, n.version)
|
||||
expect_eq(fishing_caught(fishing_st), 7)
|
||||
expect_eq(fishing_biggest(fishing_st), 52)
|
||||
expect(fishing_legend_caught(fishing_st))
|
||||
expect(not fishing_active(fishing_st))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,102 +4,102 @@ export const FISHING_NOT_WATER: int = 1
|
|||
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)
|
||||
export function fishing_cast(x: float, z: float) -> int {
|
||||
if fishing_st != FISHING_OFF { return FISHING_BUSY }
|
||||
fishing_bx = x
|
||||
fishing_bz = z
|
||||
if not fishing_water_at(x, z) { return FISHING_NOT_WATER }
|
||||
fishing_st = FISHING_WAIT
|
||||
fishing_sp = -1
|
||||
fishing_len = 0
|
||||
fishing_lure_on = FishingWorld.has_lure()
|
||||
fishing_bait_on = FishingWorld.has_bait()
|
||||
var wait = 6.0 + fishing_rand() * 14.0
|
||||
if fishing_bait_on { wait = wait * 0.5 }
|
||||
fishing_t = wait * FishingWorld.bite_mult()
|
||||
if fishing_bait_on { fishing_say(FISHING_BAIT_USED) }
|
||||
fishing_say(FISHING_CAST)
|
||||
export function fishing_cast(base_st: mut BaseState, fishing_st: mut FishingState, x: float, z: float) -> int {
|
||||
if fishing_st.fishing_st != FISHING_OFF { return FISHING_BUSY }
|
||||
fishing_st.fishing_bx = x
|
||||
fishing_st.fishing_bz = z
|
||||
if not fishing_water_at(fishing_st, x, z) { return FISHING_NOT_WATER }
|
||||
fishing_st.fishing_st = FISHING_WAIT
|
||||
fishing_st.fishing_sp = -1
|
||||
fishing_st.fishing_len = 0
|
||||
fishing_st.fishing_lure_on = FishingWorld.has_lure()
|
||||
fishing_st.fishing_bait_on = FishingWorld.has_bait()
|
||||
var wait = 6.0 + fishing_rand(fishing_st) * 14.0
|
||||
if fishing_st.fishing_bait_on { wait = wait * 0.5 }
|
||||
fishing_st.fishing_t = wait * FishingWorld.bite_mult()
|
||||
if fishing_st.fishing_bait_on { fishing_say(base_st, fishing_st, FISHING_BAIT_USED) }
|
||||
fishing_say(base_st, fishing_st, FISHING_CAST)
|
||||
return FISHING_CAST_OK
|
||||
}
|
||||
|
||||
# the bite comes exactly `secs` after now (staging, footage)
|
||||
export function fishing_set_wait(secs: float) -> void {
|
||||
if fishing_st == FISHING_WAIT { fishing_t = secs }
|
||||
export function fishing_set_wait(fishing_st: mut FishingState, secs: float) -> void {
|
||||
if fishing_st.fishing_st == FISHING_WAIT { fishing_st.fishing_t = secs }
|
||||
}
|
||||
|
||||
export function fishing_stop() -> void {
|
||||
fishing_st = FISHING_OFF
|
||||
fishing_run_t = 0.0
|
||||
export function fishing_stop(fishing_st: mut FishingState) -> void {
|
||||
fishing_st.fishing_st = FISHING_OFF
|
||||
fishing_st.fishing_run_t = 0.0
|
||||
}
|
||||
|
||||
function fishing_strike() -> void {
|
||||
let k = FishSpecies[fishing_sp]
|
||||
fishing_st = FISHING_REEL
|
||||
fishing_won = 0
|
||||
fishing_mark = 0.0
|
||||
fishing_dir = 1.0
|
||||
fishing_t = k.fight
|
||||
fishing_need_n = k.need
|
||||
fishing_run_t = 0.0
|
||||
fishing_run_left = k.runs
|
||||
fishing_run_next = 3.0 + fishing_rand() * 3.0
|
||||
fishing_len = fishing_size(fishing_sp)
|
||||
fishing_gentle_on = FishingWorld.level() == FISHING_GENTLE and not k.legend
|
||||
fishing_ring_t = 0.0
|
||||
if fishing_gentle_on {
|
||||
fishing_need_n = 1
|
||||
fishing_ring_t = 2.0
|
||||
function fishing_strike(base_st: mut BaseState, fishing_st: mut FishingState) -> void {
|
||||
let k = FishSpecies[fishing_st.fishing_sp]
|
||||
fishing_st.fishing_st = FISHING_REEL
|
||||
fishing_st.fishing_won = 0
|
||||
fishing_st.fishing_mark = 0.0
|
||||
fishing_st.fishing_dir = 1.0
|
||||
fishing_st.fishing_t = k.fight
|
||||
fishing_st.fishing_need_n = k.need
|
||||
fishing_st.fishing_run_t = 0.0
|
||||
fishing_st.fishing_run_left = k.runs
|
||||
fishing_st.fishing_run_next = 3.0 + fishing_rand(fishing_st) * 3.0
|
||||
fishing_st.fishing_len = fishing_size(fishing_st, fishing_st.fishing_sp)
|
||||
fishing_st.fishing_gentle_on = FishingWorld.level() == FISHING_GENTLE and not k.legend
|
||||
fishing_st.fishing_ring_t = 0.0
|
||||
if fishing_st.fishing_gentle_on {
|
||||
fishing_st.fishing_need_n = 1
|
||||
fishing_st.fishing_ring_t = 2.0
|
||||
}
|
||||
fishing_new_window()
|
||||
fishing_say(FISHING_HOOKED)
|
||||
fishing_new_window(fishing_st)
|
||||
fishing_say(base_st, fishing_st, FISHING_HOOKED)
|
||||
}
|
||||
|
||||
function fishing_land() -> void {
|
||||
fishing_n += 1
|
||||
if fishing_len > fishing_big { fishing_big = fishing_len }
|
||||
if FishSpecies[fishing_sp].legend { fishing_leg = true }
|
||||
fishing_say(FISHING_CAUGHT)
|
||||
fishing_stop()
|
||||
function fishing_land(base_st: mut BaseState, fishing_st: mut FishingState) -> void {
|
||||
fishing_st.fishing_n += 1
|
||||
if fishing_st.fishing_len > fishing_st.fishing_big { fishing_st.fishing_big = fishing_st.fishing_len }
|
||||
if FishSpecies[fishing_st.fishing_sp].legend { fishing_st.fishing_leg = true }
|
||||
fishing_say(base_st, fishing_st, FISHING_CAUGHT)
|
||||
fishing_stop(fishing_st)
|
||||
}
|
||||
|
||||
function fishing_lost(what: int) -> void {
|
||||
fishing_say(what)
|
||||
fishing_stop()
|
||||
function fishing_lost(base_st: mut BaseState, fishing_st: mut FishingState, what: int) -> void {
|
||||
fishing_say(base_st, fishing_st, what)
|
||||
fishing_stop(fishing_st)
|
||||
}
|
||||
|
||||
function fishing_reel_press() -> void {
|
||||
if fishing_gentle_on {
|
||||
if fishing_ring_t > 0.0 { fishing_land() } else { fishing_lost(FISHING_LINE_SNAPPED) }
|
||||
function fishing_reel_press(base_st: mut BaseState, fishing_st: mut FishingState) -> void {
|
||||
if fishing_st.fishing_gentle_on {
|
||||
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
|
||||
}
|
||||
if fishing_run_t > 0.0 {
|
||||
fishing_won = Math.max(fishing_won - 1, 0)
|
||||
fishing_say(FISHING_CHECKED)
|
||||
if fishing_st.fishing_run_t > 0.0 {
|
||||
fishing_st.fishing_won = Math.max(fishing_st.fishing_won - 1, 0)
|
||||
fishing_say(base_st, fishing_st, FISHING_CHECKED)
|
||||
return
|
||||
}
|
||||
if fishing_in_window() {
|
||||
fishing_won += 1
|
||||
fishing_new_window()
|
||||
if fishing_won >= fishing_need_n { fishing_land() } else { fishing_say(FISHING_GOOD) }
|
||||
if fishing_in_window(fishing_st) {
|
||||
fishing_st.fishing_won += 1
|
||||
fishing_new_window(fishing_st)
|
||||
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
|
||||
}
|
||||
if FishSpecies[fishing_sp].legend and fishing_won > 0 {
|
||||
fishing_won -= 1
|
||||
fishing_say(FISHING_TURNED)
|
||||
if FishSpecies[fishing_st.fishing_sp].legend and fishing_st.fishing_won > 0 {
|
||||
fishing_st.fishing_won -= 1
|
||||
fishing_say(base_st, fishing_st, FISHING_TURNED)
|
||||
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
|
||||
export function fishing_press() -> void {
|
||||
if fishing_st == FISHING_WAIT {
|
||||
fishing_lost(FISHING_REELED_IN)
|
||||
export function fishing_press(base_st: mut BaseState, fishing_st: mut FishingState) -> void {
|
||||
if fishing_st.fishing_st == FISHING_WAIT {
|
||||
fishing_lost(base_st, fishing_st, FISHING_REELED_IN)
|
||||
return
|
||||
}
|
||||
if fishing_st == FISHING_BITE_ON {
|
||||
fishing_strike()
|
||||
if fishing_st.fishing_st == FISHING_BITE_ON {
|
||||
fishing_strike(base_st, fishing_st)
|
||||
return
|
||||
}
|
||||
if fishing_st == FISHING_REEL { fishing_reel_press() }
|
||||
if fishing_st.fishing_st == FISHING_REEL { fishing_reel_press(base_st, fishing_st) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
# 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 {
|
||||
if not gear_ok(k) or gear_best(k) or GearWorld.hidden(k) { return false }
|
||||
return GearPurse.rep() >= gear_next_rep(k) and GearPurse.money() >= gear_next_cost(k)
|
||||
export function gear_can_buy(gear_st: mut GearState, k: int) -> bool {
|
||||
if not gear_ok(k) or gear_best(gear_st, k) or GearWorld.hidden(k) { return false }
|
||||
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 {
|
||||
if not gear_can_buy(k) { return false }
|
||||
let cost = gear_next_cost(k)
|
||||
export function gear_buy(base_st: mut BaseState, gear_st: mut GearState, k: int) -> bool {
|
||||
if not gear_can_buy(gear_st, k) { return false }
|
||||
let cost = gear_next_cost(gear_st, k)
|
||||
if not GearPurse.spend(cost) { return false }
|
||||
gear__lv[k] += 1
|
||||
gear_st.gear__lv[k] += 1
|
||||
let it = GearKinds[k].item
|
||||
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
|
||||
}
|
||||
|
||||
# a kind whose item is already carried counts as its first tier
|
||||
export function gear_sync() -> void {
|
||||
gear__ensure()
|
||||
export function gear_sync(gear_st: mut GearState) -> void {
|
||||
gear__ensure(gear_st)
|
||||
for k in 0 .. GEAR_COUNT {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
# 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
|
||||
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
|
||||
export function gear_set_hand(it: int) -> void {
|
||||
gear__ensure()
|
||||
gear__hand = it
|
||||
for c in 0 .. CHARGE_COUNT { if GearCharges[c].hand { gear__on[c] = false } }
|
||||
export function gear_set_hand(gear_st: mut GearState, it: int) -> void {
|
||||
gear__ensure(gear_st)
|
||||
gear_st.gear__hand = it
|
||||
for c in 0 .. CHARGE_COUNT { if GearCharges[c].hand { gear_st.gear__on[c] = false } }
|
||||
}
|
||||
|
||||
# the charge an item carries, -1 none
|
||||
|
|
@ -15,58 +15,58 @@ export function gear_charge_of(it: int) -> int {
|
|||
return -1
|
||||
}
|
||||
|
||||
export function gear_charge(c: int) -> float {
|
||||
gear__ensure()
|
||||
return gear__left[c]
|
||||
export function gear_charge(gear_st: mut GearState, c: int) -> float {
|
||||
gear__ensure(gear_st)
|
||||
return gear_st.gear__left[c]
|
||||
}
|
||||
|
||||
export function gear_charge_set(c: int, v: float) -> void {
|
||||
gear__ensure()
|
||||
gear__left[c] = Math.clamp(v, 0.0, GearCharges[c].full)
|
||||
export function gear_charge_set(gear_st: mut GearState, c: int, v: float) -> void {
|
||||
gear__ensure(gear_st)
|
||||
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 {
|
||||
gear__ensure()
|
||||
return gear__on[c]
|
||||
export function gear_running(gear_st: mut GearState, c: int) -> bool {
|
||||
gear__ensure(gear_st)
|
||||
return gear_st.gear__on[c]
|
||||
}
|
||||
|
||||
# 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 {
|
||||
gear__ensure()
|
||||
export function gear_run(gear_st: mut GearState, c: int, on: bool) -> bool {
|
||||
gear__ensure(gear_st)
|
||||
if not on {
|
||||
gear__on[c] = false
|
||||
gear_st.gear__on[c] = false
|
||||
return true
|
||||
}
|
||||
if gear__left[c] < 1.0 { return false }
|
||||
if GearCharges[c].hand and GearCharges[c].item != gear__hand { return false }
|
||||
gear__on[c] = true
|
||||
if gear_st.gear__left[c] < 1.0 { return false }
|
||||
if GearCharges[c].hand and GearCharges[c].item != gear_st.gear__hand { return false }
|
||||
gear_st.gear__on[c] = true
|
||||
return true
|
||||
}
|
||||
|
||||
# the hand's own charge is running (a torch lit, a flashlight on)
|
||||
export function gear_lit() -> bool {
|
||||
if gear__hand < 0 { return false }
|
||||
let c = gear_charge_of(gear__hand)
|
||||
return c >= 0 and GearCharges[c].hand and gear_running(c)
|
||||
export function gear_lit(gear_st: mut GearState) -> bool {
|
||||
if gear_st.gear__hand < 0 { return false }
|
||||
let c = gear_charge_of(gear_st.gear__hand)
|
||||
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]
|
||||
if d.rate >= 0 { return gear_value(d.rate) }
|
||||
if d.rate >= 0 { return gear_value(gear_st, d.rate) }
|
||||
return d.per_hour
|
||||
}
|
||||
|
||||
# the game hours of a tick burn every running charge; one that runs out stops and says so
|
||||
function gear__burn(hours: float) -> void {
|
||||
gear__ensure()
|
||||
function gear__burn(base_st: mut BaseState, gear_st: mut GearState, hours: float) -> void {
|
||||
gear__ensure(gear_st)
|
||||
for c in 0 .. CHARGE_COUNT {
|
||||
if not gear__on[c] { continue }
|
||||
gear__left[c] = gear__left[c] - hours * gear__rate(c)
|
||||
if gear__left[c] > 0.0 { continue }
|
||||
gear__left[c] = 0.0
|
||||
gear__on[c] = false
|
||||
gear__fact(GEAR_RAN_OUT, -1, c, 0)
|
||||
if not gear_st.gear__on[c] { continue }
|
||||
gear_st.gear__left[c] = gear_st.gear__left[c] - hours * gear__rate(gear_st, c)
|
||||
if gear_st.gear__left[c] > 0.0 { continue }
|
||||
gear_st.gear__left[c] = 0.0
|
||||
gear_st.gear__on[c] = false
|
||||
gear__fact(base_st, gear_st, GEAR_RAN_OUT, -1, c, 0)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,39 @@
|
|||
# state.ludic - the level of each kind, the hand, each charge's units and whether it runs, the facts
|
||||
var gear__lv: []int = null
|
||||
var gear__hand: int = -1
|
||||
var gear__left: []float = null
|
||||
var gear__on: []bool = null
|
||||
var gear__q: Queue<GearFact> = null
|
||||
export state GearState {
|
||||
gear__lv: []int = null
|
||||
gear__hand: int = -1
|
||||
gear__left: []float = null
|
||||
gear__on: []bool = null
|
||||
gear__q: Queue<GearFact> = null
|
||||
}
|
||||
|
||||
function gear__ensure() -> void {
|
||||
if gear__lv != null { return }
|
||||
gear__lv = new []int
|
||||
for k in 0 .. GEAR_COUNT { push(gear__lv, GearKinds[k].start) }
|
||||
gear__left = new []float
|
||||
gear__on = new []bool
|
||||
function gear__ensure(gear_st: mut GearState) -> void {
|
||||
if gear_st.gear__lv != null { return }
|
||||
gear_st.gear__lv = new []int
|
||||
for k in 0 .. GEAR_COUNT { push(gear_st.gear__lv, GearKinds[k].start) }
|
||||
gear_st.gear__left = new []float
|
||||
gear_st.gear__on = new []bool
|
||||
for c in 0 .. CHARGE_COUNT {
|
||||
push(gear__left, GearCharges[c].full)
|
||||
push(gear__on, false)
|
||||
push(gear_st.gear__left, GearCharges[c].full)
|
||||
push(gear_st.gear__on, false)
|
||||
}
|
||||
}
|
||||
|
||||
export function gear_facts() -> Queue<GearFact> {
|
||||
if gear__q == null { gear__q = queue_new("gear.facts") }
|
||||
return gear__q
|
||||
export function gear_facts(base_st: mut BaseState, gear_st: mut GearState) -> Queue<GearFact> {
|
||||
if gear_st.gear__q == null { gear_st.gear__q = queue_new(base_st, "gear.facts") }
|
||||
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
|
||||
f.what = what
|
||||
f.cost = cost
|
||||
f.kind = k
|
||||
f.charge = c
|
||||
if k >= 0 {
|
||||
f.level = gear__lv[k]
|
||||
f.level = gear_st.gear__lv[k]
|
||||
f.item = GearKinds[k].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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,43 @@
|
|||
# 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)
|
||||
export function gear_reset() -> void {
|
||||
gear__ensure()
|
||||
for k in 0 .. GEAR_COUNT { gear__lv[k] = GearKinds[k].start }
|
||||
export function gear_reset(base_st: mut BaseState, gear_st: mut GearState) -> void {
|
||||
gear__ensure(gear_st)
|
||||
for k in 0 .. GEAR_COUNT { gear_st.gear__lv[k] = GearKinds[k].start }
|
||||
for c in 0 .. CHARGE_COUNT {
|
||||
gear__left[c] = GearCharges[c].full
|
||||
gear__on[c] = false
|
||||
gear_st.gear__left[c] = GearCharges[c].full
|
||||
gear_st.gear__on[c] = false
|
||||
}
|
||||
gear__hand = -1
|
||||
q_clear(gear_facts())
|
||||
gear_st.gear__hand = -1
|
||||
q_clear(base_st, gear_facts(base_st, gear_st))
|
||||
}
|
||||
|
||||
export function gear_save() -> Val {
|
||||
gear__ensure()
|
||||
export function gear_save(gear_st: mut GearState) -> Val {
|
||||
gear__ensure(gear_st)
|
||||
let v = 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)
|
||||
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)
|
||||
return v
|
||||
}
|
||||
|
||||
# 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 {
|
||||
gear_reset()
|
||||
export function gear_load(base_st: mut BaseState, gear_st: mut GearState, v: Val, version: int) -> void {
|
||||
gear_reset(base_st, gear_st)
|
||||
if Value.has(v, "levels") != 0 {
|
||||
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 {
|
||||
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 {
|
||||
let s = system_new("gear", PH_SIMULATE)
|
||||
|
|
|
|||
|
|
@ -17,147 +17,149 @@ program GearTest {
|
|||
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 }
|
||||
|
||||
var money: int = 0
|
||||
var rep: int = 0
|
||||
var pack: []int = null
|
||||
var hide_pack: bool = false
|
||||
state GeartestState {
|
||||
money: int = 0
|
||||
rep: int = 0
|
||||
pack: []int = null
|
||||
hide_pack: bool = false
|
||||
}
|
||||
|
||||
function purse_money() -> int { return money }
|
||||
function purse_spend(n: int) -> bool {
|
||||
if n > money { return false }
|
||||
money -= n
|
||||
function purse_money(geartest_st: GeartestState) -> int { return geartest_st.money }
|
||||
function purse_spend(geartest_st: mut GeartestState, n: int) -> bool {
|
||||
if n > geartest_st.money { return false }
|
||||
geartest_st.money -= n
|
||||
return true
|
||||
}
|
||||
function purse_rep() -> int { return rep }
|
||||
function pack_count(it: int) -> int { return pack[it] }
|
||||
function pack_add(it: int, n: int) -> int {
|
||||
pack[it] = pack[it] + n
|
||||
function purse_rep(geartest_st: GeartestState) -> int { return geartest_st.rep }
|
||||
function pack_count(geartest_st: GeartestState, it: int) -> int { return geartest_st.pack[it] }
|
||||
function pack_add(geartest_st: mut GeartestState, it: int, n: int) -> int {
|
||||
geartest_st.pack[it] = geartest_st.pack[it] + 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 GearPack { count: fn pack_count, add: fn pack_add }
|
||||
bind GearWorld { hidden: fn hidden }
|
||||
|
||||
function fresh() -> void {
|
||||
money = 0
|
||||
rep = 0
|
||||
hide_pack = false
|
||||
pack = new []int
|
||||
for i in 0 .. 12 { push(pack, 0) }
|
||||
gear_reset()
|
||||
function fresh(base_st: mut BaseState, gear_st: mut GearState, geartest_st: mut GeartestState) -> void {
|
||||
geartest_st.money = 0
|
||||
geartest_st.rep = 0
|
||||
geartest_st.hide_pack = false
|
||||
geartest_st.pack = new []int
|
||||
for i in 0 .. 12 { push(geartest_st.pack, 0) }
|
||||
gear_reset(base_st, gear_st)
|
||||
}
|
||||
|
||||
function ran_out() -> int {
|
||||
let fs = q_drain(gear_facts())
|
||||
function ran_out(base_st: mut BaseState, gear_st: mut GearState) -> int {
|
||||
let fs = q_drain(base_st, gear_facts(base_st, gear_st))
|
||||
var n = -1
|
||||
for i in 0 .. len(fs) { if fs[i].what == GEAR_RAN_OUT { n = fs[i].charge } }
|
||||
return n
|
||||
}
|
||||
|
||||
test "a new trip starts where the kinds say" {
|
||||
fresh()
|
||||
expect_eq(gear_level(GEAR_ROD), 0)
|
||||
expect_eq(gear_level(GEAR_CAMERA), 1)
|
||||
expect(gear_next_name(GEAR_CAMERA) == "Zoom")
|
||||
expect_eq(gear_next_cost(GEAR_ROD), 30)
|
||||
test "a new trip starts where the kinds say" (base_st: mut BaseState, gear_st: mut GearState, geartest_st: mut GeartestState) {
|
||||
fresh(base_st, gear_st, geartest_st)
|
||||
expect_eq(gear_level(gear_st, GEAR_ROD), 0)
|
||||
expect_eq(gear_level(gear_st, GEAR_CAMERA), 1)
|
||||
expect(gear_next_name(gear_st, GEAR_CAMERA) == "Zoom")
|
||||
expect_eq(gear_next_cost(gear_st, GEAR_ROD), 30)
|
||||
}
|
||||
|
||||
test "bought with money once the standing is there, and the item follows" {
|
||||
fresh()
|
||||
expect(not gear_buy(GEAR_ROD))
|
||||
money = 200
|
||||
expect(gear_buy(GEAR_ROD))
|
||||
expect_eq(money, 170)
|
||||
expect_eq(pack[IT_ROD], 1)
|
||||
expect(not gear_can_buy(GEAR_ROD))
|
||||
rep = 10
|
||||
expect(gear_buy(GEAR_ROD))
|
||||
expect_eq(pack[IT_ROD], 1)
|
||||
let fs = q_drain(gear_facts())
|
||||
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(base_st, gear_st, geartest_st)
|
||||
expect(not gear_buy(base_st, gear_st, GEAR_ROD))
|
||||
geartest_st.money = 200
|
||||
expect(gear_buy(base_st, gear_st, GEAR_ROD))
|
||||
expect_eq(geartest_st.money, 170)
|
||||
expect_eq(geartest_st.pack[IT_ROD], 1)
|
||||
expect(not gear_can_buy(gear_st, GEAR_ROD))
|
||||
geartest_st.rep = 10
|
||||
expect(gear_buy(base_st, gear_st, GEAR_ROD))
|
||||
expect_eq(geartest_st.pack[IT_ROD], 1)
|
||||
let fs = q_drain(base_st, gear_facts(base_st, gear_st))
|
||||
expect_eq(len(fs), 2)
|
||||
expect_eq(fs[1].what, GEAR_BOUGHT)
|
||||
expect_eq(fs[1].level, 2)
|
||||
expect_eq(fs[1].cost, 100)
|
||||
}
|
||||
|
||||
test "the best tier is the end, and a hidden kind is not for sale" {
|
||||
fresh()
|
||||
money = 1000
|
||||
rep = 50
|
||||
for i in 0 .. 5 { gear_buy(GEAR_PACK) }
|
||||
expect(gear_best(GEAR_PACK))
|
||||
expect_eq(gear_level(GEAR_PACK), 3)
|
||||
fresh()
|
||||
money = 100
|
||||
hide_pack = true
|
||||
expect(not gear_buy(GEAR_PACK))
|
||||
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(base_st, gear_st, geartest_st)
|
||||
geartest_st.money = 1000
|
||||
geartest_st.rep = 50
|
||||
for i in 0 .. 5 { gear_buy(base_st, gear_st, GEAR_PACK) }
|
||||
expect(gear_best(gear_st, GEAR_PACK))
|
||||
expect_eq(gear_level(gear_st, GEAR_PACK), 3)
|
||||
fresh(base_st, gear_st, geartest_st)
|
||||
geartest_st.money = 100
|
||||
geartest_st.hide_pack = true
|
||||
expect(not gear_buy(base_st, gear_st, GEAR_PACK))
|
||||
expect_eq(len(gear_kinds_at(-1)), 2)
|
||||
}
|
||||
|
||||
test "a tier decides a value and a mult, and the stack's room follows" {
|
||||
fresh()
|
||||
expect_eq(gear_value(GV_BITE), 1.0)
|
||||
gear_set_level(GEAR_ROD, 3)
|
||||
expect_eq(gear_value(GV_BITE), 0.55)
|
||||
expect_eq(gear_mult(GEAR_PACK), 1.0)
|
||||
gear_set_level(GEAR_PACK, 2)
|
||||
expect_eq(gear_mult(GEAR_PACK), 1.7)
|
||||
expect_eq(gear_stack_limit(10, gear_mult(GEAR_PACK)), 17)
|
||||
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(base_st, gear_st, geartest_st)
|
||||
expect_eq(gear_value(gear_st, GV_BITE), 1.0)
|
||||
gear_set_level(gear_st, GEAR_ROD, 3)
|
||||
expect_eq(gear_value(gear_st, GV_BITE), 0.55)
|
||||
expect_eq(gear_mult(gear_st, GEAR_PACK), 1.0)
|
||||
gear_set_level(gear_st, GEAR_PACK, 2)
|
||||
expect_eq(gear_mult(gear_st, GEAR_PACK), 1.7)
|
||||
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(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" {
|
||||
fresh()
|
||||
pack[IT_ROD] = 1
|
||||
gear_sync()
|
||||
expect_eq(gear_level(GEAR_ROD), 1)
|
||||
test "a carried item counts as the first tier" (base_st: mut BaseState, gear_st: mut GearState, geartest_st: mut GeartestState) {
|
||||
fresh(base_st, gear_st, geartest_st)
|
||||
geartest_st.pack[IT_ROD] = 1
|
||||
gear_sync(gear_st)
|
||||
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" {
|
||||
fresh()
|
||||
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(base_st, gear_st, geartest_st)
|
||||
let t = CHARGE_TORCH
|
||||
expect(not gear_run(t, true))
|
||||
gear_set_hand(IT_TORCH)
|
||||
expect(gear_run(t, true))
|
||||
expect(gear_lit())
|
||||
expect(not gear_run(gear_st, t, true))
|
||||
gear_set_hand(gear_st, IT_TORCH)
|
||||
expect(gear_run(gear_st, t, true))
|
||||
expect(gear_lit(gear_st))
|
||||
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))
|
||||
expect_eq(gear_charge(t), 0.0)
|
||||
expect(not gear_lit())
|
||||
expect_eq(ran_out(), t)
|
||||
expect(not gear_run(t, true))
|
||||
expect_eq(gear_charge(gear_st, t), 0.0)
|
||||
expect(not gear_lit(gear_st))
|
||||
expect_eq(ran_out(base_st, gear_st), t)
|
||||
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" {
|
||||
fresh()
|
||||
gear_set_hand(IT_LAMP)
|
||||
gear_run(CHARGE_LAMP, true)
|
||||
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(base_st, gear_st, geartest_st)
|
||||
gear_set_hand(gear_st, IT_LAMP)
|
||||
gear_run(gear_st, CHARGE_LAMP, true)
|
||||
gear_system().tick(tick_new(0.1, 1, 1.0))
|
||||
expect_eq(gear_charge(CHARGE_LAMP), 86.0)
|
||||
gear_set_level(GEAR_CAMERA, 2)
|
||||
expect_eq(gear_charge(gear_st, CHARGE_LAMP), 86.0)
|
||||
gear_set_level(gear_st, GEAR_CAMERA, 2)
|
||||
gear_system().tick(tick_new(0.1, 2, 1.0))
|
||||
expect_eq(gear_charge(CHARGE_LAMP), 78.0)
|
||||
gear_run(CHARGE_RADIO, true)
|
||||
gear_set_hand(-1)
|
||||
expect(not gear_running(CHARGE_LAMP))
|
||||
expect(gear_running(CHARGE_RADIO))
|
||||
expect_eq(gear_charge(gear_st, CHARGE_LAMP), 78.0)
|
||||
gear_run(gear_st, CHARGE_RADIO, true)
|
||||
gear_set_hand(gear_st, -1)
|
||||
expect(not gear_running(gear_st, CHARGE_LAMP))
|
||||
expect(gear_running(gear_st, CHARGE_RADIO))
|
||||
}
|
||||
|
||||
test "levels and charges are saved by key" {
|
||||
fresh()
|
||||
gear_set_level(GEAR_ROD, 2)
|
||||
gear_charge_set(CHARGE_TORCH, 12.5)
|
||||
let v = gear_save()
|
||||
fresh()
|
||||
gear_load(v, 1)
|
||||
expect_eq(gear_level(GEAR_ROD), 2)
|
||||
expect_eq(gear_level(GEAR_CAMERA), 1)
|
||||
expect_eq(gear_charge(CHARGE_TORCH), 12.5)
|
||||
expect_eq(gear_charge(CHARGE_LAMP), 100.0)
|
||||
test "levels and charges are saved by key" (base_st: mut BaseState, gear_st: mut GearState, geartest_st: mut GeartestState) {
|
||||
fresh(base_st, gear_st, geartest_st)
|
||||
gear_set_level(gear_st, GEAR_ROD, 2)
|
||||
gear_charge_set(gear_st, CHARGE_TORCH, 12.5)
|
||||
let v = gear_save(gear_st)
|
||||
fresh(base_st, gear_st, geartest_st)
|
||||
gear_load(base_st, gear_st, v, 1)
|
||||
expect_eq(gear_level(gear_st, GEAR_ROD), 2)
|
||||
expect_eq(gear_level(gear_st, GEAR_CAMERA), 1)
|
||||
expect_eq(gear_charge(gear_st, CHARGE_TORCH), 12.5)
|
||||
expect_eq(gear_charge(gear_st, CHARGE_LAMP), 100.0)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
# 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_level(k: int) -> int {
|
||||
gear__ensure()
|
||||
return gear__lv[k]
|
||||
export function gear_level(gear_st: mut GearState, k: int) -> int {
|
||||
gear__ensure(gear_st)
|
||||
return gear_st.gear__lv[k]
|
||||
}
|
||||
|
||||
# outright: staging, a test, a camera handed over with the trip
|
||||
export function gear_set_level(k: int, lv: int) -> void {
|
||||
gear__ensure()
|
||||
gear__lv[k] = lv
|
||||
export function gear_set_level(gear_st: mut GearState, k: int, lv: int) -> void {
|
||||
gear__ensure(gear_st)
|
||||
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
|
||||
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
|
||||
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 {
|
||||
if gear_best(k) { return 0 }
|
||||
return GearKinds[k].cost[gear_level(k)]
|
||||
export function gear_next_cost(gear_st: mut GearState, k: int) -> int {
|
||||
if gear_best(gear_st, k) { return 0 }
|
||||
return GearKinds[k].cost[gear_level(gear_st, k)]
|
||||
}
|
||||
|
||||
export function gear_next_rep(k: int) -> int {
|
||||
if gear_best(k) { return 0 }
|
||||
return GearKinds[k].rep[gear_level(k)]
|
||||
export function gear_next_rep(gear_st: mut GearState, k: int) -> int {
|
||||
if gear_best(gear_st, k) { return 0 }
|
||||
return GearKinds[k].rep[gear_level(gear_st, k)]
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
export function gear_value(v: int) -> float {
|
||||
export function gear_value(gear_st: mut GearState, v: int) -> float {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,45 +1,45 @@
|
|||
# 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
|
||||
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
|
||||
export function hints_key() -> void {
|
||||
if hn_card >= 0 {
|
||||
hints_close()
|
||||
export function hints_key(base_st: mut BaseState, hints_st: mut HintsState) -> void {
|
||||
if hints_st.hn_card >= 0 {
|
||||
hints_close(base_st, hints_st)
|
||||
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
|
||||
export function hints_open(k: int) -> void {
|
||||
if k < 0 or k >= hn_n { return }
|
||||
hn_pick = k
|
||||
hn_card = hn_top[k]
|
||||
hn_fact(HINTS_F_OPENED, hn_card)
|
||||
export function hints_open(base_st: mut BaseState, hints_st: mut HintsState, k: int) -> void {
|
||||
if k < 0 or k >= hints_st.hn_n { return }
|
||||
hints_st.hn_pick = k
|
||||
hints_st.hn_card = hints_st.hn_top[k]
|
||||
hn_fact(base_st, hints_st, HINTS_F_OPENED, hints_st.hn_card)
|
||||
}
|
||||
|
||||
export function hints_close() -> void {
|
||||
if hn_card < 0 { return }
|
||||
let c = hn_card
|
||||
hn_card = -1
|
||||
hn_fact(HINTS_F_CLOSED, c)
|
||||
export function hints_close(base_st: mut BaseState, hints_st: mut HintsState) -> void {
|
||||
if hints_st.hn_card < 0 { return }
|
||||
let c = hints_st.hn_card
|
||||
hints_st.hn_card = -1
|
||||
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
|
||||
export function hints_move(d: int) -> void {
|
||||
if hn_card < 0 or hn_n <= 1 { return }
|
||||
hints_open((hn_pick + hn_n + d) % hn_n)
|
||||
export function hints_move(base_st: mut BaseState, hints_st: mut HintsState, d: int) -> void {
|
||||
if hints_st.hn_card < 0 or hints_st.hn_n <= 1 { return }
|
||||
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
|
||||
export function hints_mute_card() -> void {
|
||||
if hn_card < 0 { return }
|
||||
let c = hn_card
|
||||
hints_mute(c, true)
|
||||
hn_card = -1
|
||||
hn_t = 0.0
|
||||
hn_fact(HINTS_F_MUTED, c)
|
||||
export function hints_mute_card(base_st: mut BaseState, hints_st: mut HintsState) -> void {
|
||||
if hints_st.hn_card < 0 { return }
|
||||
let c = hints_st.hn_card
|
||||
hints_mute(hints_st, c, true)
|
||||
hints_st.hn_card = -1
|
||||
hints_st.hn_t = 0.0
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -1,62 +1,60 @@
|
|||
# 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
|
||||
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 {
|
||||
if hn_mute != null { return }
|
||||
hn_mute = new []bool
|
||||
for i in 0 .. HINT_COUNT { push(hn_mute, false) }
|
||||
hn_foreign = new []string
|
||||
function hn_mute_ensure(hints_st: mut HintsState) -> void {
|
||||
if hints_st.hn_mute != null { return }
|
||||
hints_st.hn_mute = new []bool
|
||||
for i in 0 .. HINT_COUNT { push(hints_st.hn_mute, false) }
|
||||
hints_st.hn_foreign = new []string
|
||||
}
|
||||
|
||||
export function hints_muted(id: int) -> bool {
|
||||
hn_mute_ensure()
|
||||
export function hints_muted(hints_st: mut HintsState, id: int) -> bool {
|
||||
hn_mute_ensure(hints_st)
|
||||
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 {
|
||||
hn_mute_ensure()
|
||||
export function hints_mute(hints_st: mut HintsState, id: int, on: bool) -> void {
|
||||
hn_mute_ensure(hints_st)
|
||||
if id < 0 or id >= HINT_COUNT { return }
|
||||
hn_mute[id] = on
|
||||
hints_st.hn_mute[id] = on
|
||||
}
|
||||
|
||||
export function hints_mute_count() -> int {
|
||||
hn_mute_ensure()
|
||||
export function hints_mute_count(hints_st: mut HintsState) -> int {
|
||||
hn_mute_ensure(hints_st)
|
||||
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
|
||||
}
|
||||
|
||||
# every muted card given back, from the next pass
|
||||
export function hints_unmute_all() -> void {
|
||||
hn_mute_ensure()
|
||||
for i in 0 .. HINT_COUNT { hn_mute[i] = false }
|
||||
hn_foreign = new []string
|
||||
hn_t = 0.0
|
||||
hn_fact(HINTS_F_UNMUTED, -1)
|
||||
export function hints_unmute_all(base_st: mut BaseState, hints_st: mut HintsState) -> void {
|
||||
hn_mute_ensure(hints_st)
|
||||
for i in 0 .. HINT_COUNT { hints_st.hn_mute[i] = false }
|
||||
hints_st.hn_foreign = new []string
|
||||
hints_st.hn_t = 0.0
|
||||
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
|
||||
export function hints_mutes_save() -> Val {
|
||||
hn_mute_ensure()
|
||||
export function hints_mutes_save(hints_st: mut HintsState) -> Val {
|
||||
hn_mute_ensure(hints_st)
|
||||
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 .. len(hn_foreign) { Value.add(l, Value.str(hn_foreign[i])) }
|
||||
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(hints_st.hn_foreign) { Value.add(l, Value.str(hints_st.hn_foreign[i])) }
|
||||
return l
|
||||
}
|
||||
|
||||
# a list of keys read back; anything that is not a list is no mutes
|
||||
export function hints_mutes_load(l: Val) -> void {
|
||||
hn_mute = null
|
||||
hn_mute_ensure()
|
||||
export function hints_mutes_load(hints_st: mut HintsState, l: Val) -> void {
|
||||
hints_st.hn_mute = null
|
||||
hn_mute_ensure(hints_st)
|
||||
if l == null or Value.kind(l) != 5 { return }
|
||||
for i in 0 .. Value.count(l) {
|
||||
let e = Value.at(l, i)
|
||||
if Value.kind(e) != 4 { continue }
|
||||
let k = Value.as_str(e)
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,102 +1,106 @@
|
|||
# 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
|
||||
var hn_shown: int = 3 # how many the rail holds
|
||||
var hn_every: float = 1.0 # seconds between passes
|
||||
var hn_top: []int = null # the cards on the rail this pass
|
||||
var hn_n: int = 0
|
||||
var hn_t: float = 0.0 # seconds to the next pass
|
||||
var hn_pick: int = 0 # which chip the key would open
|
||||
var hn_card: int = -1 # the card being read, or -1
|
||||
var hn_facts: Queue<HintsFact> = null
|
||||
|
||||
export function hints_facts() -> Queue<HintsFact> {
|
||||
if hn_facts == null { hn_facts = queue_new("hints.facts") }
|
||||
return hn_facts
|
||||
export state HintsState {
|
||||
hn_mute: []bool = null
|
||||
hn_foreign: []string = null # muted keys with no card in this build, written back as read
|
||||
hn_shown: int = 3 # how many the rail holds
|
||||
hn_every: float = 1.0 # seconds between passes
|
||||
hn_top: []int = null # the cards on the rail this pass
|
||||
hn_n: int = 0
|
||||
hn_t: float = 0.0 # seconds to the next pass
|
||||
hn_pick: int = 0 # which chip the key would open
|
||||
hn_card: int = -1 # the card being read, or -1
|
||||
hn_facts: Queue<HintsFact> = null
|
||||
}
|
||||
|
||||
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
|
||||
f.what = what
|
||||
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
|
||||
export function hints_config(shown: int, every: float) -> void {
|
||||
hn_shown = shown
|
||||
hn_every = every
|
||||
hn_top = null
|
||||
export function hints_config(hints_st: mut HintsState, shown: int, every: float) -> void {
|
||||
hints_st.hn_shown = shown
|
||||
hints_st.hn_every = every
|
||||
hints_st.hn_top = null
|
||||
}
|
||||
|
||||
function hn_ensure() -> void {
|
||||
if hn_top == null or len(hn_top) != hn_shown {
|
||||
hn_top = new []int
|
||||
for i in 0 .. hn_shown { push(hn_top, -1) }
|
||||
function hn_ensure(hints_st: mut HintsState) -> void {
|
||||
if hints_st.hn_top == null or len(hints_st.hn_top) != hints_st.hn_shown {
|
||||
hints_st.hn_top = new []int
|
||||
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
|
||||
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 {
|
||||
hn_ensure()
|
||||
hn_t = hn_t - dt
|
||||
if hn_t > 0.0 { return }
|
||||
hn_t = hn_every
|
||||
hints_sort()
|
||||
export function hints_tick(hints_st: mut HintsState, dt: float) -> void {
|
||||
hn_ensure(hints_st)
|
||||
hints_st.hn_t = hints_st.hn_t - dt
|
||||
if hints_st.hn_t > 0.0 { return }
|
||||
hints_st.hn_t = hints_st.hn_every
|
||||
hints_sort(hints_st)
|
||||
}
|
||||
|
||||
# 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
|
||||
export function hints_sort() -> void {
|
||||
hn_ensure()
|
||||
hn_n = 0
|
||||
export function hints_sort(hints_st: mut HintsState) -> void {
|
||||
hn_ensure(hints_st)
|
||||
hints_st.hn_n = 0
|
||||
let on = HintsWorld.showing()
|
||||
for u in 0 .. 4 {
|
||||
let want = HINTS_URGENT - u
|
||||
for id in 0 .. HINT_COUNT {
|
||||
if not on or hn_n >= hn_shown { break }
|
||||
if Hints[id].urgency != want or hints_muted(id) { continue }
|
||||
if not on or hints_st.hn_n >= hints_st.hn_shown { break }
|
||||
if Hints[id].urgency != want or hints_muted(hints_st, id) { continue }
|
||||
if not HintsWorld.true_now(id) { continue }
|
||||
hn_top[hn_n] = id
|
||||
hn_n += 1
|
||||
hints_st.hn_top[hints_st.hn_n] = id
|
||||
hints_st.hn_n += 1
|
||||
}
|
||||
}
|
||||
if hn_pick >= hn_n { hn_pick = 0 }
|
||||
if hn_card < 0 { return }
|
||||
if not on or hints_muted(hn_card) or not HintsWorld.true_now(hn_card) {
|
||||
hn_card = -1
|
||||
if hints_st.hn_pick >= hints_st.hn_n { hints_st.hn_pick = 0 }
|
||||
if hints_st.hn_card < 0 { return }
|
||||
if not on or hints_muted(hints_st, hints_st.hn_card) or not HintsWorld.true_now(hints_st.hn_card) {
|
||||
hints_st.hn_card = -1
|
||||
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)
|
||||
export function hints_rail() -> []int {
|
||||
hn_ensure()
|
||||
export function hints_rail(hints_st: mut HintsState) -> []int {
|
||||
hn_ensure(hints_st)
|
||||
let out = new []int
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
if k < 0 or k >= hn_n { return -1 }
|
||||
return hn_top[k]
|
||||
export function hints_at(hints_st: HintsState, k: int) -> int {
|
||||
if k < 0 or k >= hints_st.hn_n { return -1 }
|
||||
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 {
|
||||
hn_top = null
|
||||
hn_n = 0
|
||||
hn_t = 0.0
|
||||
hn_pick = 0
|
||||
hn_card = -1
|
||||
q_clear(hints_facts())
|
||||
export function hints_reset(base_st: mut BaseState, hints_st: mut HintsState) -> void {
|
||||
hints_st.hn_top = null
|
||||
hints_st.hn_n = 0
|
||||
hints_st.hn_t = 0.0
|
||||
hints_st.hn_pick = 0
|
||||
hints_st.hn_card = -1
|
||||
q_clear(base_st, hints_facts(base_st, hints_st))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 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)
|
||||
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 {
|
||||
let s = system_new("hints", PH_PRESENT)
|
||||
|
|
|
|||
|
|
@ -11,24 +11,26 @@ program HintsTest {
|
|||
def Hints night { urgency: HINTS_TIP, line: "Night soon" }
|
||||
def Hints from "cards.lres"
|
||||
|
||||
var on: []bool = null
|
||||
var 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())
|
||||
state HintstestState {
|
||||
on: []bool = null
|
||||
showing: bool = true
|
||||
}
|
||||
|
||||
function count(what: int) -> int {
|
||||
let fs = q_drain(hints_facts())
|
||||
function fake_true(hintstest_st: HintstestState, i: int) -> bool { return hintstest_st.on[i] }
|
||||
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
|
||||
for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } }
|
||||
return n
|
||||
|
|
@ -44,119 +46,119 @@ program HintsTest {
|
|||
expect(hints_step_text(HINT_THIRSTY, 1) == "Drink at a shore")
|
||||
}
|
||||
|
||||
test "the most urgent true cards, capped, ties in registry order" {
|
||||
fresh()
|
||||
for i in 0 .. HINT_COUNT { on[i] = true }
|
||||
hints_tick(1.0)
|
||||
expect_eq(hints_count(), 3)
|
||||
expect_eq(hints_at(0), HINT_PARCHED)
|
||||
expect_eq(hints_at(1), HINT_BEAR)
|
||||
expect_eq(hints_at(2), HINT_COLD)
|
||||
on[HINT_PARCHED] = false
|
||||
on[HINT_BEAR] = false
|
||||
hints_tick(0.5)
|
||||
expect_eq(hints_at(0), HINT_PARCHED) # not a second yet: the rail holds
|
||||
hints_tick(0.6)
|
||||
expect_eq(hints_at(0), HINT_COLD)
|
||||
expect_eq(hints_at(1), HINT_THIRSTY)
|
||||
expect_eq(hints_at(2), HINT_FIRE)
|
||||
test "the most urgent true cards, capped, ties in registry order" (base_st: mut BaseState, hints_st: mut HintsState, hintstest_st: mut HintstestState) {
|
||||
fresh(base_st, hints_st, hintstest_st)
|
||||
for i in 0 .. HINT_COUNT { hintstest_st.on[i] = true }
|
||||
hints_tick(hints_st, 1.0)
|
||||
expect_eq(hints_count(hints_st), 3)
|
||||
expect_eq(hints_at(hints_st, 0), HINT_PARCHED)
|
||||
expect_eq(hints_at(hints_st, 1), HINT_BEAR)
|
||||
expect_eq(hints_at(hints_st, 2), HINT_COLD)
|
||||
hintstest_st.on[HINT_PARCHED] = false
|
||||
hintstest_st.on[HINT_BEAR] = false
|
||||
hints_tick(hints_st, 0.5)
|
||||
expect_eq(hints_at(hints_st, 0), HINT_PARCHED) # not a second yet: the rail holds
|
||||
hints_tick(hints_st, 0.6)
|
||||
expect_eq(hints_at(hints_st, 0), HINT_COLD)
|
||||
expect_eq(hints_at(hints_st, 1), HINT_THIRSTY)
|
||||
expect_eq(hints_at(hints_st, 2), HINT_FIRE)
|
||||
}
|
||||
|
||||
test "nothing on the rail while it is not showing" {
|
||||
fresh()
|
||||
on[HINT_COLD] = true
|
||||
showing = false
|
||||
hints_sort()
|
||||
expect_eq(len(hints_rail()), 0)
|
||||
expect_eq(hints_count(), 0)
|
||||
showing = true
|
||||
hints_sort()
|
||||
expect_eq(len(hints_rail()), 1)
|
||||
test "nothing on the rail while it is not showing" (base_st: mut BaseState, hints_st: mut HintsState, hintstest_st: mut HintstestState) {
|
||||
fresh(base_st, hints_st, hintstest_st)
|
||||
hintstest_st.on[HINT_COLD] = true
|
||||
hintstest_st.showing = false
|
||||
hints_sort(hints_st)
|
||||
expect_eq(len(hints_rail(hints_st)), 0)
|
||||
expect_eq(hints_count(hints_st), 0)
|
||||
hintstest_st.showing = true
|
||||
hints_sort(hints_st)
|
||||
expect_eq(len(hints_rail(hints_st)), 1)
|
||||
}
|
||||
|
||||
test "the key opens the picked chip, the arrows walk round, the key puts it down" {
|
||||
fresh()
|
||||
on[HINT_COLD] = true
|
||||
on[HINT_THIRSTY] = true
|
||||
on[HINT_RAIN] = true
|
||||
hints_sort()
|
||||
hints_key()
|
||||
expect_eq(hints_card(), HINT_COLD)
|
||||
hints_move(1)
|
||||
expect_eq(hints_card(), HINT_THIRSTY)
|
||||
expect(hints_hot(1))
|
||||
hints_move(-1)
|
||||
hints_move(-1)
|
||||
expect_eq(hints_card(), HINT_RAIN)
|
||||
hints_key()
|
||||
expect_eq(hints_card(), -1)
|
||||
expect_eq(count(HINTS_F_OPENED), 4)
|
||||
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(base_st, hints_st, hintstest_st)
|
||||
hintstest_st.on[HINT_COLD] = true
|
||||
hintstest_st.on[HINT_THIRSTY] = true
|
||||
hintstest_st.on[HINT_RAIN] = true
|
||||
hints_sort(hints_st)
|
||||
hints_key(base_st, hints_st)
|
||||
expect_eq(hints_card(hints_st), HINT_COLD)
|
||||
hints_move(base_st, hints_st, 1)
|
||||
expect_eq(hints_card(hints_st), HINT_THIRSTY)
|
||||
expect(hints_hot(hints_st, 1))
|
||||
hints_move(base_st, hints_st, -1)
|
||||
hints_move(base_st, hints_st, -1)
|
||||
expect_eq(hints_card(hints_st), HINT_RAIN)
|
||||
hints_key(base_st, hints_st)
|
||||
expect_eq(hints_card(hints_st), -1)
|
||||
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" {
|
||||
fresh()
|
||||
on[HINT_THIRSTY] = true
|
||||
on[HINT_RAIN] = true
|
||||
hints_sort()
|
||||
hints_open(1)
|
||||
expect_eq(hints_card(), HINT_RAIN)
|
||||
on[HINT_COLD] = true
|
||||
hints_sort()
|
||||
expect_eq(hints_card(), HINT_RAIN)
|
||||
expect_eq(hints_pick(), 2)
|
||||
on[HINT_RAIN] = false
|
||||
hints_sort()
|
||||
expect_eq(hints_card(), -1)
|
||||
expect_eq(count(HINTS_F_CLOSED), 0) # it went; nobody put it down
|
||||
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(base_st, hints_st, hintstest_st)
|
||||
hintstest_st.on[HINT_THIRSTY] = true
|
||||
hintstest_st.on[HINT_RAIN] = true
|
||||
hints_sort(hints_st)
|
||||
hints_open(base_st, hints_st, 1)
|
||||
expect_eq(hints_card(hints_st), HINT_RAIN)
|
||||
hintstest_st.on[HINT_COLD] = true
|
||||
hints_sort(hints_st)
|
||||
expect_eq(hints_card(hints_st), HINT_RAIN)
|
||||
expect_eq(hints_pick(hints_st), 2)
|
||||
hintstest_st.on[HINT_RAIN] = false
|
||||
hints_sort(hints_st)
|
||||
expect_eq(hints_card(hints_st), -1)
|
||||
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" {
|
||||
fresh()
|
||||
on[HINT_COLD] = true
|
||||
on[HINT_THIRSTY] = true
|
||||
hints_sort()
|
||||
hints_key()
|
||||
hints_mute_card()
|
||||
expect_eq(hints_card(), -1)
|
||||
expect(hints_muted(HINT_COLD))
|
||||
expect_eq(hints_mute_count(), 1)
|
||||
expect_eq(count(HINTS_F_MUTED), 1)
|
||||
hints_tick(0.0) # the next pass comes at once
|
||||
expect_eq(hints_at(0), HINT_THIRSTY)
|
||||
expect_eq(hints_count(), 1)
|
||||
hints_unmute_all()
|
||||
hints_tick(0.0)
|
||||
expect_eq(hints_at(0), HINT_COLD)
|
||||
expect_eq(count(HINTS_F_UNMUTED), 1)
|
||||
test "I know this: muted off the rail, counted, given back" (base_st: mut BaseState, hints_st: mut HintsState, hintstest_st: mut HintstestState) {
|
||||
fresh(base_st, hints_st, hintstest_st)
|
||||
hintstest_st.on[HINT_COLD] = true
|
||||
hintstest_st.on[HINT_THIRSTY] = true
|
||||
hints_sort(hints_st)
|
||||
hints_key(base_st, hints_st)
|
||||
hints_mute_card(base_st, hints_st)
|
||||
expect_eq(hints_card(hints_st), -1)
|
||||
expect(hints_muted(hints_st, HINT_COLD))
|
||||
expect_eq(hints_mute_count(hints_st), 1)
|
||||
expect_eq(count(base_st, hints_st, HINTS_F_MUTED), 1)
|
||||
hints_tick(hints_st, 0.0) # the next pass comes at once
|
||||
expect_eq(hints_at(hints_st, 0), HINT_THIRSTY)
|
||||
expect_eq(hints_count(hints_st), 1)
|
||||
hints_unmute_all(base_st, hints_st)
|
||||
hints_tick(hints_st, 0.0)
|
||||
expect_eq(hints_at(hints_st, 0), HINT_COLD)
|
||||
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" {
|
||||
fresh()
|
||||
hints_mute(HINT_NIGHT, true)
|
||||
hints_mute(HINT_BEAR, true)
|
||||
let l = hints_mutes_save()
|
||||
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(base_st, hints_st, hintstest_st)
|
||||
hints_mute(hints_st, HINT_NIGHT, true)
|
||||
hints_mute(hints_st, HINT_BEAR, true)
|
||||
let l = hints_mutes_save(hints_st)
|
||||
expect_eq(Value.count(l), 2)
|
||||
Value.add(l, Value.str("gone_card"))
|
||||
Value.add(l, Value.int(3)) # not a key: ignored
|
||||
hints_mutes_load(l)
|
||||
expect(hints_muted(HINT_NIGHT))
|
||||
expect(hints_muted(HINT_BEAR))
|
||||
expect(not hints_muted(HINT_RAIN))
|
||||
expect_eq(hints_mute_count(), 2)
|
||||
let back = hints_mutes_save()
|
||||
hints_mutes_load(hints_st, l)
|
||||
expect(hints_muted(hints_st, HINT_NIGHT))
|
||||
expect(hints_muted(hints_st, HINT_BEAR))
|
||||
expect(not hints_muted(hints_st, HINT_RAIN))
|
||||
expect_eq(hints_mute_count(hints_st), 2)
|
||||
let back = hints_mutes_save(hints_st)
|
||||
expect_eq(Value.count(back), 3)
|
||||
expect(Value.as_str(Value.at(back, 2)) == "gone_card")
|
||||
hints_mutes_load(null)
|
||||
expect_eq(hints_mute_count(), 0)
|
||||
hints_mutes_load(hints_st, null)
|
||||
expect_eq(hints_mute_count(hints_st), 0)
|
||||
}
|
||||
|
||||
test "as a system it ticks the rail" {
|
||||
fresh()
|
||||
core_clear()
|
||||
core_add(hints_system())
|
||||
core_reset_all()
|
||||
on[HINT_FIRE] = true
|
||||
core_tick_all(tick_new(1.0, 1, 0.0))
|
||||
expect_eq(hints_at(0), HINT_FIRE)
|
||||
test "as a system it ticks the rail" (base_st: mut BaseState, hints_st: mut HintsState, hintstest_st: mut HintstestState) {
|
||||
fresh(base_st, hints_st, hintstest_st)
|
||||
core_clear(base_st)
|
||||
core_add(base_st, hints_system())
|
||||
core_reset_all(base_st)
|
||||
hintstest_st.on[HINT_FIRE] = true
|
||||
core_tick_all(base_st, tick_new(1.0, 1, 0.0))
|
||||
expect_eq(hints_at(hints_st, 0), HINT_FIRE)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
# 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
|
||||
export function i18n_init() -> void {
|
||||
codes = new []string
|
||||
names = new []string
|
||||
paths = new []string
|
||||
fonts = new []string
|
||||
lang_add("en", "English", "", "")
|
||||
let idx = Fs.read_text(lang_dir + "/languages.txt")
|
||||
export function i18n_init(i18n_st: mut I18nState) -> void {
|
||||
i18n_st.codes = new []string
|
||||
i18n_st.names = new []string
|
||||
i18n_st.paths = new []string
|
||||
i18n_st.fonts = new []string
|
||||
lang_add(i18n_st, "en", "English", "", "")
|
||||
let idx = Fs.read_text(i18n_st.lang_dir + "/languages.txt")
|
||||
if idx != null {
|
||||
let ls = lines_of(idx)
|
||||
for i in 0 .. len(ls) {
|
||||
let ln = trim(ls[i])
|
||||
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()
|
||||
|
|
@ -23,60 +23,60 @@ export function i18n_init() -> void {
|
|||
for i in 0 .. len(files) {
|
||||
let nm = files[i]
|
||||
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
|
||||
export function i18n_mod_dir() -> string { return I18nWorld.mod_dir() }
|
||||
|
||||
function lang_add(code: string, name: string, path: string, font: string) -> void {
|
||||
for i in 0 .. len(codes) {
|
||||
if codes[i] == code {
|
||||
names[i] = name
|
||||
paths[i] = path
|
||||
fonts[i] = font
|
||||
function lang_add(i18n_st: mut I18nState, code: string, name: string, path: string, font: string) -> void {
|
||||
for i in 0 .. len(i18n_st.codes) {
|
||||
if i18n_st.codes[i] == code {
|
||||
i18n_st.names[i] = name
|
||||
i18n_st.paths[i] = path
|
||||
i18n_st.fonts[i] = font
|
||||
return
|
||||
}
|
||||
}
|
||||
push(codes, code)
|
||||
push(names, name)
|
||||
push(paths, path)
|
||||
push(fonts, font)
|
||||
push(i18n_st.codes, code)
|
||||
push(i18n_st.names, name)
|
||||
push(i18n_st.paths, path)
|
||||
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)
|
||||
if text == null { return }
|
||||
var name = i18n_header(text, "X-Language-Name")
|
||||
if len(name) == 0 { name = code }
|
||||
var font = i18n_header(text, "X-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 {
|
||||
if codes == null { return 1 }
|
||||
return len(codes)
|
||||
export function i18n_count(i18n_st: I18nState) -> int {
|
||||
if i18n_st.codes == null { return 1 }
|
||||
return len(i18n_st.codes)
|
||||
}
|
||||
|
||||
export function i18n_index(code: string) -> int {
|
||||
if codes != null { for i in 0 .. len(codes) { if codes[i] == code { return i } } }
|
||||
export function i18n_index(i18n_st: I18nState, code: string) -> int {
|
||||
if i18n_st.codes != null { for i in 0 .. len(i18n_st.codes) { if i18n_st.codes[i] == code { return i } } }
|
||||
return 0
|
||||
}
|
||||
|
||||
export function i18n_code_at(i: int) -> string {
|
||||
if codes == null or i < 0 or i >= len(codes) { return "en" }
|
||||
return codes[i]
|
||||
export function i18n_code_at(i18n_st: I18nState, i: int) -> string {
|
||||
if i18n_st.codes == null or i < 0 or i >= len(i18n_st.codes) { return "en" }
|
||||
return i18n_st.codes[i]
|
||||
}
|
||||
|
||||
export function i18n_name_at(i: int) -> string {
|
||||
if names == null or i < 0 or i >= len(names) { return "English" }
|
||||
return names[i]
|
||||
export function i18n_name_at(i18n_st: I18nState, i: int) -> string {
|
||||
if i18n_st.names == null or i < 0 or i >= len(i18n_st.names) { return "English" }
|
||||
return i18n_st.names[i]
|
||||
}
|
||||
|
||||
# the font atlas a language names with X-Font, beside its .po; "" is the default
|
||||
export function i18n_font_at(i: int) -> string {
|
||||
if fonts == null or i < 0 or i >= len(fonts) { return "" }
|
||||
return fonts[i]
|
||||
export function i18n_font_at(i18n_st: I18nState, i: int) -> string {
|
||||
if i18n_st.fonts == null or i < 0 or i >= len(i18n_st.fonts) { return "" }
|
||||
return i18n_st.fonts[i]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
# English, and the text that filled each hole translated in turn
|
||||
|
||||
# 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 l0 = pat_l0[p]
|
||||
let nl = pat_nl[p]
|
||||
let first = lits[l0]
|
||||
let l0 = i18n_st.pat_l0[p]
|
||||
let nl = i18n_st.pat_nl[p]
|
||||
let first = i18n_st.lits[l0]
|
||||
let f_n = len(first)
|
||||
let last = lits[l0 + nl - 1]
|
||||
let last = i18n_st.lits[l0 + nl - 1]
|
||||
let l_n = len(last)
|
||||
if nl - 1 > CAPS { return false }
|
||||
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 adv = 0
|
||||
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))
|
||||
if stop < 0 { return false }
|
||||
adv = len(mid)
|
||||
}
|
||||
if stop <= pos { return false }
|
||||
caps[g] = s[pos..stop]
|
||||
i18n_st.caps[g] = s[pos..stop]
|
||||
pos = stop + adv
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
# 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 c = head
|
||||
while c > 0 {
|
||||
let p = c - 1
|
||||
var sc = -1
|
||||
if b >= 0 { sc = pat_sc[b] }
|
||||
if pat_sc[p] > sc and try_pattern(p, s) { b = p }
|
||||
if b >= 0 { sc = i18n_st.pat_sc[b] }
|
||||
if i18n_st.pat_sc[p] > sc and try_pattern(i18n_st, p, s) { b = p }
|
||||
c = next[p]
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
function pattern(s: string, depth: int) -> string {
|
||||
hit = false
|
||||
if pat_n == 0 { return s }
|
||||
function pattern(i18n_st: mut I18nState, s: string, depth: int) -> string {
|
||||
i18n_st.hit = false
|
||||
if i18n_st.pat_n == 0 { return s }
|
||||
let n = len(s)
|
||||
var best = -1
|
||||
if n >= 4 {
|
||||
best = best_of(Dict.get_or(pfx, s[0..4], 0), 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.pfx, s[0..4], 0), i18n_st.nx_p, 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 }
|
||||
try_pattern(best, s)
|
||||
let l0 = pat_l0[best]
|
||||
let nl = pat_nl[best]
|
||||
try_pattern(i18n_st, best, s)
|
||||
let l0 = i18n_st.pat_l0[best]
|
||||
let nl = i18n_st.pat_nl[best]
|
||||
# copied out first: translating a hole may match another pattern and reuse caps
|
||||
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
|
||||
for g in 0 .. nl - 1 { push(cs, piece(raw[g], depth + 1)) }
|
||||
let r = fill_holes(pat_out[best], l0, nl, cs)
|
||||
for g in 0 .. nl - 1 { push(cs, piece(i18n_st, raw[g], depth + 1)) }
|
||||
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
|
||||
# the sentence-by-sentence pass
|
||||
if r == s { return s }
|
||||
hit = true
|
||||
i18n_st.hit = true
|
||||
return r
|
||||
}
|
||||
|
||||
# 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
|
||||
function piece(c: string, depth: int) -> string {
|
||||
let e = Dict.get_or(exact, c, -1)
|
||||
if e >= 0 { return out[e] }
|
||||
function piece(i18n_st: mut I18nState, c: string, depth: int) -> string {
|
||||
let e = Dict.get_or(i18n_st.exact, c, -1)
|
||||
if e >= 0 { return i18n_st.out[e] }
|
||||
let n = len(c)
|
||||
var bare = -1
|
||||
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 > 2 and (i18n_starts(c, "a ") or i18n_starts(c, "A ")) { bare = 2 }
|
||||
if bare > 0 {
|
||||
let e2 = Dict.get_or(exact, c[bare..n], -1)
|
||||
if e2 >= 0 { return out[e2] }
|
||||
let e2 = Dict.get_or(i18n_st.exact, c[bare..n], -1)
|
||||
if e2 >= 0 { return i18n_st.out[e2] }
|
||||
}
|
||||
if depth < 3 {
|
||||
let r = pattern(c, depth)
|
||||
if hit { return r }
|
||||
let r = pattern(i18n_st, c, depth)
|
||||
if i18n_st.hit { return r }
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ function holes(s: string) -> int {
|
|||
return h
|
||||
}
|
||||
|
||||
function add_pattern(id: string, tr: string) -> void {
|
||||
let p = pat_n
|
||||
pat_n += 1
|
||||
let l0 = len(lits)
|
||||
function add_pattern(i18n_st: mut I18nState, id: string, tr: string) -> void {
|
||||
let p = i18n_st.pat_n
|
||||
i18n_st.pat_n += 1
|
||||
let l0 = len(i18n_st.lits)
|
||||
let n = len(id)
|
||||
var seg = 0
|
||||
var i = 0
|
||||
|
|
@ -27,9 +27,9 @@ function add_pattern(id: string, tr: string) -> void {
|
|||
while i < n {
|
||||
let num = hole_at(id, i)
|
||||
if num >= 0 {
|
||||
push(lits, id[seg..i])
|
||||
push(i18n_st.lits, id[seg..i])
|
||||
score += i - seg
|
||||
pat_ph[l0 + nl] = num
|
||||
i18n_st.pat_ph[l0 + nl] = num
|
||||
nl += 1
|
||||
i = hole_end(id, i)
|
||||
seg = i
|
||||
|
|
@ -37,33 +37,33 @@ function add_pattern(id: string, tr: string) -> void {
|
|||
}
|
||||
i += 1
|
||||
}
|
||||
push(lits, id[seg..n])
|
||||
push(i18n_st.lits, id[seg..n])
|
||||
score += n - seg
|
||||
nl += 1
|
||||
pat_l0[p] = l0
|
||||
pat_nl[p] = nl
|
||||
pat_sc[p] = score
|
||||
push(pat_out, tr)
|
||||
chain(p, lits[l0], lits[l0 + nl - 1])
|
||||
i18n_st.pat_l0[p] = l0
|
||||
i18n_st.pat_nl[p] = nl
|
||||
i18n_st.pat_sc[p] = score
|
||||
push(i18n_st.pat_out, tr)
|
||||
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 {
|
||||
let key: string = first[0..4]
|
||||
nx_p[p] = Dict.get_or(pfx, key, 0)
|
||||
Dict.set(pfx, key, p + 1)
|
||||
i18n_st.nx_p[p] = Dict.get_or(i18n_st.pfx, key, 0)
|
||||
Dict.set(i18n_st.pfx, key, p + 1)
|
||||
} else if len(last) >= 4 {
|
||||
let key: string = last[len(last) - 4..len(last)]
|
||||
nx_s[p] = Dict.get_or(sfx, key, 0)
|
||||
Dict.set(sfx, key, p + 1)
|
||||
i18n_st.nx_s[p] = Dict.get_or(i18n_st.sfx, key, 0)
|
||||
Dict.set(i18n_st.sfx, key, p + 1)
|
||||
} else {
|
||||
nx_g[p] = gen_head
|
||||
gen_head = p + 1
|
||||
i18n_st.nx_g[p] = i18n_st.gen_head
|
||||
i18n_st.gen_head = p + 1
|
||||
}
|
||||
}
|
||||
|
||||
# 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)
|
||||
var r = ""
|
||||
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)
|
||||
if num >= 0 {
|
||||
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)
|
||||
seg = i
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ function plural_rule_of(h: string) -> int {
|
|||
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
|
||||
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
|
||||
}
|
||||
|
||||
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 }
|
||||
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 {
|
||||
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",
|
||||
# "{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
|
||||
if n == 1 { t = one }
|
||||
if on {
|
||||
let k = Dict.get_or(plural, one, -1)
|
||||
if i18n_st.on {
|
||||
let k = Dict.get_or(i18n_st.plural, one, -1)
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,13 +12,44 @@ function i18n__no_code() -> string { return "" }
|
|||
function i18n__no() -> bool { return false }
|
||||
function i18n__no_font(dir: string) -> bool { return false }
|
||||
|
||||
var lang_dir: string = "assets/lang"
|
||||
var font_default: string = "assets/kit/font"
|
||||
export state I18nState {
|
||||
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
|
||||
# language with no X-Font draws in
|
||||
export function i18n_config(dir: string, default_font: string) -> void {
|
||||
lang_dir = dir
|
||||
font_default = default_font
|
||||
codes = null
|
||||
export function i18n_config(i18n_st: mut I18nState, dir: string, default_font: string) -> void {
|
||||
i18n_st.lang_dir = dir
|
||||
i18n_st.font_default = default_font
|
||||
i18n_st.codes = null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,47 +1,18 @@
|
|||
# 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 CAPS: int = 16
|
||||
|
||||
export function i18n_on() -> bool { return on }
|
||||
export function i18n_current() -> string { return cur }
|
||||
export function i18n_font_now() -> string { return font_now }
|
||||
export function i18n_exact_count() -> int {
|
||||
if out == null { return 0 }
|
||||
return len(out)
|
||||
export function i18n_on(i18n_st: I18nState) -> bool { return i18n_st.on }
|
||||
export function i18n_current(i18n_st: I18nState) -> string { return i18n_st.cur }
|
||||
export function i18n_font_now(i18n_st: I18nState) -> string { return i18n_st.font_now }
|
||||
export function i18n_exact_count(i18n_st: I18nState) -> int {
|
||||
if i18n_st.out == null { return 0 }
|
||||
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
|
||||
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 }
|
||||
|
|
|
|||
|
|
@ -4,18 +4,20 @@
|
|||
import "ludic.i18n"
|
||||
program I18nTest {
|
||||
numbers float
|
||||
var forced_code: string = ""
|
||||
var fonts_set: []string = null
|
||||
var font_ok: bool = true
|
||||
state I18ntestState {
|
||||
forced_code: string = ""
|
||||
fonts_set: []string = null
|
||||
font_ok: bool = true
|
||||
}
|
||||
|
||||
function dir() -> string { return Os.temp_dir() + "/ludic-i18n-test" }
|
||||
function mods() -> string { return dir() + "/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_font(d: string) -> bool {
|
||||
push(fonts_set, d)
|
||||
return font_ok
|
||||
function t_font(i18ntest_st: mut I18ntestState, d: string) -> bool {
|
||||
push(i18ntest_st.fonts_set, d)
|
||||
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 }
|
||||
|
||||
|
|
@ -36,116 +38,116 @@ program I18nTest {
|
|||
return s
|
||||
}
|
||||
|
||||
function fresh() -> void {
|
||||
forced_code = ""
|
||||
fonts_set = new []string
|
||||
font_ok = true
|
||||
function fresh(i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) -> void {
|
||||
i18ntest_st.forced_code = ""
|
||||
i18ntest_st.fonts_set = new []string
|
||||
i18ntest_st.font_ok = true
|
||||
Fs.mkdir(dir())
|
||||
Fs.mkdir(mods())
|
||||
Fs.write_text(dir() + "/languages.txt", "# shipped\ntt\nnope\n")
|
||||
Fs.write_text(dir() + "/tt.po", tt())
|
||||
Fs.remove(mods() + "/zz.po")
|
||||
i18n_config(dir(), "font_default")
|
||||
i18n_init()
|
||||
i18n_config(i18n_st, dir(), "font_default")
|
||||
i18n_init(i18n_st)
|
||||
}
|
||||
|
||||
test "the shipped list and English are found, a missing file is not" {
|
||||
fresh()
|
||||
expect_eq(i18n_count(), 2)
|
||||
expect(i18n_code_at(0) == "en")
|
||||
expect_eq(i18n_index("tt"), 1)
|
||||
expect_eq(i18n_index("nope"), 0)
|
||||
expect(i18n_name_at(1) == "Test Tongue")
|
||||
expect(i18n_font_at(1) == dir() + "/font_tt")
|
||||
expect(L("Settings") == "Settings")
|
||||
test "the shipped list and English are found, a missing file is not" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
|
||||
fresh(i18n_st, i18ntest_st)
|
||||
expect_eq(i18n_count(i18n_st), 2)
|
||||
expect(i18n_code_at(i18n_st, 0) == "en")
|
||||
expect_eq(i18n_index(i18n_st, "tt"), 1)
|
||||
expect_eq(i18n_index(i18n_st, "nope"), 0)
|
||||
expect(i18n_name_at(i18n_st, 1) == "Test Tongue")
|
||||
expect(i18n_font_at(i18n_st, 1) == dir() + "/font_tt")
|
||||
expect(L(i18n_st, "Settings") == "Settings")
|
||||
}
|
||||
|
||||
test "exact lines, patterns with translated holes, and English where unknown" {
|
||||
fresh()
|
||||
i18n_use("tt")
|
||||
expect(i18n_on())
|
||||
expect(i18n_current() == "tt")
|
||||
expect(L("Settings") == "Zettings")
|
||||
expect(L("Day 12") == "12. Gun")
|
||||
expect(L("+2 Plant fiber") == "Lif +2")
|
||||
expect(L("You saw a deer.") == "geyik gordun.")
|
||||
expect(L("zzqx words nobody wrote") == "zzqx words nobody wrote")
|
||||
expect(L("Multiline") == "Coksatir\n")
|
||||
expect_eq(i18n_exact_count(), 6)
|
||||
expect_eq(i18n_pattern_count(), 3)
|
||||
test "exact lines, patterns with translated holes, and English where unknown" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
|
||||
fresh(i18n_st, i18ntest_st)
|
||||
i18n_use(i18n_st, "tt")
|
||||
expect(i18n_on(i18n_st))
|
||||
expect(i18n_current(i18n_st) == "tt")
|
||||
expect(L(i18n_st, "Settings") == "Zettings")
|
||||
expect(L(i18n_st, "Day 12") == "12. Gun")
|
||||
expect(L(i18n_st, "+2 Plant fiber") == "Lif +2")
|
||||
expect(L(i18n_st, "You saw a deer.") == "geyik gordun.")
|
||||
expect(L(i18n_st, "zzqx words nobody wrote") == "zzqx words nobody wrote")
|
||||
expect(L(i18n_st, "Multiline") == "Coksatir\n")
|
||||
expect_eq(i18n_exact_count(i18n_st), 6)
|
||||
expect_eq(i18n_pattern_count(i18n_st), 3)
|
||||
}
|
||||
|
||||
test "a paragraph a sentence at a time, and a layout's padding kept" {
|
||||
fresh()
|
||||
i18n_use("tt")
|
||||
expect(L("You need to eat. zzqx words nobody wrote.") == "Yemek lazim. zzqx words nobody wrote.")
|
||||
expect(L("zzqx. zzqy.") == "zzqx. zzqy.")
|
||||
expect(L(" owned") == " senin")
|
||||
expect(L(" zzqx") == " zzqx")
|
||||
test "a paragraph a sentence at a time, and a layout's padding kept" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
|
||||
fresh(i18n_st, i18ntest_st)
|
||||
i18n_use(i18n_st, "tt")
|
||||
expect(L(i18n_st, "You need to eat. zzqx words nobody wrote.") == "Yemek lazim. zzqx words nobody wrote.")
|
||||
expect(L(i18n_st, "zzqx. zzqy.") == "zzqx. zzqy.")
|
||||
expect(L(i18n_st, " owned") == " senin")
|
||||
expect(L(i18n_st, " zzqx") == " zzqx")
|
||||
}
|
||||
|
||||
test "plurals by the file's rule, and English by one and many" {
|
||||
fresh()
|
||||
expect(Ln("{1} trout", "{1} trouts", 1) == "1 trout")
|
||||
expect(Ln("{1} trout", "{1} trouts", 3) == "3 trouts")
|
||||
i18n_use("tt")
|
||||
expect_eq(i18n_plural_rule(), PL_SLAVIC)
|
||||
expect(Ln("{1} trout", "{1} trouts", 21) == "21 alabalik")
|
||||
expect(Ln("{1} trout", "{1} trouts", 5) == "5 alabaliklar")
|
||||
expect(Ln("{1} fish", "{1} fishes", 2) == "2 fishes")
|
||||
test "plurals by the file's rule, and English by one and many" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
|
||||
fresh(i18n_st, i18ntest_st)
|
||||
expect(Ln(i18n_st, "{1} trout", "{1} trouts", 1) == "1 trout")
|
||||
expect(Ln(i18n_st, "{1} trout", "{1} trouts", 3) == "3 trouts")
|
||||
i18n_use(i18n_st, "tt")
|
||||
expect_eq(i18n_plural_rule(i18n_st), PL_SLAVIC)
|
||||
expect(Ln(i18n_st, "{1} trout", "{1} trouts", 21) == "21 alabalik")
|
||||
expect(Ln(i18n_st, "{1} trout", "{1} trouts", 5) == "5 alabaliklar")
|
||||
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_ABOVE_ONE, 0), 0)
|
||||
expect_eq(i18n_plural_form(PL_SLAVIC, 3), 1)
|
||||
expect_eq(i18n_plural_form(PL_ONE_FORM, 7), 0)
|
||||
}
|
||||
|
||||
test "the font follows the language, and falls back when it will not load" {
|
||||
fresh()
|
||||
i18n_font_base("font_default")
|
||||
i18n_use("tt")
|
||||
expect_eq(len(fonts_set), 1)
|
||||
expect(i18n_font_now() == dir() + "/font_tt")
|
||||
i18n_use("en")
|
||||
expect(i18n_font_now() == "font_default")
|
||||
font_ok = false
|
||||
i18n_use("tt")
|
||||
expect(i18n_font_now() == "font_default")
|
||||
test "the font follows the language, and falls back when it will not load" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
|
||||
fresh(i18n_st, i18ntest_st)
|
||||
i18n_font_base(i18n_st, "font_default")
|
||||
i18n_use(i18n_st, "tt")
|
||||
expect_eq(len(i18ntest_st.fonts_set), 1)
|
||||
expect(i18n_font_now(i18n_st) == dir() + "/font_tt")
|
||||
i18n_use(i18n_st, "en")
|
||||
expect(i18n_font_now(i18n_st) == "font_default")
|
||||
i18ntest_st.font_ok = false
|
||||
i18n_use(i18n_st, "tt")
|
||||
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" {
|
||||
fresh()
|
||||
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(i18n_st, i18ntest_st)
|
||||
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"))
|
||||
i18n_init()
|
||||
expect_eq(i18n_count(), 3)
|
||||
expect(i18n_name_at(i18n_index("zz")) == "Mod Tongue")
|
||||
i18n_use("zz")
|
||||
expect(L("Settings") == "Mettings")
|
||||
expect(L("Day 3") == "Day 3")
|
||||
i18n_use("tt")
|
||||
expect(L("Settings") == "Z2")
|
||||
i18n_init(i18n_st)
|
||||
expect_eq(i18n_count(i18n_st), 3)
|
||||
expect(i18n_name_at(i18n_st, i18n_index(i18n_st, "zz")) == "Mod Tongue")
|
||||
i18n_use(i18n_st, "zz")
|
||||
expect(L(i18n_st, "Settings") == "Mettings")
|
||||
expect(L(i18n_st, "Day 3") == "Day 3")
|
||||
i18n_use(i18n_st, "tt")
|
||||
expect(L(i18n_st, "Settings") == "Z2")
|
||||
Fs.remove(mods() + "/zz.po")
|
||||
Fs.remove(mods() + "/tt.po")
|
||||
i18n_init()
|
||||
expect_eq(i18n_index("zz"), 0)
|
||||
i18n_use("en")
|
||||
expect(not i18n_on())
|
||||
expect(L("Settings") == "Settings")
|
||||
i18n_init(i18n_st)
|
||||
expect_eq(i18n_index(i18n_st, "zz"), 0)
|
||||
i18n_use(i18n_st, "en")
|
||||
expect(not i18n_on(i18n_st))
|
||||
expect(L(i18n_st, "Settings") == "Settings")
|
||||
}
|
||||
|
||||
test "a forced code wins over the one asked for" {
|
||||
fresh()
|
||||
forced_code = "tt"
|
||||
i18n_use("en")
|
||||
expect(i18n_current() == "tt")
|
||||
test "a forced code wins over the one asked for" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
|
||||
fresh(i18n_st, i18ntest_st)
|
||||
i18ntest_st.forced_code = "tt"
|
||||
i18n_use(i18n_st, "en")
|
||||
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-Nothing") == "")
|
||||
i18n_load_text(po_line("Quit", "Cik"))
|
||||
expect(L("Quit") == "Cik")
|
||||
i18n_off()
|
||||
expect(L("Quit") == "Quit")
|
||||
i18n_load_text(i18n_st, po_line("Quit", "Cik"))
|
||||
expect(L(i18n_st, "Quit") == "Cik")
|
||||
i18n_off(i18n_st)
|
||||
expect(L(i18n_st, "Quit") == "Quit")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
# 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
|
||||
export function L(s: string) -> string {
|
||||
if not on { return s }
|
||||
let ci = Dict.get_or(cache, s, -1)
|
||||
if ci >= 0 { return cache_out[ci] }
|
||||
let r = whole(s)
|
||||
if Dict.size(cache) >= CACHE_MAX {
|
||||
Dict.clear(cache)
|
||||
cache_out = new []string
|
||||
export function L(i18n_st: mut I18nState, s: string) -> string {
|
||||
if not i18n_st.on { return s }
|
||||
let ci = Dict.get_or(i18n_st.cache, s, -1)
|
||||
if ci >= 0 { return i18n_st.cache_out[ci] }
|
||||
let r = whole(i18n_st, s)
|
||||
if Dict.size(i18n_st.cache) >= CACHE_MAX {
|
||||
Dict.clear(i18n_st.cache)
|
||||
i18n_st.cache_out = new []string
|
||||
}
|
||||
Dict.set(cache, s, len(cache_out))
|
||||
push(cache_out, r)
|
||||
Dict.set(i18n_st.cache, s, len(i18n_st.cache_out))
|
||||
push(i18n_st.cache_out, r)
|
||||
return r
|
||||
}
|
||||
|
||||
function whole(s: string) -> string {
|
||||
function whole(i18n_st: mut I18nState, s: string) -> string {
|
||||
let n = len(s)
|
||||
if n == 0 { return s }
|
||||
let e = Dict.get_or(exact, s, -1)
|
||||
if e >= 0 { return out[e] }
|
||||
let r = pattern(s, 0)
|
||||
if hit { return r }
|
||||
let e = Dict.get_or(i18n_st.exact, s, -1)
|
||||
if e >= 0 { return i18n_st.out[e] }
|
||||
let r = pattern(i18n_st, s, 0)
|
||||
if i18n_st.hit { return r }
|
||||
# the padding a layout put round a word: " owned"
|
||||
var a = 0
|
||||
var b = n
|
||||
|
|
@ -29,15 +29,15 @@ function whole(s: string) -> string {
|
|||
if a > 0 or b < n {
|
||||
if b <= a { return s }
|
||||
let core: string = s[a..b]
|
||||
let t = whole(core)
|
||||
let t = whole(i18n_st, core)
|
||||
if t == core { return s }
|
||||
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
|
||||
function sentences(s: string) -> string {
|
||||
function sentences(i18n_st: mut I18nState, s: string) -> string {
|
||||
let n = len(s)
|
||||
var r = ""
|
||||
var start = 0
|
||||
|
|
@ -47,8 +47,8 @@ function sentences(s: string) -> string {
|
|||
while i < n - 1 {
|
||||
let c = s[i]
|
||||
if (c == 46 or c == 33 or c == 63) and s[i + 1] == 32 {
|
||||
let t = sentence(s[start..i + 1])
|
||||
if hit { any = true }
|
||||
let t = sentence(i18n_st, s[start..i + 1])
|
||||
if i18n_st.hit { any = true }
|
||||
r = r + t + " "
|
||||
start = i + 2
|
||||
pieces += 1
|
||||
|
|
@ -58,18 +58,18 @@ function sentences(s: string) -> string {
|
|||
i += 1
|
||||
}
|
||||
if pieces == 0 { return s }
|
||||
let t = sentence(s[start..n])
|
||||
if hit { any = true }
|
||||
let t = sentence(i18n_st, s[start..n])
|
||||
if i18n_st.hit { any = true }
|
||||
if not any { return s }
|
||||
return r + t
|
||||
}
|
||||
|
||||
function sentence(p: string) -> string {
|
||||
hit = false
|
||||
let e = Dict.get_or(exact, p, -1)
|
||||
function sentence(i18n_st: mut I18nState, p: string) -> string {
|
||||
i18n_st.hit = false
|
||||
let e = Dict.get_or(i18n_st.exact, p, -1)
|
||||
if e >= 0 {
|
||||
hit = true
|
||||
return out[e]
|
||||
i18n_st.hit = true
|
||||
return i18n_st.out[e]
|
||||
}
|
||||
return pattern(p, 0)
|
||||
return pattern(i18n_st, p, 0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,43 @@
|
|||
# use.ludic - choosing a language: its file loaded into the lookups, and the font atlas it names
|
||||
export function i18n_use(code: string) -> void {
|
||||
if codes == null { i18n_init() }
|
||||
on = false
|
||||
cur = "en"
|
||||
var k = i18n_index(code)
|
||||
export function i18n_use(i18n_st: mut I18nState, code: string) -> void {
|
||||
if i18n_st.codes == null { i18n_init(i18n_st) }
|
||||
i18n_st.on = false
|
||||
i18n_st.cur = "en"
|
||||
var k = i18n_index(i18n_st, code)
|
||||
let forced = I18nWorld.forced()
|
||||
if len(forced) > 0 { k = i18n_index(forced) }
|
||||
if len(forced) > 0 { k = i18n_index(i18n_st, forced) }
|
||||
var font = ""
|
||||
if k > 0 {
|
||||
let text = Fs.read_text(paths[k])
|
||||
let text = Fs.read_text(i18n_st.paths[k])
|
||||
if text != null {
|
||||
i18n_load_text(text)
|
||||
on = true
|
||||
cur = codes[k]
|
||||
font = fonts[k]
|
||||
i18n_load_text(i18n_st, text)
|
||||
i18n_st.on = true
|
||||
i18n_st.cur = i18n_st.codes[k]
|
||||
font = i18n_st.fonts[k]
|
||||
}
|
||||
}
|
||||
# the atlas follows the language, and is only reloaded when it actually changes
|
||||
if len(font) == 0 { font = font_default }
|
||||
if I18nWorld.font_ready() and not (font == font_now) {
|
||||
if I18nWorld.set_font(font) { font_now = font } else if not (font_now == font_default) {
|
||||
I18nWorld.set_font(font_default)
|
||||
font_now = font_default
|
||||
if len(font) == 0 { font = i18n_st.font_default }
|
||||
if I18nWorld.font_ready() and not (font == i18n_st.font_now) {
|
||||
if I18nWorld.set_font(font) { i18n_st.font_now = font } else if not (i18n_st.font_now == i18n_st.font_default) {
|
||||
I18nWorld.set_font(i18n_st.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)
|
||||
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)
|
||||
exact = Dict.new()
|
||||
out = new []string
|
||||
cache = Dict.new()
|
||||
cache_out = new []string
|
||||
caps = new []string
|
||||
for c in 0 .. CAPS { push(caps, "") }
|
||||
plural = Dict.new()
|
||||
forms = new []string
|
||||
plural_rule = plural_rule_of(i18n_header(text, "Plural-Forms"))
|
||||
i18n_st.exact = Dict.new()
|
||||
i18n_st.out = new []string
|
||||
i18n_st.cache = Dict.new()
|
||||
i18n_st.cache_out = new []string
|
||||
i18n_st.caps = new []string
|
||||
for c in 0 .. CAPS { push(i18n_st.caps, "") }
|
||||
i18n_st.plural = Dict.new()
|
||||
i18n_st.forms = new []string
|
||||
i18n_st.plural_rule = plural_rule_of(i18n_header(text, "Plural-Forms"))
|
||||
var np = 0
|
||||
var nlit = 0
|
||||
for i in 0 .. len(es) {
|
||||
|
|
@ -48,33 +48,33 @@ export function i18n_load_text(text: string) -> void {
|
|||
nlit += h + 1
|
||||
}
|
||||
}
|
||||
pat_out = new []string
|
||||
lits = new []string
|
||||
pat_l0 = words(np + 1)
|
||||
pat_nl = words(np + 1)
|
||||
pat_sc = words(np + 1)
|
||||
pat_ph = words(nlit + 1)
|
||||
nx_p = words(np + 1)
|
||||
nx_s = words(np + 1)
|
||||
nx_g = words(np + 1)
|
||||
pfx = Dict.new()
|
||||
sfx = Dict.new()
|
||||
gen_head = 0
|
||||
pat_n = 0
|
||||
i18n_st.pat_out = new []string
|
||||
i18n_st.lits = new []string
|
||||
i18n_st.pat_l0 = words(np + 1)
|
||||
i18n_st.pat_nl = words(np + 1)
|
||||
i18n_st.pat_sc = words(np + 1)
|
||||
i18n_st.pat_ph = words(nlit + 1)
|
||||
i18n_st.nx_p = words(np + 1)
|
||||
i18n_st.nx_s = words(np + 1)
|
||||
i18n_st.nx_g = words(np + 1)
|
||||
i18n_st.pfx = Dict.new()
|
||||
i18n_st.sfx = Dict.new()
|
||||
i18n_st.gen_head = 0
|
||||
i18n_st.pat_n = 0
|
||||
for i in 0 .. len(es) {
|
||||
let e = es[i]
|
||||
if len(e.plural) > 0 { plural_add(e) } else if not (blank(e.id) or blank(e.str)) {
|
||||
if holes(e.id) > 0 { add_pattern(e.id, e.str) } else {
|
||||
Dict.set(exact, e.id, len(out))
|
||||
push(out, 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(i18n_st, e.id, e.str) } else {
|
||||
Dict.set(i18n_st.exact, e.id, len(i18n_st.out))
|
||||
push(i18n_st.out, e.str)
|
||||
}
|
||||
}
|
||||
}
|
||||
on = true
|
||||
i18n_st.on = true
|
||||
}
|
||||
|
||||
# English again: nothing loaded, nothing translated
|
||||
export function i18n_off() -> void {
|
||||
on = false
|
||||
cur = "en"
|
||||
export function i18n_off(i18n_st: mut I18nState) -> void {
|
||||
i18n_st.on = false
|
||||
i18n_st.cur = "en"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,31 +12,33 @@ export property ItemChanged {
|
|||
after: int = 0
|
||||
}
|
||||
|
||||
var iv_keys: []string = null
|
||||
var iv_counts: []int = null
|
||||
var iv_changes: Queue<ItemChanged> = null
|
||||
export state InventoryState {
|
||||
iv_keys: []string = null
|
||||
iv_counts: []int = null
|
||||
iv_changes: Queue<ItemChanged> = null
|
||||
}
|
||||
|
||||
# the kinds, by the name each is saved under; the pack starts empty
|
||||
export function inv_config(keys: []string) -> void {
|
||||
iv_keys = keys
|
||||
inv_clear()
|
||||
export function inv_config(inventory_st: mut InventoryState, keys: []string) -> void {
|
||||
inventory_st.iv_keys = keys
|
||||
inv_clear(inventory_st)
|
||||
}
|
||||
|
||||
export function inv_changes() -> Queue<ItemChanged> {
|
||||
if iv_changes == null { iv_changes = queue_new("inventory.changes") }
|
||||
return iv_changes
|
||||
export function inv_changes(base_st: mut BaseState, inventory_st: mut InventoryState) -> Queue<ItemChanged> {
|
||||
if inventory_st.iv_changes == null { inventory_st.iv_changes = queue_new(base_st, "inventory.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 {
|
||||
if iv_keys == null { return 0 }
|
||||
return len(iv_keys)
|
||||
export function inv_kinds(inventory_st: InventoryState) -> int {
|
||||
if inventory_st.iv_keys == null { return 0 }
|
||||
return len(inventory_st.iv_keys)
|
||||
}
|
||||
|
||||
export function inv_count(it: int) -> int {
|
||||
if not iv_valid(it) { return 0 }
|
||||
return iv_counts[it]
|
||||
export function inv_count(inventory_st: InventoryState, it: int) -> int {
|
||||
if not iv_valid(inventory_st, it) { return 0 }
|
||||
return inventory_st.iv_counts[it]
|
||||
}
|
||||
|
||||
export function inv_limit(it: int) -> int {
|
||||
|
|
@ -44,16 +46,16 @@ export function inv_limit(it: int) -> int {
|
|||
}
|
||||
|
||||
# how many more fit
|
||||
export function inv_room(it: int) -> int {
|
||||
if not iv_valid(it) { return 0 }
|
||||
return Math.max(inv_limit(it) - iv_counts[it], 0)
|
||||
export function inv_room(inventory_st: InventoryState, it: int) -> int {
|
||||
if not iv_valid(inventory_st, it) { return 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
|
||||
if iv_counts == null { return 0 }
|
||||
for i in 0 .. len(iv_counts) { t += iv_counts[i] }
|
||||
if inventory_st.iv_counts == null { return 0 }
|
||||
for i in 0 .. len(inventory_st.iv_counts) { t += inventory_st.iv_counts[i] }
|
||||
return t
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
# 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
|
||||
export function inv_reset() -> void {
|
||||
inv_clear()
|
||||
q_clear(inv_changes())
|
||||
export function inv_reset(base_st: mut BaseState, inventory_st: mut InventoryState) -> void {
|
||||
inv_clear(inventory_st)
|
||||
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()
|
||||
for i in 0 .. inv_kinds() {
|
||||
if iv_counts[i] != 0 { sv_put_int(counts, iv_keys[i], iv_counts[i]) }
|
||||
for i in 0 .. inv_kinds(inventory_st) {
|
||||
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()
|
||||
Value.put(v, "counts", counts)
|
||||
|
|
@ -16,11 +16,11 @@ export function inv_save() -> Val {
|
|||
}
|
||||
|
||||
# a name this build does not know is dropped
|
||||
export function inv_load(v: Val, version: int) -> void {
|
||||
inv_clear()
|
||||
export function inv_load(inventory_st: mut InventoryState, v: Val, version: int) -> void {
|
||||
inv_clear(inventory_st)
|
||||
if Value.has(v, "counts") == 0 { return }
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -23,74 +23,74 @@ program InventoryTest {
|
|||
|
||||
bind PackRules { stack_limit: fn limit }
|
||||
|
||||
function fresh() -> void {
|
||||
inv_config(names("wood", "rod", "fish"))
|
||||
inv_reset()
|
||||
function fresh(base_st: mut BaseState, inventory_st: mut InventoryState) -> void {
|
||||
inv_config(inventory_st, names("wood", "rod", "fish"))
|
||||
inv_reset(base_st, inventory_st)
|
||||
}
|
||||
|
||||
function changes() -> []ItemChanged { return q_drain(inv_changes()) }
|
||||
function waiting() -> int { return q_len(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(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" {
|
||||
fresh()
|
||||
expect_eq(inv_add(WOOD, 7), 7)
|
||||
expect_eq(inv_add(WOOD, 7), 3)
|
||||
expect_eq(inv_count(WOOD), 10)
|
||||
expect_eq(inv_room(WOOD), 0)
|
||||
expect_eq(inv_add(ROD, 2), 1)
|
||||
expect_eq(inv_total(), 11)
|
||||
test "add puts in what fits and says how many" (base_st: mut BaseState, inventory_st: mut InventoryState) {
|
||||
fresh(base_st, inventory_st)
|
||||
expect_eq(inv_add(base_st, inventory_st, WOOD, 7), 7)
|
||||
expect_eq(inv_add(base_st, inventory_st, WOOD, 7), 3)
|
||||
expect_eq(inv_count(inventory_st, WOOD), 10)
|
||||
expect_eq(inv_room(inventory_st, WOOD), 0)
|
||||
expect_eq(inv_add(base_st, inventory_st, ROD, 2), 1)
|
||||
expect_eq(inv_total(inventory_st), 11)
|
||||
}
|
||||
|
||||
test "take is all or none" {
|
||||
fresh()
|
||||
inv_add(FISH, 3)
|
||||
expect(not inv_take(FISH, 4))
|
||||
expect_eq(inv_count(FISH), 3)
|
||||
expect(inv_take(FISH, 3))
|
||||
expect(not inv_has(FISH, 1))
|
||||
test "take is all or none" (base_st: mut BaseState, inventory_st: mut InventoryState) {
|
||||
fresh(base_st, inventory_st)
|
||||
inv_add(base_st, inventory_st, FISH, 3)
|
||||
expect(not inv_take(base_st, inventory_st, FISH, 4))
|
||||
expect_eq(inv_count(inventory_st, FISH), 3)
|
||||
expect(inv_take(base_st, inventory_st, FISH, 3))
|
||||
expect(not inv_has(inventory_st, FISH, 1))
|
||||
}
|
||||
|
||||
test "set goes past the room, never below zero" {
|
||||
fresh()
|
||||
inv_set(ROD, 3)
|
||||
expect_eq(inv_count(ROD), 3)
|
||||
expect_eq(inv_room(ROD), 0)
|
||||
inv_set(ROD, -2)
|
||||
expect_eq(inv_count(ROD), 0)
|
||||
test "set goes past the room, never below zero" (base_st: mut BaseState, inventory_st: mut InventoryState) {
|
||||
fresh(base_st, inventory_st)
|
||||
inv_set(base_st, inventory_st, ROD, 3)
|
||||
expect_eq(inv_count(inventory_st, ROD), 3)
|
||||
expect_eq(inv_room(inventory_st, ROD), 0)
|
||||
inv_set(base_st, inventory_st, ROD, -2)
|
||||
expect_eq(inv_count(inventory_st, ROD), 0)
|
||||
}
|
||||
|
||||
test "every change is a fact, and no change is not" {
|
||||
fresh()
|
||||
inv_add(WOOD, 2)
|
||||
inv_take(WOOD, 1)
|
||||
inv_set(WOOD, 1)
|
||||
let cs = changes()
|
||||
test "every change is a fact, and no change is not" (base_st: mut BaseState, inventory_st: mut InventoryState) {
|
||||
fresh(base_st, inventory_st)
|
||||
inv_add(base_st, inventory_st, WOOD, 2)
|
||||
inv_take(base_st, inventory_st, WOOD, 1)
|
||||
inv_set(base_st, inventory_st, WOOD, 1)
|
||||
let cs = changes(base_st, inventory_st)
|
||||
expect_eq(len(cs), 2)
|
||||
expect_eq(cs[0].after, 2)
|
||||
expect_eq(cs[1].before, 2)
|
||||
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" {
|
||||
fresh()
|
||||
expect_eq(inv_add(9, 1), 0)
|
||||
expect_eq(inv_count(-1), 0)
|
||||
expect(not inv_take(9, 0))
|
||||
test "an item out of range is nothing" (base_st: mut BaseState, inventory_st: mut InventoryState) {
|
||||
fresh(base_st, inventory_st)
|
||||
expect_eq(inv_add(base_st, inventory_st, 9, 1), 0)
|
||||
expect_eq(inv_count(inventory_st, -1), 0)
|
||||
expect(not inv_take(base_st, inventory_st, 9, 0))
|
||||
}
|
||||
|
||||
test "the save is by name, so a reordered table still reads it" {
|
||||
fresh()
|
||||
inv_add(WOOD, 4)
|
||||
inv_add(FISH, 2)
|
||||
test "the save is by name, so a reordered table still reads it" (base_st: mut BaseState, inventory_st: mut InventoryState) {
|
||||
fresh(base_st, inventory_st)
|
||||
inv_add(base_st, inventory_st, WOOD, 4)
|
||||
inv_add(base_st, inventory_st, FISH, 2)
|
||||
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)
|
||||
inv_config(names("fish", "bait", "wood"))
|
||||
inv_config(inventory_st, names("fish", "bait", "wood"))
|
||||
let n = load_section(save_decode(text), "inventory")
|
||||
inv_load(n.data, n.version)
|
||||
expect_eq(inv_count(0), 2)
|
||||
expect_eq(inv_count(1), 0)
|
||||
expect_eq(inv_count(2), 4)
|
||||
inv_load(inventory_st, n.data, n.version)
|
||||
expect_eq(inv_count(inventory_st, 0), 2)
|
||||
expect_eq(inv_count(inventory_st, 1), 0)
|
||||
expect_eq(inv_count(inventory_st, 2), 4)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,38 @@
|
|||
# verbs.ludic - the only way a count changes, and each change is said once
|
||||
function iv_change(it: int, after: int) -> void {
|
||||
let before = iv_counts[it]
|
||||
function iv_change(base_st: mut BaseState, inventory_st: mut InventoryState, it: int, after: int) -> void {
|
||||
let before = inventory_st.iv_counts[it]
|
||||
if before == after { return }
|
||||
iv_counts[it] = after
|
||||
inventory_st.iv_counts[it] = after
|
||||
let c = new ItemChanged
|
||||
c.item = it
|
||||
c.before = before
|
||||
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
|
||||
export function inv_add(it: int, n: int) -> int {
|
||||
if not iv_valid(it) or n <= 0 { return 0 }
|
||||
let k = Math.min(n, inv_room(it))
|
||||
if k > 0 { iv_change(it, iv_counts[it] + k) }
|
||||
export function inv_add(base_st: mut BaseState, inventory_st: mut InventoryState, it: int, n: int) -> int {
|
||||
if not iv_valid(inventory_st, it) or n <= 0 { return 0 }
|
||||
let k = Math.min(n, inv_room(inventory_st, it))
|
||||
if k > 0 { iv_change(base_st, inventory_st, it, inventory_st.iv_counts[it] + k) }
|
||||
return k
|
||||
}
|
||||
|
||||
# all n or none
|
||||
export function inv_take(it: int, n: int) -> bool {
|
||||
if not iv_valid(it) or iv_counts[it] < n { return false }
|
||||
if n > 0 { iv_change(it, iv_counts[it] - n) }
|
||||
export function inv_take(base_st: mut BaseState, inventory_st: mut InventoryState, it: int, n: int) -> bool {
|
||||
if not iv_valid(inventory_st, it) or inventory_st.iv_counts[it] < n { return false }
|
||||
if n > 0 { iv_change(base_st, inventory_st, it, inventory_st.iv_counts[it] - n) }
|
||||
return true
|
||||
}
|
||||
|
||||
# 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 {
|
||||
if not iv_valid(it) { return }
|
||||
iv_change(it, Math.max(n, 0))
|
||||
export function inv_set(base_st: mut BaseState, inventory_st: mut InventoryState, it: int, n: int) -> void {
|
||||
if not iv_valid(inventory_st, it) { return }
|
||||
iv_change(base_st, inventory_st, it, Math.max(n, 0))
|
||||
}
|
||||
|
||||
# everything gone, silently: a new trip, or before a load
|
||||
export function inv_clear() -> void {
|
||||
iv_counts = new []int
|
||||
for i in 0 .. inv_kinds() { push(iv_counts, 0) }
|
||||
export function inv_clear(inventory_st: mut InventoryState) -> void {
|
||||
inventory_st.iv_counts = new []int
|
||||
for i in 0 .. inv_kinds(inventory_st) { push(inventory_st.iv_counts, 0) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,88 +2,88 @@
|
|||
# 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 }
|
||||
|
||||
function jobs__at(b: int, s: int) -> int {
|
||||
jobs__ensure()
|
||||
return jobs__off[b] + s
|
||||
function jobs__at(jobs_st: mut JobsState, b: int, s: int) -> int {
|
||||
jobs__ensure(jobs_st)
|
||||
return jobs_st.jobs__off[b] + s
|
||||
}
|
||||
|
||||
# a whole number in [lo, hi] from the jobs' dice: what JobsWorld.roll draws with
|
||||
export function jobs_roll(lo: int, hi: int) -> int {
|
||||
jobs__ensure()
|
||||
return rng_between(jobs__dice, lo, hi)
|
||||
export function jobs_roll(jobs_st: mut JobsState, lo: int, hi: int) -> int {
|
||||
jobs__ensure(jobs_st)
|
||||
return rng_between(jobs_st.jobs__dice, lo, hi)
|
||||
}
|
||||
|
||||
# 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 {
|
||||
let k = jobs__at(b, s)
|
||||
jobs__pk[k] = kind
|
||||
jobs__pp[k] = param
|
||||
jobs__pn[k] = need
|
||||
jobs__pm[k] = money
|
||||
jobs__pr[k] = rep
|
||||
jobs__ph[k] = 0
|
||||
jobs__ps[k] = JOBS_ACTIVE
|
||||
jobs__pd[k] = JobsWorld.day()
|
||||
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(jobs_st, b, s)
|
||||
jobs_st.jobs__pk[k] = kind
|
||||
jobs_st.jobs__pp[k] = param
|
||||
jobs_st.jobs__pn[k] = need
|
||||
jobs_st.jobs__pm[k] = money
|
||||
jobs_st.jobs__pr[k] = rep
|
||||
jobs_st.jobs__ph[k] = 0
|
||||
jobs_st.jobs__ps[k] = JOBS_ACTIVE
|
||||
jobs_st.jobs__pd[k] = JobsWorld.day()
|
||||
let e = JobsWorld.evidence(kind, param)
|
||||
jobs__pb[k] = 0
|
||||
if e > 0 { jobs__pb[k] = e }
|
||||
jobs_st.jobs__pb[k] = 0
|
||||
if e > 0 { jobs_st.jobs__pb[k] = e }
|
||||
}
|
||||
|
||||
function jobs__clear(k: int) -> void {
|
||||
jobs__pk[k] = -1
|
||||
jobs__pp[k] = -1
|
||||
jobs__pn[k] = 0
|
||||
jobs__ph[k] = 0
|
||||
jobs__ps[k] = JOBS_OFFERED
|
||||
function jobs__clear(jobs_st: mut JobsState, k: int) -> void {
|
||||
jobs_st.jobs__pk[k] = -1
|
||||
jobs_st.jobs__pp[k] = -1
|
||||
jobs_st.jobs__pn[k] = 0
|
||||
jobs_st.jobs__ph[k] = 0
|
||||
jobs_st.jobs__ps[k] = JOBS_OFFERED
|
||||
}
|
||||
|
||||
# take a slot down and ask the game for what replaces it
|
||||
function jobs__repost(b: int, s: int) -> void {
|
||||
let k = jobs__at(b, s)
|
||||
function jobs__repost(base_st: mut BaseState, jobs_st: mut JobsState, b: int, s: int) -> void {
|
||||
let k = jobs__at(jobs_st, b, s)
|
||||
let day = JobsWorld.day()
|
||||
rng_seed(jobs__dice, jobs__seed + day * 7 + b * 7919 + s * 104729 + jobs__serial[b] * 31)
|
||||
jobs__serial[b] += 1
|
||||
jobs__clear(k)
|
||||
rng_seed(jobs_st.jobs__dice, jobs_st.jobs__seed + day * 7 + b * 7919 + s * 104729 + jobs_st.jobs__serial[b] * 31)
|
||||
jobs_st.jobs__serial[b] += 1
|
||||
jobs__clear(jobs_st, k)
|
||||
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
|
||||
# finished on an earlier day
|
||||
export function jobs_morning() -> void {
|
||||
jobs__ensure()
|
||||
export function jobs_morning(base_st: mut BaseState, jobs_st: mut JobsState) -> void {
|
||||
jobs__ensure(jobs_st)
|
||||
let day = JobsWorld.day()
|
||||
for b in 0 .. BOARD_COUNT {
|
||||
let bd = JobBoards[b]
|
||||
if not JobsWorld.owns(bd.scope) { continue }
|
||||
for s in 0 .. bd.slots {
|
||||
let k = jobs__off[b] + s
|
||||
let finished = jobs__ps[k] == JOBS_DONE and day > jobs__pd[k]
|
||||
if jobs__pk[k] < 0 or finished or day - jobs__pd[k] >= bd.keep { jobs__repost(b, s) }
|
||||
let k = jobs_st.jobs__off[b] + s
|
||||
let finished = jobs_st.jobs__ps[k] == JOBS_DONE and day > jobs_st.jobs__pd[k]
|
||||
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 {
|
||||
let k = jobs__off[b] + s
|
||||
let was = jobs__ph[k]
|
||||
jobs__ph[k] = have
|
||||
if jobs__ph[k] > jobs__pn[k] { jobs__ph[k] = jobs__pn[k] }
|
||||
if jobs__ph[k] > was { jobs__fact(JOBS_F_PROGRESSED, -1, b, s, jobs__ph[k], jobs__pn[k]) }
|
||||
if jobs__ps[k] != JOBS_ACTIVE or jobs__ph[k] < jobs__pn[k] { return }
|
||||
jobs__ps[k] = JOBS_READY
|
||||
jobs__fact(JOBS_F_COMPLETED, -1, b, s, jobs__ph[k], jobs__pn[k])
|
||||
if JobBoards[b].auto { jobs_post_hand_in(b, s) }
|
||||
function jobs__post_moved(base_st: mut BaseState, jobs_st: mut JobsState, b: int, s: int, have: int) -> void {
|
||||
let k = jobs_st.jobs__off[b] + s
|
||||
let was = jobs_st.jobs__ph[k]
|
||||
jobs_st.jobs__ph[k] = have
|
||||
if jobs_st.jobs__ph[k] > jobs_st.jobs__pn[k] { jobs_st.jobs__ph[k] = jobs_st.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_st.jobs__ps[k] != JOBS_ACTIVE or jobs_st.jobs__ph[k] < jobs_st.jobs__pn[k] { return }
|
||||
jobs_st.jobs__ps[k] = JOBS_READY
|
||||
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(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 }
|
||||
let k = jobs__at(b, s)
|
||||
if jobs__ps[k] != JOBS_READY { return false }
|
||||
jobs__ps[k] = JOBS_DONE
|
||||
jobs__total[b] += 1
|
||||
JobsPay.reward(JobBoards[b].scope, jobs__pm[k], jobs__pr[k])
|
||||
jobs__fact(JOBS_F_HANDED_IN, -1, b, s, jobs__ph[k], jobs__pn[k])
|
||||
if JobBoards[b].refill { jobs__repost(b, s) }
|
||||
let k = jobs__at(jobs_st, b, s)
|
||||
if jobs_st.jobs__ps[k] != JOBS_READY { return false }
|
||||
jobs_st.jobs__ps[k] = JOBS_DONE
|
||||
jobs_st.jobs__total[b] += 1
|
||||
JobsPay.reward(JobBoards[b].scope, jobs_st.jobs__pm[k], jobs_st.jobs__pr[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(base_st, jobs_st, b, s) }
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
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 {
|
||||
if only >= 0 { return scope == only }
|
||||
return JobsWorld.owns(scope)
|
||||
}
|
||||
|
||||
function jobs__count(only: int, kind: int, param: int, n: int) -> void {
|
||||
jobs__ensure()
|
||||
function jobs__count(base_st: mut BaseState, jobs_st: mut JobsState, only: int, kind: int, param: int, n: int) -> void {
|
||||
jobs__ensure(jobs_st)
|
||||
for i in 0 .. JOB_COUNT {
|
||||
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 }
|
||||
let was = jobs__hv[i]
|
||||
jobs__hv[i] = was + n
|
||||
jobs__moved(i, was)
|
||||
let was = jobs_st.jobs__hv[i]
|
||||
jobs_st.jobs__hv[i] = was + n
|
||||
jobs__moved(base_st, jobs_st, i, was)
|
||||
}
|
||||
for b in 0 .. BOARD_COUNT {
|
||||
if not jobs__takes(only, JobBoards[b].scope) { continue }
|
||||
for s in 0 .. JobBoards[b].slots {
|
||||
let k = jobs__off[b] + s
|
||||
if jobs__ps[k] != JOBS_ACTIVE or not jobs__matches(jobs__pk[k], jobs__pp[k], kind, param) { continue }
|
||||
jobs__post_moved(b, s, jobs__ph[k] + n)
|
||||
let k = jobs_st.jobs__off[b] + s
|
||||
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(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]
|
||||
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)
|
||||
if e <= jobs__hv[i] { return }
|
||||
let was = jobs__hv[i]
|
||||
jobs__hv[i] = e
|
||||
jobs__moved(i, was)
|
||||
if e <= jobs_st.jobs__hv[i] { return }
|
||||
let was = jobs_st.jobs__hv[i]
|
||||
jobs_st.jobs__hv[i] = e
|
||||
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
|
||||
# for a written job; a post counts only what came after it went up
|
||||
export function jobs_recheck() -> void {
|
||||
jobs__ensure()
|
||||
for i in 0 .. JOB_COUNT { jobs__recheck_one(i) }
|
||||
export function jobs_recheck(base_st: mut BaseState, jobs_st: mut JobsState) -> void {
|
||||
jobs__ensure(jobs_st)
|
||||
for i in 0 .. JOB_COUNT { jobs__recheck_one(base_st, jobs_st, i) }
|
||||
for b in 0 .. BOARD_COUNT {
|
||||
if not JobsWorld.owns(JobBoards[b].scope) { continue }
|
||||
for s in 0 .. JobBoards[b].slots {
|
||||
let k = jobs__off[b] + s
|
||||
if jobs__ps[k] != JOBS_ACTIVE or jobs__pk[k] < 0 { continue }
|
||||
let e = JobsWorld.evidence(jobs__pk[k], jobs__pp[k])
|
||||
if e >= 0 and e - jobs__pb[k] > jobs__ph[k] { jobs__post_moved(b, s, e - jobs__pb[k]) }
|
||||
let k = jobs_st.jobs__off[b] + s
|
||||
if jobs_st.jobs__ps[k] != JOBS_ACTIVE or jobs_st.jobs__pk[k] < 0 { continue }
|
||||
let e = JobsWorld.evidence(jobs_st.jobs__pk[k], jobs_st.jobs__pp[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
|
||||
function jobs__offers() -> void {
|
||||
jobs__ensure()
|
||||
function jobs__offers(base_st: mut BaseState, jobs_st: mut JobsState) -> void {
|
||||
jobs__ensure(jobs_st)
|
||||
for i in 0 .. JOB_COUNT {
|
||||
let now = jobs_offered(i)
|
||||
if now and not jobs__seen[i] and jobs__primed { jobs__fact(JOBS_F_OFFERED, i, -1, -1, 0, Jobs[i].need) }
|
||||
jobs__seen[i] = now
|
||||
let now = jobs_offered(jobs_st, i)
|
||||
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_st.jobs__seen[i] = now
|
||||
}
|
||||
jobs__primed = true
|
||||
jobs_st.jobs__primed = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,96 +1,96 @@
|
|||
# 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_state(i: int) -> int {
|
||||
jobs__ensure()
|
||||
return jobs__st[i]
|
||||
export function jobs_state(jobs_st: mut JobsState, i: int) -> int {
|
||||
jobs__ensure(jobs_st)
|
||||
return jobs_st.jobs__st[i]
|
||||
}
|
||||
|
||||
export function jobs_have(i: int) -> int {
|
||||
jobs__ensure()
|
||||
return jobs__hv[i]
|
||||
export function jobs_have(jobs_st: mut JobsState, i: int) -> int {
|
||||
jobs__ensure(jobs_st)
|
||||
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
|
||||
export function jobs_under_way(i: int) -> bool {
|
||||
let s = jobs_state(i)
|
||||
export function jobs_under_way(jobs_st: mut JobsState, i: int) -> bool {
|
||||
let s = jobs_state(jobs_st, i)
|
||||
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
|
||||
export function jobs_offered(i: int) -> bool {
|
||||
export function jobs_offered(jobs_st: mut JobsState, i: int) -> bool {
|
||||
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)
|
||||
}
|
||||
|
||||
export function jobs_take(i: int) -> bool {
|
||||
if not jobs_ok(i) or jobs_state(i) != JOBS_OFFERED { return false }
|
||||
jobs__st[i] = JOBS_ACTIVE
|
||||
jobs__hv[i] = 0
|
||||
jobs__recheck_one(i)
|
||||
export function jobs_take(base_st: mut BaseState, jobs_st: mut JobsState, i: int) -> bool {
|
||||
if not jobs_ok(i) or jobs_state(jobs_st, i) != JOBS_OFFERED { return false }
|
||||
jobs_st.jobs__st[i] = JOBS_ACTIVE
|
||||
jobs_st.jobs__hv[i] = 0
|
||||
jobs__recheck_one(base_st, jobs_st, i)
|
||||
return true
|
||||
}
|
||||
|
||||
# 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]
|
||||
if d.need > 0 and jobs__hv[i] > d.need { 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[i] == JOBS_ACTIVE and d.need > 0 and jobs__hv[i] >= d.need { jobs_ready(i) }
|
||||
if d.need > 0 and jobs_st.jobs__hv[i] > d.need { jobs_st.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.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
|
||||
export function jobs_set_have(i: int, v: int) -> void {
|
||||
let was = jobs_have(i)
|
||||
jobs__hv[i] = v
|
||||
jobs__moved(i, was)
|
||||
export function jobs_set_have(base_st: mut BaseState, jobs_st: mut JobsState, i: int, v: int) -> void {
|
||||
let was = jobs_have(jobs_st, i)
|
||||
jobs_st.jobs__hv[i] = v
|
||||
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
|
||||
export function jobs_ready(i: int) -> void {
|
||||
if jobs_state(i) != JOBS_ACTIVE { return }
|
||||
jobs__st[i] = JOBS_READY
|
||||
jobs__fact(JOBS_F_COMPLETED, i, -1, -1, jobs__hv[i], Jobs[i].need)
|
||||
export function jobs_ready(base_st: mut BaseState, jobs_st: mut JobsState, i: int) -> void {
|
||||
if jobs_state(jobs_st, i) != JOBS_ACTIVE { return }
|
||||
jobs_st.jobs__st[i] = JOBS_READY
|
||||
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 {
|
||||
if not jobs_ok(i) or jobs_state(i) != JOBS_READY { return false }
|
||||
export function jobs_hand_in(base_st: mut BaseState, jobs_st: mut JobsState, i: int) -> bool {
|
||||
if not jobs_ok(i) or jobs_state(jobs_st, i) != JOBS_READY { return false }
|
||||
let d = Jobs[i]
|
||||
jobs__st[i] = JOBS_DONE
|
||||
if d.group >= 0 and d.group < JOBS_GROUPS { jobs__done_n[d.group] += 1 }
|
||||
jobs_st.jobs__st[i] = JOBS_DONE
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
# finished and paid in one go, whatever the count says
|
||||
export function jobs_complete(i: int) -> bool {
|
||||
if not jobs_ok(i) or not jobs_under_way(i) { return false }
|
||||
jobs_ready(i)
|
||||
return jobs_hand_in(i)
|
||||
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(jobs_st, i) { return false }
|
||||
jobs_ready(base_st, jobs_st, i)
|
||||
return jobs_hand_in(base_st, jobs_st, i)
|
||||
}
|
||||
|
||||
# the state outright: the host's word for a party job, a test
|
||||
export function jobs_set(i: int, state: int, have: int) -> void {
|
||||
jobs__ensure()
|
||||
jobs__st[i] = state
|
||||
jobs__hv[i] = have
|
||||
export function jobs_set(jobs_st: mut JobsState, i: int, state: int, have: int) -> void {
|
||||
jobs__ensure(jobs_st)
|
||||
jobs_st.jobs__st[i] = state
|
||||
jobs_st.jobs__hv[i] = have
|
||||
}
|
||||
|
||||
export function jobs_done(group: int) -> int {
|
||||
jobs__ensure()
|
||||
export function jobs_done(jobs_st: mut JobsState, group: int) -> int {
|
||||
jobs__ensure(jobs_st)
|
||||
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 {
|
||||
jobs__ensure()
|
||||
if group >= 0 and group < JOBS_GROUPS { jobs__done_n[group] = n }
|
||||
export function jobs_set_done(jobs_st: mut JobsState, group: int, n: int) -> void {
|
||||
jobs__ensure(jobs_st)
|
||||
if group >= 0 and group < JOBS_GROUPS { jobs_st.jobs__done_n[group] = n }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,36 @@
|
|||
# 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)
|
||||
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
|
||||
for i in 0 .. JOB_COUNT {
|
||||
let d = Jobs[i]
|
||||
if d.group != group or (vendor >= 0 and d.vendor != vendor) { continue }
|
||||
let s = jobs_state(i)
|
||||
if s == JOBS_DONE or (s == JOBS_OFFERED and not jobs_offered(i)) { continue }
|
||||
let s = jobs_state(jobs_st, i)
|
||||
if s == JOBS_DONE or (s == JOBS_OFFERED and not jobs_offered(jobs_st, i)) { continue }
|
||||
push(out, i)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
# 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
|
||||
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
|
||||
}
|
||||
|
||||
# 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
|
||||
for i in 0 .. JOB_COUNT {
|
||||
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
|
||||
}
|
||||
|
||||
# the first under way and not yet met, or -1
|
||||
export function jobs_first_open(group: int) -> int {
|
||||
for i in 0 .. JOB_COUNT { if Jobs[i].group == group and jobs_state(i) == JOBS_ACTIVE { return i } }
|
||||
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(jobs_st, i) == JOBS_ACTIVE { return i } }
|
||||
return -1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
# 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_param(b: int, s: int) -> int { return jobs__pp[jobs__at(b, s)] }
|
||||
export function jobs_post_need(b: int, s: int) -> int { return jobs__pn[jobs__at(b, s)] }
|
||||
export function jobs_post_have(b: int, s: int) -> int { return jobs__ph[jobs__at(b, s)] }
|
||||
export function jobs_post_money(b: int, s: int) -> int { return jobs__pm[jobs__at(b, s)] }
|
||||
export function jobs_post_rep(b: int, s: int) -> int { return jobs__pr[jobs__at(b, s)] }
|
||||
export function jobs_post_state(b: int, s: int) -> int { return jobs__ps[jobs__at(b, s)] }
|
||||
export function jobs_post_day(b: int, s: int) -> int { return jobs__pd[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(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(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(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(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(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(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(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 {
|
||||
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
|
||||
export function jobs_posted(b: int) -> []int {
|
||||
export function jobs_posted(jobs_st: mut JobsState, b: int) -> []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
|
||||
}
|
||||
|
||||
# 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
|
||||
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
|
||||
}
|
||||
|
||||
export function jobs_total(b: int) -> int {
|
||||
jobs__ensure()
|
||||
return jobs__total[b]
|
||||
export function jobs_total(jobs_st: mut JobsState, b: int) -> int {
|
||||
jobs__ensure(jobs_st)
|
||||
return jobs_st.jobs__total[b]
|
||||
}
|
||||
|
||||
# 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 {
|
||||
let k = jobs__at(b, s)
|
||||
jobs__pk[k] = kind
|
||||
jobs__pp[k] = param
|
||||
jobs__pn[k] = need
|
||||
jobs__ph[k] = have
|
||||
jobs__pm[k] = money
|
||||
jobs__pr[k] = rep
|
||||
jobs__ps[k] = state
|
||||
jobs__pd[k] = day
|
||||
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(jobs_st, b, s)
|
||||
jobs_st.jobs__pk[k] = kind
|
||||
jobs_st.jobs__pp[k] = param
|
||||
jobs_st.jobs__pn[k] = need
|
||||
jobs_st.jobs__ph[k] = have
|
||||
jobs_st.jobs__pm[k] = money
|
||||
jobs_st.jobs__pr[k] = rep
|
||||
jobs_st.jobs__ps[k] = state
|
||||
jobs_st.jobs__pd[k] = day
|
||||
}
|
||||
|
||||
export function jobs_set_total(b: int, n: int) -> void {
|
||||
jobs__ensure()
|
||||
jobs__total[b] = n
|
||||
export function jobs_set_total(jobs_st: mut JobsState, b: int, n: int) -> void {
|
||||
jobs__ensure(jobs_st)
|
||||
jobs_st.jobs__total[b] = n
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# removed is dropped, a new one starts offered
|
||||
function jobs__reset_scope(sc: int) -> void {
|
||||
jobs__ensure()
|
||||
function jobs__reset_scope(jobs_st: mut JobsState, sc: int) -> void {
|
||||
jobs__ensure(jobs_st)
|
||||
for i in 0 .. JOB_COUNT {
|
||||
if Jobs[i].scope != sc { continue }
|
||||
jobs__st[i] = JOBS_OFFERED
|
||||
jobs__hv[i] = 0
|
||||
jobs_st.jobs__st[i] = JOBS_OFFERED
|
||||
jobs_st.jobs__hv[i] = 0
|
||||
}
|
||||
for b in 0 .. BOARD_COUNT {
|
||||
if JobBoards[b].scope != sc { continue }
|
||||
for s in 0 .. JobBoards[b].slots { jobs__clear(jobs__off[b] + s) }
|
||||
jobs__total[b] = 0
|
||||
jobs__serial[b] = 0
|
||||
for s in 0 .. JobBoards[b].slots { jobs__clear(jobs_st, jobs_st.jobs__off[b] + s) }
|
||||
jobs_st.jobs__total[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 js = Value.object()
|
||||
for i in 0 .. JOB_COUNT {
|
||||
if Jobs[i].scope != sc or jobs__st[i] == JOBS_OFFERED { continue }
|
||||
sv_put_ints(js, Jobs[i].key, [jobs__st[i], jobs__hv[i]])
|
||||
if Jobs[i].scope != sc or jobs_st.jobs__st[i] == JOBS_OFFERED { continue }
|
||||
sv_put_ints(js, Jobs[i].key, [jobs_st.jobs__st[i], jobs_st.jobs__hv[i]])
|
||||
}
|
||||
Value.put(v, "jobs", js)
|
||||
let bs = Value.object()
|
||||
for b in 0 .. BOARD_COUNT {
|
||||
if JobBoards[b].scope != sc { continue }
|
||||
let o = Value.object()
|
||||
sv_put_int(o, "total", jobs__total[b])
|
||||
sv_put_int(o, "serial", jobs__serial[b])
|
||||
sv_put_int(o, "total", jobs_st.jobs__total[b])
|
||||
sv_put_int(o, "serial", jobs_st.jobs__serial[b])
|
||||
let slots = new []int
|
||||
for s in 0 .. JobBoards[b].slots {
|
||||
let k = 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 k = jobs_st.jobs__off[b] + s
|
||||
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]) }
|
||||
}
|
||||
sv_put_ints(o, "slots", slots)
|
||||
|
|
@ -43,13 +43,13 @@ function jobs__save_scope(sc: int) -> Val {
|
|||
return v
|
||||
}
|
||||
|
||||
function jobs__load_scope(sc: int, v: Val) -> void {
|
||||
jobs__reset_scope(sc)
|
||||
function jobs__load_scope(jobs_st: mut JobsState, sc: int, v: Val) -> void {
|
||||
jobs__reset_scope(jobs_st, sc)
|
||||
if Value.has(v, "jobs") != 0 {
|
||||
let js = Value.get(v, "jobs")
|
||||
for i in 0 .. JOB_COUNT {
|
||||
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 }
|
||||
|
|
@ -57,15 +57,15 @@ function jobs__load_scope(sc: int, v: Val) -> void {
|
|||
for b in 0 .. BOARD_COUNT {
|
||||
if JobBoards[b].scope != sc or Value.has(bs, JobBoards[b].key) == 0 { continue }
|
||||
let o = Value.get(bs, JobBoards[b].key)
|
||||
jobs__total[b] = sv_int(o, "total", 0)
|
||||
jobs__serial[b] = sv_int(o, "serial", 0)
|
||||
jobs_st.jobs__total[b] = sv_int(o, "total", 0)
|
||||
jobs_st.jobs__serial[b] = sv_int(o, "serial", 0)
|
||||
let r = sv_ints(o, "slots")
|
||||
for s in 0 .. JobBoards[b].slots {
|
||||
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
|
||||
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__pb[k] = r[q + 4]
|
||||
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_st.jobs__pb[k] = r[q + 4]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,30 @@
|
|||
# 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.
|
||||
var jobs__st: []int = null
|
||||
var jobs__hv: []int = null
|
||||
var jobs__seen: []bool = null # an offer already said
|
||||
var jobs__primed: bool = false # the first tick after a reset learns the offers quietly
|
||||
var jobs__done_n: []int = null # handed in, per group
|
||||
export state JobsState {
|
||||
jobs__st: []int = null
|
||||
jobs__hv: []int = null
|
||||
jobs__seen: []bool = null # an offer already said
|
||||
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]
|
||||
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 {
|
||||
let out = new []int
|
||||
|
|
@ -30,39 +32,39 @@ function jobs__ints(n: int, v: int) -> []int {
|
|||
return out
|
||||
}
|
||||
|
||||
function jobs__ensure() -> void {
|
||||
if jobs__st != null { return }
|
||||
jobs__st = jobs__ints(JOB_COUNT, JOBS_OFFERED)
|
||||
jobs__hv = jobs__ints(JOB_COUNT, 0)
|
||||
jobs__seen = new []bool
|
||||
for i in 0 .. JOB_COUNT { push(jobs__seen, false) }
|
||||
jobs__done_n = jobs__ints(JOBS_GROUPS, 0)
|
||||
jobs__off = new []int
|
||||
function jobs__ensure(jobs_st: mut JobsState) -> void {
|
||||
if jobs_st.jobs__st != null { return }
|
||||
jobs_st.jobs__st = jobs__ints(JOB_COUNT, JOBS_OFFERED)
|
||||
jobs_st.jobs__hv = jobs__ints(JOB_COUNT, 0)
|
||||
jobs_st.jobs__seen = new []bool
|
||||
for i in 0 .. JOB_COUNT { push(jobs_st.jobs__seen, false) }
|
||||
jobs_st.jobs__done_n = jobs__ints(JOBS_GROUPS, 0)
|
||||
jobs_st.jobs__off = new []int
|
||||
var n = 0
|
||||
for b in 0 .. BOARD_COUNT {
|
||||
push(jobs__off, n)
|
||||
push(jobs_st.jobs__off, n)
|
||||
n += JobBoards[b].slots
|
||||
}
|
||||
jobs__pk = jobs__ints(n, -1)
|
||||
jobs__pp = jobs__ints(n, -1)
|
||||
jobs__pn = jobs__ints(n, 0)
|
||||
jobs__ph = jobs__ints(n, 0)
|
||||
jobs__pb = jobs__ints(n, 0)
|
||||
jobs__pm = jobs__ints(n, 0)
|
||||
jobs__pr = jobs__ints(n, 0)
|
||||
jobs__ps = jobs__ints(n, JOBS_OFFERED)
|
||||
jobs__pd = jobs__ints(n, 0)
|
||||
jobs__total = jobs__ints(BOARD_COUNT, 0)
|
||||
jobs__serial = jobs__ints(BOARD_COUNT, 0)
|
||||
jobs__dice = rng_new(jobs__seed)
|
||||
jobs_st.jobs__pk = jobs__ints(n, -1)
|
||||
jobs_st.jobs__pp = jobs__ints(n, -1)
|
||||
jobs_st.jobs__pn = jobs__ints(n, 0)
|
||||
jobs_st.jobs__ph = jobs__ints(n, 0)
|
||||
jobs_st.jobs__pb = jobs__ints(n, 0)
|
||||
jobs_st.jobs__pm = jobs__ints(n, 0)
|
||||
jobs_st.jobs__pr = jobs__ints(n, 0)
|
||||
jobs_st.jobs__ps = jobs__ints(n, JOBS_OFFERED)
|
||||
jobs_st.jobs__pd = jobs__ints(n, 0)
|
||||
jobs_st.jobs__total = jobs__ints(BOARD_COUNT, 0)
|
||||
jobs_st.jobs__serial = jobs__ints(BOARD_COUNT, 0)
|
||||
jobs_st.jobs__dice = rng_new(jobs_st.jobs__seed)
|
||||
}
|
||||
|
||||
export function jobs_facts() -> Queue<JobsFact> {
|
||||
if jobs__q == null { jobs__q = queue_new("jobs.facts") }
|
||||
return jobs__q
|
||||
export function jobs_facts(base_st: mut BaseState, jobs_st: mut JobsState) -> Queue<JobsFact> {
|
||||
if jobs_st.jobs__q == null { jobs_st.jobs__q = queue_new(base_st, "jobs.facts") }
|
||||
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
|
||||
f.what = what
|
||||
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.scope = Jobs[job].scope
|
||||
} else {
|
||||
f.kind = jobs__pk[jobs__off[b] + s]
|
||||
f.param = jobs__pp[jobs__off[b] + s]
|
||||
f.money = jobs__pm[jobs__off[b] + s]
|
||||
f.rep = jobs__pr[jobs__off[b] + s]
|
||||
f.kind = jobs_st.jobs__pk[jobs_st.jobs__off[b] + s]
|
||||
f.param = jobs_st.jobs__pp[jobs_st.jobs__off[b] + s]
|
||||
f.money = jobs_st.jobs__pm[jobs_st.jobs__off[b] + s]
|
||||
f.rep = jobs_st.jobs__pr[jobs_st.jobs__off[b] + s]
|
||||
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
|
||||
export function jobs_config_seed(seed: int) -> void {
|
||||
jobs__seed = seed
|
||||
jobs__dice = rng_new(seed)
|
||||
export function jobs_config_seed(jobs_st: mut JobsState, seed: int) -> void {
|
||||
jobs_st.jobs__seed = seed
|
||||
jobs_st.jobs__dice = rng_new(seed)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,39 +1,39 @@
|
|||
# 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
|
||||
export function jobs_reset() -> void {
|
||||
jobs__reset_scope(JOBS_SELF)
|
||||
for g in 0 .. JOBS_GROUPS { jobs__done_n[g] = 0 }
|
||||
q_clear(jobs_facts())
|
||||
export function jobs_reset(base_st: mut BaseState, jobs_st: mut JobsState) -> void {
|
||||
jobs__reset_scope(jobs_st, JOBS_SELF)
|
||||
for g in 0 .. JOBS_GROUPS { jobs_st.jobs__done_n[g] = 0 }
|
||||
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 {
|
||||
jobs__ensure()
|
||||
let v = jobs__save_scope(JOBS_SELF)
|
||||
sv_put_ints(v, "done", jobs__done_n)
|
||||
export function jobs_save(jobs_st: mut JobsState) -> Val {
|
||||
jobs__ensure(jobs_st)
|
||||
let v = jobs__save_scope(jobs_st, JOBS_SELF)
|
||||
sv_put_ints(v, "done", jobs_st.jobs__done_n)
|
||||
return v
|
||||
}
|
||||
|
||||
export function jobs_load(v: Val, version: int) -> void {
|
||||
jobs__load_scope(JOBS_SELF, v)
|
||||
export function jobs_load(jobs_st: mut JobsState, v: Val, version: int) -> void {
|
||||
jobs__load_scope(jobs_st, JOBS_SELF, v)
|
||||
let d = sv_ints(v, "done")
|
||||
for g in 0 .. JOBS_GROUPS {
|
||||
jobs__done_n[g] = 0
|
||||
if g < len(d) { jobs__done_n[g] = d[g] }
|
||||
jobs_st.jobs__done_n[g] = 0
|
||||
if g < len(d) { jobs_st.jobs__done_n[g] = d[g] }
|
||||
}
|
||||
}
|
||||
|
||||
export function jobs_party_save() -> Val {
|
||||
jobs__ensure()
|
||||
return jobs__save_scope(JOBS_PARTY)
|
||||
export function jobs_party_save(jobs_st: mut JobsState) -> Val {
|
||||
jobs__ensure(jobs_st)
|
||||
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 {
|
||||
jobs__offers()
|
||||
jobs_recheck()
|
||||
function jobs__tick(base_st: mut BaseState, jobs_st: mut JobsState, t: Tick) -> void {
|
||||
jobs__offers(base_st, jobs_st)
|
||||
jobs_recheck(base_st, jobs_st)
|
||||
}
|
||||
|
||||
export function jobs_system() -> System {
|
||||
|
|
|
|||
|
|
@ -15,50 +15,52 @@ program JobsTest {
|
|||
def JobBoards daily { slots: 3, keep: 1, auto: true, refill: false }
|
||||
def JobBoards ada { slots: 2, keep: 7, scope: JOBS_PARTY }
|
||||
|
||||
var rep: int = 0
|
||||
var stage: int = 0
|
||||
var day: int = 1
|
||||
var guest: bool = false
|
||||
var fish: int = 0 # the state's count of fish
|
||||
var paid: int = 0
|
||||
var paid_rep: int = 0
|
||||
var paid_scope: int = -1
|
||||
state JobstestState {
|
||||
rep: int = 0
|
||||
stage: int = 0
|
||||
day: int = 1
|
||||
guest: bool = false
|
||||
fish: int = 0 # the state's count of fish
|
||||
paid: int = 0
|
||||
paid_rep: int = 0
|
||||
paid_scope: int = -1
|
||||
}
|
||||
|
||||
function world_rep(sc: int) -> int { return rep }
|
||||
function world_stage() -> int { return stage }
|
||||
function world_day() -> int { return day }
|
||||
function world_owns(sc: int) -> bool { return sc == JOBS_SELF or not guest }
|
||||
function world_evidence(kind: int, param: int) -> int {
|
||||
if kind == K_FISH { return fish }
|
||||
function world_rep(jobstest_st: JobstestState, sc: int) -> int { return jobstest_st.rep }
|
||||
function world_stage(jobstest_st: JobstestState) -> int { return jobstest_st.stage }
|
||||
function world_day(jobstest_st: JobstestState) -> int { return jobstest_st.day }
|
||||
function world_owns(jobstest_st: JobstestState, sc: int) -> bool { return sc == JOBS_SELF or not jobstest_st.guest }
|
||||
function world_evidence(jobstest_st: JobstestState, kind: int, param: int) -> int {
|
||||
if kind == K_FISH { return jobstest_st.fish }
|
||||
return -1
|
||||
}
|
||||
function world_roll(b: int, s: int) -> void {
|
||||
let need = jobs_roll(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) }
|
||||
function world_roll(jobs_st: mut JobsState, b: int, s: int) -> void {
|
||||
let need = jobs_roll(jobs_st, 2, 4)
|
||||
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 {
|
||||
paid += money
|
||||
paid_rep += r
|
||||
paid_scope = sc
|
||||
function pay(jobstest_st: mut JobstestState, sc: int, money: int, r: int) -> void {
|
||||
jobstest_st.paid += money
|
||||
jobstest_st.paid_rep += r
|
||||
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 JobsPay { reward: fn pay }
|
||||
|
||||
function fresh() -> void {
|
||||
rep = 0
|
||||
stage = 0
|
||||
day = 1
|
||||
guest = false
|
||||
fish = 0
|
||||
paid = 0
|
||||
paid_rep = 0
|
||||
jobs_reset()
|
||||
jobs_party_reset()
|
||||
function fresh(base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) -> void {
|
||||
jobstest_st.rep = 0
|
||||
jobstest_st.stage = 0
|
||||
jobstest_st.day = 1
|
||||
jobstest_st.guest = false
|
||||
jobstest_st.fish = 0
|
||||
jobstest_st.paid = 0
|
||||
jobstest_st.paid_rep = 0
|
||||
jobs_reset(base_st, jobs_st)
|
||||
jobs_party_reset(jobs_st)
|
||||
}
|
||||
|
||||
function count_of(what: int) -> int {
|
||||
let fs = q_drain(jobs_facts())
|
||||
function count_of(base_st: mut BaseState, jobs_st: mut JobsState, what: int) -> int {
|
||||
let fs = q_drain(base_st, jobs_facts(base_st, jobs_st))
|
||||
var n = 0
|
||||
for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } }
|
||||
return n
|
||||
|
|
@ -70,166 +72,166 @@ program JobsTest {
|
|||
expect_eq(BOARD_COUNT, 2)
|
||||
}
|
||||
|
||||
test "a gate of standing and story" {
|
||||
fresh()
|
||||
expect(jobs_offered(JOB_CATCH))
|
||||
expect(not jobs_offered(JOB_DEER))
|
||||
rep = 10
|
||||
expect(jobs_offered(JOB_DEER))
|
||||
expect(not jobs_offered(JOB_STILL))
|
||||
stage = 2
|
||||
expect(jobs_offered(JOB_STILL))
|
||||
expect_eq(len(jobs_at(0, -1)), 3)
|
||||
test "a gate of standing and story" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
|
||||
fresh(base_st, jobs_st, jobstest_st)
|
||||
expect(jobs_offered(jobs_st, JOB_CATCH))
|
||||
expect(not jobs_offered(jobs_st, JOB_DEER))
|
||||
jobstest_st.rep = 10
|
||||
expect(jobs_offered(jobs_st, JOB_DEER))
|
||||
expect(not jobs_offered(jobs_st, JOB_STILL))
|
||||
jobstest_st.stage = 2
|
||||
expect(jobs_offered(jobs_st, JOB_STILL))
|
||||
expect_eq(len(jobs_at(jobs_st, 0, -1)), 3)
|
||||
}
|
||||
|
||||
test "counted by events, readied, handed in and paid" {
|
||||
fresh()
|
||||
jobs_count(K_FISH, 0, 1)
|
||||
expect_eq(jobs_have(JOB_CATCH), 0)
|
||||
expect(jobs_take(JOB_CATCH))
|
||||
jobs_count(K_FISH, 0, 2)
|
||||
expect_eq(jobs_state(JOB_CATCH), JOBS_ACTIVE)
|
||||
jobs_count(K_FISH, 0, 5)
|
||||
expect_eq(jobs_have(JOB_CATCH), 3)
|
||||
expect(jobs_ready_now(JOB_CATCH))
|
||||
expect_eq(count_of(JOBS_F_COMPLETED), 1)
|
||||
expect(jobs_hand_in(JOB_CATCH))
|
||||
expect(not jobs_hand_in(JOB_CATCH))
|
||||
expect_eq(paid, 30)
|
||||
expect_eq(paid_rep, 5)
|
||||
expect_eq(jobs_done(0), 1)
|
||||
expect_eq(count_of(JOBS_F_HANDED_IN), 1)
|
||||
test "counted by events, readied, handed in and paid" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
|
||||
fresh(base_st, jobs_st, jobstest_st)
|
||||
jobs_count(base_st, jobs_st, K_FISH, 0, 1)
|
||||
expect_eq(jobs_have(jobs_st, JOB_CATCH), 0)
|
||||
expect(jobs_take(base_st, jobs_st, JOB_CATCH))
|
||||
jobs_count(base_st, jobs_st, K_FISH, 0, 2)
|
||||
expect_eq(jobs_state(jobs_st, JOB_CATCH), JOBS_ACTIVE)
|
||||
jobs_count(base_st, jobs_st, K_FISH, 0, 5)
|
||||
expect_eq(jobs_have(jobs_st, JOB_CATCH), 3)
|
||||
expect(jobs_ready_now(jobs_st, JOB_CATCH))
|
||||
expect_eq(count_of(base_st, jobs_st, JOBS_F_COMPLETED), 1)
|
||||
expect(jobs_hand_in(base_st, jobs_st, JOB_CATCH))
|
||||
expect(not jobs_hand_in(base_st, jobs_st, JOB_CATCH))
|
||||
expect_eq(jobstest_st.paid, 30)
|
||||
expect_eq(jobstest_st.paid_rep, 5)
|
||||
expect_eq(jobs_done(jobs_st, 0), 1)
|
||||
expect_eq(count_of(base_st, jobs_st, JOBS_F_HANDED_IN), 1)
|
||||
}
|
||||
|
||||
test "the state already proves the work" {
|
||||
fresh()
|
||||
fish = 2
|
||||
jobs_take(JOB_CATCH)
|
||||
expect_eq(jobs_have(JOB_CATCH), 2)
|
||||
fish = 3
|
||||
jobs_recheck()
|
||||
expect(jobs_ready_now(JOB_CATCH))
|
||||
test "the state already proves the work" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
|
||||
fresh(base_st, jobs_st, jobstest_st)
|
||||
jobstest_st.fish = 2
|
||||
jobs_take(base_st, jobs_st, JOB_CATCH)
|
||||
expect_eq(jobs_have(jobs_st, JOB_CATCH), 2)
|
||||
jobstest_st.fish = 3
|
||||
jobs_recheck(base_st, jobs_st)
|
||||
expect(jobs_ready_now(jobs_st, JOB_CATCH))
|
||||
}
|
||||
|
||||
test "a param narrows the count" {
|
||||
fresh()
|
||||
rep = 10
|
||||
jobs_take(JOB_DEER)
|
||||
jobs_count(K_PHOTO, 3, 1)
|
||||
expect_eq(jobs_have(JOB_DEER), 0)
|
||||
jobs_count(K_PHOTO, 4, 1)
|
||||
expect(jobs_ready_now(JOB_DEER))
|
||||
test "a param narrows the count" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
|
||||
fresh(base_st, jobs_st, jobstest_st)
|
||||
jobstest_st.rep = 10
|
||||
jobs_take(base_st, jobs_st, JOB_DEER)
|
||||
jobs_count(base_st, jobs_st, K_PHOTO, 3, 1)
|
||||
expect_eq(jobs_have(jobs_st, JOB_DEER), 0)
|
||||
jobs_count(base_st, jobs_st, K_PHOTO, 4, 1)
|
||||
expect(jobs_ready_now(jobs_st, JOB_DEER))
|
||||
}
|
||||
|
||||
test "the game's own play readies a job with no need" {
|
||||
fresh()
|
||||
jobs_take(JOB_STILL)
|
||||
jobs_mark(JOB_STILL, 2)
|
||||
expect_eq(jobs_have(JOB_STILL), 4)
|
||||
expect_eq(jobs_state(JOB_STILL), JOBS_ACTIVE)
|
||||
expect(jobs_complete(JOB_STILL))
|
||||
expect_eq(paid, 60)
|
||||
expect_eq(jobs_done(1), 1)
|
||||
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(base_st, jobs_st, jobstest_st)
|
||||
jobs_take(base_st, jobs_st, JOB_STILL)
|
||||
jobs_mark(base_st, jobs_st, JOB_STILL, 2)
|
||||
expect_eq(jobs_have(jobs_st, JOB_STILL), 4)
|
||||
expect_eq(jobs_state(jobs_st, JOB_STILL), JOBS_ACTIVE)
|
||||
expect(jobs_complete(base_st, jobs_st, JOB_STILL))
|
||||
expect_eq(jobstest_st.paid, 60)
|
||||
expect_eq(jobs_done(jobs_st, 1), 1)
|
||||
}
|
||||
|
||||
test "a guest counts its own and leaves the party's to the host" {
|
||||
fresh()
|
||||
guest = true
|
||||
jobs_take(JOB_TEN)
|
||||
jobs_take(JOB_CATCH)
|
||||
jobs_count(K_FISH, 0, 2)
|
||||
expect_eq(jobs_have(JOB_TEN), 0)
|
||||
expect_eq(jobs_have(JOB_CATCH), 2)
|
||||
jobs_count_in(JOBS_PARTY, K_FISH, 0, 4)
|
||||
expect_eq(jobs_have(JOB_TEN), 4)
|
||||
expect_eq(jobs_have(JOB_CATCH), 2)
|
||||
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(base_st, jobs_st, jobstest_st)
|
||||
jobstest_st.guest = true
|
||||
jobs_take(base_st, jobs_st, JOB_TEN)
|
||||
jobs_take(base_st, jobs_st, JOB_CATCH)
|
||||
jobs_count(base_st, jobs_st, K_FISH, 0, 2)
|
||||
expect_eq(jobs_have(jobs_st, JOB_TEN), 0)
|
||||
expect_eq(jobs_have(jobs_st, JOB_CATCH), 2)
|
||||
jobs_count_in(base_st, jobs_st, JOBS_PARTY, K_FISH, 0, 4)
|
||||
expect_eq(jobs_have(jobs_st, JOB_TEN), 4)
|
||||
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" {
|
||||
fresh()
|
||||
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(base_st, jobs_st, jobstest_st)
|
||||
jobs_system().tick(tick_new(0.1, 1, 0.0))
|
||||
expect_eq(count_of(JOBS_F_OFFERED), 0)
|
||||
rep = 10
|
||||
expect_eq(count_of(base_st, jobs_st, JOBS_F_OFFERED), 0)
|
||||
jobstest_st.rep = 10
|
||||
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" {
|
||||
fresh()
|
||||
day = 3
|
||||
jobs_morning()
|
||||
expect_eq(len(jobs_posted(BOARD_DAILY)), 3)
|
||||
expect_eq(len(jobs_posted(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)
|
||||
fresh()
|
||||
day = 3
|
||||
jobs_morning()
|
||||
let b = jobs_post_need(BOARD_DAILY, 0) * 100 + jobs_post_need(BOARD_DAILY, 1) * 10 + jobs_post_need(BOARD_DAILY, 2)
|
||||
test "a board rolled by the day, the same every time" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
|
||||
fresh(base_st, jobs_st, jobstest_st)
|
||||
jobstest_st.day = 3
|
||||
jobs_morning(base_st, jobs_st)
|
||||
expect_eq(len(jobs_posted(jobs_st, BOARD_DAILY)), 3)
|
||||
expect_eq(len(jobs_posted(jobs_st, BOARD_ADA)), 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(base_st, jobs_st, jobstest_st)
|
||||
jobstest_st.day = 3
|
||||
jobs_morning(base_st, jobs_st)
|
||||
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(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" {
|
||||
fresh()
|
||||
day = 2
|
||||
jobs_morning()
|
||||
let need = jobs_post_need(BOARD_DAILY, 1)
|
||||
jobs_count(K_WOOD, -1, need)
|
||||
expect_eq(jobs_post_state(BOARD_DAILY, 1), JOBS_DONE)
|
||||
expect(jobs_posts_done(BOARD_DAILY) >= 1)
|
||||
expect_eq(paid, 7 * jobs_posts_done(BOARD_DAILY))
|
||||
jobs_morning()
|
||||
expect_eq(jobs_post_state(BOARD_DAILY, 1), JOBS_DONE)
|
||||
day = 3
|
||||
jobs_morning()
|
||||
expect_eq(jobs_post_state(BOARD_DAILY, 1), JOBS_ACTIVE)
|
||||
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(base_st, jobs_st, jobstest_st)
|
||||
jobstest_st.day = 2
|
||||
jobs_morning(base_st, jobs_st)
|
||||
let need = jobs_post_need(jobs_st, BOARD_DAILY, 1)
|
||||
jobs_count(base_st, jobs_st, K_WOOD, -1, need)
|
||||
expect_eq(jobs_post_state(jobs_st, BOARD_DAILY, 1), JOBS_DONE)
|
||||
expect(jobs_posts_done(jobs_st, BOARD_DAILY) >= 1)
|
||||
expect_eq(jobstest_st.paid, 7 * jobs_posts_done(jobs_st, BOARD_DAILY))
|
||||
jobs_morning(base_st, jobs_st)
|
||||
expect_eq(jobs_post_state(jobs_st, BOARD_DAILY, 1), JOBS_DONE)
|
||||
jobstest_st.day = 3
|
||||
jobs_morning(base_st, jobs_st)
|
||||
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" {
|
||||
fresh()
|
||||
fish = 5
|
||||
jobs_morning()
|
||||
let need = jobs_post_need(BOARD_ADA, 0)
|
||||
jobs_recheck()
|
||||
expect_eq(jobs_post_have(BOARD_ADA, 0), 0)
|
||||
fish = 5 + need
|
||||
jobs_recheck()
|
||||
expect_eq(jobs_post_state(BOARD_ADA, 0), JOBS_READY)
|
||||
expect(jobs_post_hand_in(BOARD_ADA, 0))
|
||||
expect_eq(paid_scope, JOBS_PARTY)
|
||||
expect_eq(jobs_total(BOARD_ADA), 1)
|
||||
expect_eq(jobs_post_state(BOARD_ADA, 0), JOBS_ACTIVE)
|
||||
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(base_st, jobs_st, jobstest_st)
|
||||
jobstest_st.fish = 5
|
||||
jobs_morning(base_st, jobs_st)
|
||||
let need = jobs_post_need(jobs_st, BOARD_ADA, 0)
|
||||
jobs_recheck(base_st, jobs_st)
|
||||
expect_eq(jobs_post_have(jobs_st, BOARD_ADA, 0), 0)
|
||||
jobstest_st.fish = 5 + need
|
||||
jobs_recheck(base_st, jobs_st)
|
||||
expect_eq(jobs_post_state(jobs_st, BOARD_ADA, 0), JOBS_READY)
|
||||
expect(jobs_post_hand_in(base_st, jobs_st, BOARD_ADA, 0))
|
||||
expect_eq(jobstest_st.paid_scope, JOBS_PARTY)
|
||||
expect_eq(jobs_total(jobs_st, BOARD_ADA), 1)
|
||||
expect_eq(jobs_post_state(jobs_st, BOARD_ADA, 0), JOBS_ACTIVE)
|
||||
}
|
||||
|
||||
test "a stale post comes down after its days" {
|
||||
fresh()
|
||||
jobs_morning()
|
||||
day = 7
|
||||
jobs_morning()
|
||||
expect_eq(jobs_post_day(BOARD_ADA, 0), 1)
|
||||
day = 8
|
||||
jobs_morning()
|
||||
expect_eq(jobs_post_day(BOARD_ADA, 0), 8)
|
||||
test "a stale post comes down after its days" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
|
||||
fresh(base_st, jobs_st, jobstest_st)
|
||||
jobs_morning(base_st, jobs_st)
|
||||
jobstest_st.day = 7
|
||||
jobs_morning(base_st, jobs_st)
|
||||
expect_eq(jobs_post_day(jobs_st, BOARD_ADA, 0), 1)
|
||||
jobstest_st.day = 8
|
||||
jobs_morning(base_st, jobs_st)
|
||||
expect_eq(jobs_post_day(jobs_st, BOARD_ADA, 0), 8)
|
||||
}
|
||||
|
||||
test "each scope saves its own, by key" {
|
||||
fresh()
|
||||
jobs_take(JOB_CATCH)
|
||||
jobs_count(K_FISH, 0, 2)
|
||||
jobs_take(JOB_TEN)
|
||||
jobs_count(K_FISH, 0, 1)
|
||||
day = 2
|
||||
jobs_morning()
|
||||
let body = jobs_save()
|
||||
let world = jobs_party_save()
|
||||
let need = jobs_post_need(BOARD_ADA, 1)
|
||||
fresh()
|
||||
jobs_load(body, 1)
|
||||
expect_eq(jobs_have(JOB_CATCH), 3)
|
||||
expect_eq(jobs_state(JOB_TEN), JOBS_OFFERED)
|
||||
jobs_party_load(world, 1)
|
||||
expect_eq(jobs_have(JOB_TEN), 1)
|
||||
expect_eq(jobs_post_need(BOARD_ADA, 1), need)
|
||||
expect_eq(jobs_post_day(BOARD_DAILY, 0), 2)
|
||||
test "each scope saves its own, by key" (base_st: mut BaseState, jobs_st: mut JobsState, jobstest_st: mut JobstestState) {
|
||||
fresh(base_st, jobs_st, jobstest_st)
|
||||
jobs_take(base_st, jobs_st, JOB_CATCH)
|
||||
jobs_count(base_st, jobs_st, K_FISH, 0, 2)
|
||||
jobs_take(base_st, jobs_st, JOB_TEN)
|
||||
jobs_count(base_st, jobs_st, K_FISH, 0, 1)
|
||||
jobstest_st.day = 2
|
||||
jobs_morning(base_st, jobs_st)
|
||||
let body = jobs_save(jobs_st)
|
||||
let world = jobs_party_save(jobs_st)
|
||||
let need = jobs_post_need(jobs_st, BOARD_ADA, 1)
|
||||
fresh(base_st, jobs_st, jobstest_st)
|
||||
jobs_load(jobs_st, body, 1)
|
||||
expect_eq(jobs_have(jobs_st, JOB_CATCH), 3)
|
||||
expect_eq(jobs_state(jobs_st, JOB_TEN), JOBS_OFFERED)
|
||||
jobs_party_load(jobs_st, world, 1)
|
||||
expect_eq(jobs_have(jobs_st, JOB_TEN), 1)
|
||||
expect_eq(jobs_post_need(jobs_st, BOARD_ADA, 1), need)
|
||||
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
Loading…
Add table
Add a link
Reference in a new issue