@adam0white/human-framework 2.20.0 → 2.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/dist/body/bladder.d.ts +21 -0
- package/dist/body/bladder.js +64 -0
- package/dist/body/body.js +41 -3
- package/dist/body/doze.d.ts +28 -0
- package/dist/body/doze.js +114 -0
- package/dist/body/index.d.ts +3 -0
- package/dist/body/index.js +3 -0
- package/dist/body/quantity.d.ts +8 -0
- package/dist/body/quantity.js +27 -0
- package/dist/cognition/cognition.js +3 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/memory/memory.js +2 -1
- package/dist/migrate.js +5 -0
- package/dist/narrate/lexicon.js +2 -0
- package/dist/narrate/plain.js +2 -0
- package/dist/person.js +33 -3
- package/dist/persona.js +6 -4
- package/dist/sim/sim.d.ts +1 -1
- package/dist/sim/sim.js +71 -3
- package/dist/types.d.ts +24 -1
- package/dist/types.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -222,7 +222,7 @@ reference host: `createVillage`.
|
|
|
222
222
|
|
|
223
223
|
There are two version numbers, and they move independently.
|
|
224
224
|
|
|
225
|
-
- **The release version** is the package version (`package.json`,
|
|
225
|
+
- **The release version** is the package version (`package.json`, currently `2.21.0`) and `FRAMEWORK_VERSION`, the same
|
|
226
226
|
string compiled into the build; a test keeps them equal. It versions the public API under
|
|
227
227
|
[semver](https://semver.org/): a breaking change to an exported name or signature bumps the major, a new
|
|
228
228
|
faculty or export the minor, a fix the patch. Each release is a tag `vX.Y.Z` and a GitHub release whose notes
|
|
@@ -236,6 +236,16 @@ There are two version numbers, and they move independently.
|
|
|
236
236
|
The root `package.json` version is the site's version (stamped into the site's `release.json`) and is not part of
|
|
237
237
|
this policy.
|
|
238
238
|
|
|
239
|
+
## Optional felt body (2.21 source candidate)
|
|
240
|
+
|
|
241
|
+
`PersonSpec.enable.doze` accepts `{ posture: 'upright' | 'seated' | 'reclined', stimulation: number }`; `enableDoze` and `setDozeContext` are the explicit host calls. Doze onset is checked at integer-minute boundaries while awake. `readDoze` reports the current hazard; `nextDozeCheck` tells a manual host when to tick next. The opt-in costs one check per elapsed minute, including when onset gates are closed. It changes integration granularity, so enabled runs require a new replay pin. A dozed activity records `dozedAt`; the host resolves interrupted work. Doze does not wake spontaneously. `perceive` wakes only for a percept delivered at `p.now`, with `salience >= 0.8`, a heard/felt/social channel, detectable proximity (`near !== false`) and either `targetId === p.id` or `near === true`. The host must supply those alarms. These hazard and salience values are engineering constants.
|
|
242
|
+
|
|
243
|
+
`estimateQuantity(p, { truth, difficulty })` is an explicit observation, not a display reader. Finite nonnegative truth and difficulty in `[0,1]` are required. A nonzero request consumes one draw from the person's saved seeded RNG; zero truth or zero difficulty returns truth with confidence 1 and consumes none. The result does not store a belief. Objective sleepiness widens its uniform error; perceived sleepiness determines confidence. Coefficients are engineering choices, not calibrated uncertainty intervals.
|
|
244
|
+
|
|
245
|
+
`PersonSpec.enable.bladder` or `enableBladder(p)` starts a normalized fill store. `readBladder` returns true and perceived urgency, equal in this first slice. Water intake adds delayed fill and a small base rate fills it over time. A host marks a toilet `Affordance` with `voidsBladder: true` to add urgency-based choice value. Actual successful urination is separate: a completed `Outcome` with `voidedBladder: true` empties it; `voidBladder(p)` is the direct host operation. An interrupted or failed outcome does not empty it. For an offer that declares voiding, expectation learning treats completion without actual voiding as failure of that purpose. Do not use `needs.relief` as a substitute: that field reduces pain. Filling rates, threshold and choice weight are engineering constants. The model has no continence, automatic toilet search or automatic waking.
|
|
246
|
+
|
|
247
|
+
With none of these options enabled or calls made, person state and seeded RNG continuation match HF 2.20.0 after normalizing the engine stamp only. HF 2.21.0 is library source here; the three site games still declare HF 2.20.0. Watch's adoption of `gestureVoiceTrust` and `addTiredness` uses the 2.20.0 helpers.
|
|
248
|
+
|
|
239
249
|
## Long runs (2.0)
|
|
240
250
|
|
|
241
251
|
HF 2.0 adds what a life over decades needs. Each faculty is opt-in or absent until used, so a host that uses none
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Affordance, Person } from '../types.ts';
|
|
2
|
+
export declare const BLADDER_DEFAULTS: Readonly<{
|
|
3
|
+
intakeScale: 0.5;
|
|
4
|
+
maxPending: 4;
|
|
5
|
+
halfLife: 45;
|
|
6
|
+
basePerMinute: 0.001;
|
|
7
|
+
/** Frozen engineering default, not a clinical threshold or fitted choice result. */
|
|
8
|
+
choiceThreshold: 0.35;
|
|
9
|
+
/** Frozen engineering weight on perceived urgency above the threshold. */
|
|
10
|
+
choiceScale: 4;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function enableBladder(p: Person): void;
|
|
13
|
+
/** Pure host readout; equal perceived urgency is a first-slice assumption. */
|
|
14
|
+
export declare function readBladder(p: Person): {
|
|
15
|
+
urgency: number;
|
|
16
|
+
perceived: number;
|
|
17
|
+
} | undefined;
|
|
18
|
+
/** Pure opt-in choice contribution; a declared offer says nothing about actual success. */
|
|
19
|
+
export declare function bladderMotivation(p: Person, offer: Affordance, believedSuccess: number): number;
|
|
20
|
+
/** Host calls only after successful urination. Pending intake remains. */
|
|
21
|
+
export declare function voidBladder(p: Person): boolean;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCOPE: opt-in normalized storage and delayed water intake; hosts resolve actual voiding.
|
|
3
|
+
* NIDDK, The Urinary Tract & How It Works supports production/storage/urination, not these
|
|
4
|
+
* engineering rates. Base filling is independent of hydration, including dehydration.
|
|
5
|
+
* Urgency and perceived urgency are equal in this slice; hosts supply toilet choices. It models no renal calibration, continence, injury, search for a toilet or automatic waking.
|
|
6
|
+
*/
|
|
7
|
+
import { clamp01, dexp, isNum, isObj } from '../core/index.js';
|
|
8
|
+
export const BLADDER_DEFAULTS = Object.freeze({
|
|
9
|
+
intakeScale: 0.5,
|
|
10
|
+
maxPending: 4,
|
|
11
|
+
halfLife: 45,
|
|
12
|
+
basePerMinute: 0.001,
|
|
13
|
+
/** Frozen engineering default, not a clinical threshold or fitted choice result. */
|
|
14
|
+
choiceThreshold: 0.35,
|
|
15
|
+
/** Frozen engineering weight on perceived urgency above the threshold. */
|
|
16
|
+
choiceScale: 4,
|
|
17
|
+
});
|
|
18
|
+
export function enableBladder(p) {
|
|
19
|
+
p.body.bladder ??= { fill: 0, pending: 0 };
|
|
20
|
+
}
|
|
21
|
+
/** Pure host readout; equal perceived urgency is a first-slice assumption. */
|
|
22
|
+
export function readBladder(p) {
|
|
23
|
+
const b = p.body.bladder;
|
|
24
|
+
return b ? { urgency: b.fill, perceived: b.fill } : undefined;
|
|
25
|
+
}
|
|
26
|
+
/** Pure opt-in choice contribution; a declared offer says nothing about actual success. */
|
|
27
|
+
export function bladderMotivation(p, offer, believedSuccess) {
|
|
28
|
+
if (offer.voidsBladder !== true)
|
|
29
|
+
return 0;
|
|
30
|
+
const perceived = readBladder(p)?.perceived;
|
|
31
|
+
if (perceived === undefined)
|
|
32
|
+
return 0;
|
|
33
|
+
return (BLADDER_DEFAULTS.choiceScale *
|
|
34
|
+
clamp01((perceived - BLADDER_DEFAULTS.choiceThreshold) / (1 - BLADDER_DEFAULTS.choiceThreshold)) *
|
|
35
|
+
clamp01(believedSuccess));
|
|
36
|
+
}
|
|
37
|
+
/** Host calls only after successful urination. Pending intake remains. */
|
|
38
|
+
export function voidBladder(p) {
|
|
39
|
+
if (!p.body.bladder || !p.body.alive || p.body.asleep)
|
|
40
|
+
return false;
|
|
41
|
+
p.body.bladder.fill = 0;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
/** @internal */
|
|
45
|
+
export function intakeBladder(p, water) {
|
|
46
|
+
const b = p.body.bladder;
|
|
47
|
+
if (b && isNum(water) && water > 0)
|
|
48
|
+
b.pending = Math.min(BLADDER_DEFAULTS.maxPending, b.pending + BLADDER_DEFAULTS.intakeScale * clamp01(water));
|
|
49
|
+
}
|
|
50
|
+
/** @internal */
|
|
51
|
+
export function advanceBladder(b, dt) {
|
|
52
|
+
const remaining = b.pending * dexp((-Math.LN2 * dt) / BLADDER_DEFAULTS.halfLife);
|
|
53
|
+
b.fill = clamp01(b.fill + BLADDER_DEFAULTS.basePerMinute * dt + b.pending - remaining);
|
|
54
|
+
b.pending = remaining;
|
|
55
|
+
}
|
|
56
|
+
/** @internal */
|
|
57
|
+
export function sanitizeBladder(raw) {
|
|
58
|
+
if (!isObj(raw) || !isNum(raw.fill) || !isNum(raw.pending))
|
|
59
|
+
return undefined;
|
|
60
|
+
return {
|
|
61
|
+
fill: clamp01(raw.fill),
|
|
62
|
+
pending: Math.min(BLADDER_DEFAULTS.maxPending, Math.max(0, raw.pending)),
|
|
63
|
+
};
|
|
64
|
+
}
|
package/dist/body/body.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* and is under-perceived (cumulative-cost-of-wakefulness pattern, Van Dongen et al. 2003). Satiety and
|
|
9
9
|
* hydration fall linearly with metabolic rate and effort. Fitness relaxes exponentially toward a training
|
|
10
10
|
* target or a detraining floor. All rates are engineering defaults; none is a calibrated physiological
|
|
11
|
-
* prediction. Not modelled:
|
|
11
|
+
* prediction. Not modelled: temperature, specific nutrients, endocrine and immune detail,
|
|
12
12
|
* stress physiology, and masking of hunger or pain by emotion or focus (that belongs to other faculties).
|
|
13
13
|
* `advanceBody` integrates in deterministic sub-steps of at most 15 minutes, using the exact solution
|
|
14
14
|
* within each sub-step where the dynamics are linear, so one long step matches many short ones closely.
|
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
*/
|
|
45
45
|
import { chance, clamp, clamp01, dcos, decay, dexp, dlog, hasNumbers, isNum, isObj, minuteOfDay, smoothstep, } from '../core/index.js';
|
|
46
46
|
import { MINUTES_PER_DAY, MINUTES_PER_YEAR } from '../types.js';
|
|
47
|
+
import { advanceBladder, intakeBladder, sanitizeBladder } from './bladder.js';
|
|
48
|
+
import { sanitizeDoze } from './doze.js';
|
|
47
49
|
import { bleedStep, INJURY_DEFAULTS } from './injury.js';
|
|
48
50
|
export const BODY_DEFAULTS = {
|
|
49
51
|
maxSubStep: 15,
|
|
@@ -218,6 +220,20 @@ export function sanitizeExposures(input, now) {
|
|
|
218
220
|
* downing are checked by `sanitizeInjuries`. A save the engine wrote is unchanged. @internal
|
|
219
221
|
*/
|
|
220
222
|
export function sanitizeBody(b, now) {
|
|
223
|
+
if (b.doze !== undefined) {
|
|
224
|
+
const d = sanitizeDoze(b.doze, b, now);
|
|
225
|
+
if (d)
|
|
226
|
+
b.doze = d;
|
|
227
|
+
else
|
|
228
|
+
delete b.doze;
|
|
229
|
+
}
|
|
230
|
+
if (b.bladder !== undefined) {
|
|
231
|
+
const bladder = sanitizeBladder(b.bladder);
|
|
232
|
+
if (bladder)
|
|
233
|
+
b.bladder = bladder;
|
|
234
|
+
else
|
|
235
|
+
delete b.bladder;
|
|
236
|
+
}
|
|
221
237
|
if (b.rates !== undefined) {
|
|
222
238
|
const rates = sanitizeRates(b.rates);
|
|
223
239
|
if (rates)
|
|
@@ -304,6 +320,8 @@ export function createBody(init, now) {
|
|
|
304
320
|
illnesses: [],
|
|
305
321
|
nextId: 0,
|
|
306
322
|
};
|
|
323
|
+
delete b.doze;
|
|
324
|
+
delete b.bladder;
|
|
307
325
|
b.satiety = clamp01(b.satiety);
|
|
308
326
|
b.hydration = clamp01(b.hydration);
|
|
309
327
|
b.sleepPressure = clamp01(b.sleepPressure);
|
|
@@ -486,7 +504,7 @@ export function advanceBody(p, dt, load, mods, params = BODY_DEFAULTS) {
|
|
|
486
504
|
const b = p.body;
|
|
487
505
|
if (!b.alive)
|
|
488
506
|
return;
|
|
489
|
-
const wantAsleep = load.mode === 'sleep';
|
|
507
|
+
const wantAsleep = load.mode === 'sleep' || b.doze?.since !== undefined;
|
|
490
508
|
if (wantAsleep !== b.asleep) {
|
|
491
509
|
if (b.asleep)
|
|
492
510
|
b.lastSleep = { from: b.since, to: p.now };
|
|
@@ -497,8 +515,15 @@ export function advanceBody(p, dt, load, mods, params = BODY_DEFAULTS) {
|
|
|
497
515
|
return;
|
|
498
516
|
const n = Math.ceil(dt / params.maxSubStep);
|
|
499
517
|
const h = dt / n;
|
|
500
|
-
for (let i = 0; i < n && b.alive; i++)
|
|
518
|
+
for (let i = 0; i < n && b.alive; i++) {
|
|
519
|
+
if (b.bladder)
|
|
520
|
+
advanceBladder(b.bladder, h);
|
|
501
521
|
subStep(b, h, load, mods, params);
|
|
522
|
+
}
|
|
523
|
+
if (!b.alive && b.doze?.since !== undefined) {
|
|
524
|
+
delete b.doze.since;
|
|
525
|
+
b.asleep = false;
|
|
526
|
+
}
|
|
502
527
|
}
|
|
503
528
|
/** Circadian alertness 0..1 (Process C): cosine peaking at `circadianPeak`, trough 12 h earlier, post-lunch dip. */
|
|
504
529
|
export function circadianAlertness(now, circadianPeak) {
|
|
@@ -598,6 +623,7 @@ export function consume(p, deltas) {
|
|
|
598
623
|
const b = p.body;
|
|
599
624
|
if (!b.alive)
|
|
600
625
|
return;
|
|
626
|
+
intakeBladder(p, deltas.water);
|
|
601
627
|
if (deltas.food)
|
|
602
628
|
b.satiety = clamp01(b.satiety + deltas.food);
|
|
603
629
|
if (deltas.water)
|
|
@@ -617,6 +643,8 @@ function freshId(b, prefix, now) {
|
|
|
617
643
|
*/
|
|
618
644
|
export function skipBody(p, to) {
|
|
619
645
|
p.body.since = to;
|
|
646
|
+
if (p.body.doze?.since !== undefined)
|
|
647
|
+
p.body.doze.since = to;
|
|
620
648
|
}
|
|
621
649
|
/**
|
|
622
650
|
* The body-owned death setter for host-rolled deaths (`lifecourse.mortalityEvent`): health 0, not alive, awake.
|
|
@@ -629,6 +657,8 @@ export function die(p) {
|
|
|
629
657
|
b.alive = false;
|
|
630
658
|
b.health = 0;
|
|
631
659
|
b.asleep = false;
|
|
660
|
+
if (b.doze)
|
|
661
|
+
delete b.doze.since;
|
|
632
662
|
b.since = p.now;
|
|
633
663
|
}
|
|
634
664
|
export function injure(p, injury) {
|
|
@@ -761,6 +791,10 @@ const cloneBody = (b) => {
|
|
|
761
791
|
ex[k] = { ...e };
|
|
762
792
|
out.exposures = ex;
|
|
763
793
|
}
|
|
794
|
+
if (b.doze)
|
|
795
|
+
out.doze = { ...b.doze, context: { ...b.doze.context } };
|
|
796
|
+
if (b.bladder)
|
|
797
|
+
out.bladder = { ...b.bladder };
|
|
764
798
|
if (b.rates)
|
|
765
799
|
out.rates = { ...b.rates };
|
|
766
800
|
if (b.downed)
|
|
@@ -793,6 +827,8 @@ export function nextBodyThreshold(p, load, mods, thresholds = { hunger: 0.7, thi
|
|
|
793
827
|
return watch.some((k) => r[k] >= thresholds[k]);
|
|
794
828
|
};
|
|
795
829
|
const sim = { ...p, body: cloneBody(p.body) };
|
|
830
|
+
if (sim.body.doze)
|
|
831
|
+
delete sim.body.doze.since;
|
|
796
832
|
advanceBody(sim, 0, load, mods, params); // apply mode change
|
|
797
833
|
const coarse = 30;
|
|
798
834
|
let t = 0;
|
|
@@ -823,6 +859,8 @@ export function nextBodyThreshold(p, load, mods, thresholds = { hunger: 0.7, thi
|
|
|
823
859
|
export function forecastBody(p, spans, mods, params = BODY_DEFAULTS, step = 30, perception) {
|
|
824
860
|
const out = [];
|
|
825
861
|
const sim = { ...p, body: cloneBody(p.body) };
|
|
862
|
+
if (sim.body.doze)
|
|
863
|
+
delete sim.body.doze.since;
|
|
826
864
|
for (const s of spans) {
|
|
827
865
|
const span = { label: s.label, endsAt: sim.now, worst: 0, worstOf: 'fatigue' };
|
|
828
866
|
// Hard internal cap (a week) so a hostile span cannot stall a decision.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { DozeContext, Person } from '../types.ts';
|
|
2
|
+
export declare const DOZE_DEFAULTS: Readonly<{
|
|
3
|
+
sleepiness: 0.75;
|
|
4
|
+
stimulation: 0.5;
|
|
5
|
+
hazard: 0.08;
|
|
6
|
+
seatedWeight: 0.5;
|
|
7
|
+
wakeSalience: 0.8;
|
|
8
|
+
}>;
|
|
9
|
+
/**
|
|
10
|
+
* Enable involuntary doze without drawing RNG. Idempotent; off when absent.
|
|
11
|
+
* Hosts must deliver current, detectable heard/felt/social alarms with salience >= 0.8
|
|
12
|
+
* to this person (targetId, or near: true). There is no spontaneous wake: without such
|
|
13
|
+
* percepts sleep continues. The community driver checks percepts on its ordinary idle cadence.
|
|
14
|
+
* Enabled tick integration uses one-minute boundaries even with closed gates or during sleep;
|
|
15
|
+
* cost is O(elapsed minutes) and continuous state can differ from the absent opt-in arm.
|
|
16
|
+
* Closed gates draw no onset RNG; passive community checkpoints do not call choice.
|
|
17
|
+
* Activity.dozedAt pins stopped work until the host resolves it, even after an alarm;
|
|
18
|
+
* the pending interrupt reason may retain an earlier host/percept cause.
|
|
19
|
+
*/
|
|
20
|
+
export declare function enableDoze(p: Person, input: DozeContext): void;
|
|
21
|
+
export declare function setDozeContext(p: Person, input: DozeContext): void;
|
|
22
|
+
/** Pure; closed onset gates still require the next checkpoint. */
|
|
23
|
+
export declare function nextDozeCheck(p: Person): number;
|
|
24
|
+
/** Pure truth/debug reader, not a first-person lens. */
|
|
25
|
+
export declare function readDoze(p: Person): {
|
|
26
|
+
asleep: boolean;
|
|
27
|
+
hazardPerMinute: number;
|
|
28
|
+
} | undefined;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCOPE: opt-in minute-grid involuntary onset and current urgent-percept waking. Cole 1989
|
|
3
|
+
* (PMID 2606843) supports posture context; auditory arousal studies (PMIDs 8776786,
|
|
4
|
+
* 8153254) distinguish arousal from attention. None calibrates these engineering gates,
|
|
5
|
+
* hazard weights or salience. No stages, inertia, spontaneous wake or clinical prediction.
|
|
6
|
+
* Manual hosts schedule nextDozeCheck and resolve activities stopped at Activity.dozedAt.
|
|
7
|
+
* A prior pending interrupt reason is retained; physical doze still stops work.
|
|
8
|
+
*/
|
|
9
|
+
import { clamp01, dexp, isNum, isObj, random, smoothstep } from '../core/index.js';
|
|
10
|
+
import { readBody } from './body.js';
|
|
11
|
+
export const DOZE_DEFAULTS = Object.freeze({
|
|
12
|
+
sleepiness: 0.75,
|
|
13
|
+
stimulation: 0.5,
|
|
14
|
+
hazard: 0.08,
|
|
15
|
+
seatedWeight: 0.5,
|
|
16
|
+
wakeSalience: 0.8,
|
|
17
|
+
});
|
|
18
|
+
function context(raw) {
|
|
19
|
+
if (!isObj(raw) ||
|
|
20
|
+
(raw.posture !== 'upright' && raw.posture !== 'seated' && raw.posture !== 'reclined') ||
|
|
21
|
+
!isNum(raw.stimulation))
|
|
22
|
+
return undefined;
|
|
23
|
+
return { posture: raw.posture, stimulation: clamp01(raw.stimulation) };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Enable involuntary doze without drawing RNG. Idempotent; off when absent.
|
|
27
|
+
* Hosts must deliver current, detectable heard/felt/social alarms with salience >= 0.8
|
|
28
|
+
* to this person (targetId, or near: true). There is no spontaneous wake: without such
|
|
29
|
+
* percepts sleep continues. The community driver checks percepts on its ordinary idle cadence.
|
|
30
|
+
* Enabled tick integration uses one-minute boundaries even with closed gates or during sleep;
|
|
31
|
+
* cost is O(elapsed minutes) and continuous state can differ from the absent opt-in arm.
|
|
32
|
+
* Closed gates draw no onset RNG; passive community checkpoints do not call choice.
|
|
33
|
+
* Activity.dozedAt pins stopped work until the host resolves it, even after an alarm;
|
|
34
|
+
* the pending interrupt reason may retain an earlier host/percept cause.
|
|
35
|
+
*/
|
|
36
|
+
export function enableDoze(p, input) {
|
|
37
|
+
if (p.body.doze)
|
|
38
|
+
return;
|
|
39
|
+
const ctx = context(input);
|
|
40
|
+
if (!ctx)
|
|
41
|
+
throw new RangeError('Invalid doze context');
|
|
42
|
+
p.body.doze = { context: ctx };
|
|
43
|
+
}
|
|
44
|
+
export function setDozeContext(p, input) {
|
|
45
|
+
if (!p.body.doze)
|
|
46
|
+
return;
|
|
47
|
+
const ctx = context(input);
|
|
48
|
+
if (!ctx)
|
|
49
|
+
throw new RangeError('Invalid doze context');
|
|
50
|
+
p.body.doze.context = ctx;
|
|
51
|
+
}
|
|
52
|
+
/** Pure; closed onset gates still require the next checkpoint. */
|
|
53
|
+
export function nextDozeCheck(p) {
|
|
54
|
+
return p.body.doze && p.body.alive && !p.body.asleep ? Math.floor(p.now) + 1 : Number.POSITIVE_INFINITY;
|
|
55
|
+
}
|
|
56
|
+
/** Pure truth/debug reader, not a first-person lens. */
|
|
57
|
+
export function readDoze(p) {
|
|
58
|
+
const d = p.body.doze;
|
|
59
|
+
if (!d)
|
|
60
|
+
return undefined;
|
|
61
|
+
const s = readBody(p).sleepiness;
|
|
62
|
+
const D = DOZE_DEFAULTS;
|
|
63
|
+
const eligible = p.body.alive &&
|
|
64
|
+
!p.body.downed &&
|
|
65
|
+
!p.body.asleep &&
|
|
66
|
+
s > D.sleepiness &&
|
|
67
|
+
d.context.posture !== 'upright' &&
|
|
68
|
+
d.context.stimulation < D.stimulation;
|
|
69
|
+
const lambda = eligible
|
|
70
|
+
? D.hazard *
|
|
71
|
+
smoothstep(D.sleepiness, 1, s) *
|
|
72
|
+
(d.context.posture === 'seated' ? D.seatedWeight : 1) *
|
|
73
|
+
(1 - d.context.stimulation / D.stimulation)
|
|
74
|
+
: 0;
|
|
75
|
+
return { asleep: d.since !== undefined, hazardPerMinute: 1 - dexp(-lambda) };
|
|
76
|
+
}
|
|
77
|
+
/** @internal Composite dispatch only, once per crossed integer minute. */
|
|
78
|
+
export function checkDoze(p, at) {
|
|
79
|
+
const d = p.body.doze;
|
|
80
|
+
const hazard = readDoze(p)?.hazardPerMinute ?? 0;
|
|
81
|
+
if (!d || hazard <= 0 || random(p.rng) >= hazard)
|
|
82
|
+
return false;
|
|
83
|
+
d.since = at;
|
|
84
|
+
p.body.asleep = true;
|
|
85
|
+
p.body.since = at;
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
/** @internal */
|
|
89
|
+
export function wakeDoze(p, at) {
|
|
90
|
+
if (p.body.doze?.since === undefined)
|
|
91
|
+
return false;
|
|
92
|
+
delete p.body.doze.since;
|
|
93
|
+
p.body.lastSleep = { from: p.body.since, to: at };
|
|
94
|
+
p.body.asleep = false;
|
|
95
|
+
p.body.since = at;
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
/** @internal Transfer without resetting the continuous sleep span. */
|
|
99
|
+
export function enterVoluntarySleep(p) {
|
|
100
|
+
if (p.body.doze)
|
|
101
|
+
delete p.body.doze.since;
|
|
102
|
+
}
|
|
103
|
+
/** @internal */
|
|
104
|
+
export function sanitizeDoze(raw, b, now) {
|
|
105
|
+
if (!isObj(raw))
|
|
106
|
+
return undefined;
|
|
107
|
+
const ctx = context(raw.context);
|
|
108
|
+
if (!ctx)
|
|
109
|
+
return undefined;
|
|
110
|
+
const out = { context: ctx };
|
|
111
|
+
if (isNum(raw.since) && raw.since <= now && raw.since === b.since && b.alive && b.asleep)
|
|
112
|
+
out.since = raw.since;
|
|
113
|
+
return out;
|
|
114
|
+
}
|
package/dist/body/index.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
+
export { BLADDER_DEFAULTS, enableBladder, readBladder, voidBladder } from './bladder.ts';
|
|
1
2
|
export * from './body.ts';
|
|
3
|
+
export { DOZE_DEFAULTS, enableDoze, nextDozeCheck, readDoze, setDozeContext } from './doze.ts';
|
|
2
4
|
export * from './injury.ts';
|
|
5
|
+
export { estimateQuantity } from './quantity.ts';
|
package/dist/body/index.js
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
+
export { BLADDER_DEFAULTS, enableBladder, readBladder, voidBladder } from './bladder.js';
|
|
1
2
|
export * from './body.js';
|
|
3
|
+
export { DOZE_DEFAULTS, enableDoze, nextDozeCheck, readDoze, setDozeContext } from './doze.js';
|
|
2
4
|
export * from './injury.js';
|
|
5
|
+
export { estimateQuantity } from './quantity.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCOPE: explicit scalar observation, never a render-time reader. Valid nonzero truth and
|
|
3
|
+
* difficulty consume one seeded draw. Zero truth or zero difficulty return exact truth without
|
|
4
|
+
* drawing; invalid/overflowing requests reject before drawing.
|
|
5
|
+
* Van Dongen et al. 2003 (PMID 12683469) supports separating objective impairment from
|
|
6
|
+
* subjective awareness, not quantity-task coefficients. Uniform errors and certainty are
|
|
7
|
+
* engineering defaults, not calibrated error intervals. No history or belief mutation.
|
|
8
|
+
*/
|
|
9
|
+
import { clamp01, random } from '../core/index.js';
|
|
10
|
+
import { readBody } from './body.js';
|
|
11
|
+
export function estimateQuantity(p, input) {
|
|
12
|
+
const { truth, difficulty } = input;
|
|
13
|
+
if (!Number.isFinite(truth) ||
|
|
14
|
+
truth < 0 ||
|
|
15
|
+
!Number.isFinite(difficulty) ||
|
|
16
|
+
difficulty < 0 ||
|
|
17
|
+
difficulty > 1)
|
|
18
|
+
throw new RangeError('Quantity requires finite non-negative truth and difficulty in [0,1]');
|
|
19
|
+
if (truth === 0 || difficulty === 0)
|
|
20
|
+
return { value: truth, confidence: 1 };
|
|
21
|
+
const body = readBody(p);
|
|
22
|
+
const error = difficulty * (0.05 + 0.45 * body.sleepiness);
|
|
23
|
+
if (!Number.isFinite(truth * (1 + error)))
|
|
24
|
+
throw new RangeError('Quantity estimate would overflow');
|
|
25
|
+
const value = truth * (1 + (2 * random(p.rng) - 1) * error);
|
|
26
|
+
return { value, confidence: clamp01(1 - difficulty * (0.05 + 0.45 * body.perceived.sleepiness)) };
|
|
27
|
+
}
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
import { emotionLevel, inBreak } from '../affect/index.js';
|
|
39
39
|
import { agendaTermsWithGoalEstimates } from '../agenda/index.js';
|
|
40
40
|
import { trustOf } from '../beliefs/index.js';
|
|
41
|
+
import { bladderMotivation } from '../body/bladder.js';
|
|
41
42
|
import { normTerms } from '../conscience/index.js';
|
|
42
43
|
import { clamp01, cmpStr, dexp, minuteOfDay, round } from '../core/index.js';
|
|
43
44
|
import { keysOf } from '../core/keys.js';
|
|
@@ -271,6 +272,8 @@ export function consider(p, aff, ctx) {
|
|
|
271
272
|
};
|
|
272
273
|
// Needs: urgency × believed gain × believed success.
|
|
273
274
|
const expected = expectedEffect(p, aff);
|
|
275
|
+
if (p.body.bladder)
|
|
276
|
+
push('need:bladder', bladderMotivation(p, aff, expected.successRate));
|
|
274
277
|
for (const id of keysOf(expected.needs)) {
|
|
275
278
|
const gain = expected.needs[id] ?? 0;
|
|
276
279
|
if (gain === 0)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Human Framework public surface. */
|
|
2
|
-
export declare const FRAMEWORK_VERSION = "2.
|
|
2
|
+
export declare const FRAMEWORK_VERSION = "2.21.0";
|
|
3
3
|
export type { CrisisEvent, PracticeKind } from './affect/index.ts';
|
|
4
4
|
export { AFFECT_DEFAULTS, actionTendencies, advanceAffect, appraise, breakAllows, breakBehaviour, breakHazard, CRISIS_DEFAULTS, checkCrisis, createAffect, easeBreak, emotionLevel, enableBreaks, feel, inBreak, readAffect, regulate, release as releaseEmotion, skipCrisis, strain, tendencyEmotions, } from './affect/index.ts';
|
|
5
5
|
export type { GoalTemplate, MakeUpReceipt, PrayerCalendar, PrayerSchedule, PrayerTimes, Retimer, } from './agenda/index.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Human Framework public surface. */
|
|
2
|
-
export const FRAMEWORK_VERSION = '2.
|
|
2
|
+
export const FRAMEWORK_VERSION = '2.21.0';
|
|
3
3
|
export { AFFECT_DEFAULTS, actionTendencies, advanceAffect, appraise, breakAllows, breakBehaviour, breakHazard, CRISIS_DEFAULTS, checkCrisis, createAffect, easeBreak, emotionLevel, enableBreaks, feel, inBreak, readAffect, regulate, release as releaseEmotion, skipCrisis, strain, tendencyEmotions, } from './affect/index.js';
|
|
4
4
|
// `agenda.release` collides with `affect.release`; the agenda one is re-exported under a clearer name.
|
|
5
5
|
export { AGENDA_DEFAULTS, abandonGoal, adoptGoal, advanceAgenda, agendaTerms, applyExemptions, CARE_DEFAULTS, calendarRetimer, careDuty, commitmentPressure, createAgenda, DEFAULT_PRAYER_TIMES, EID_PRAYER_IMPORTANCE, EXEMPTION_DEFAULTS, eidPrayer, eidWindow, fastWindow, iftarWindow, inMakruhTime, isContinuingCommitment, isUnderWay, MAKRUH_DEFAULTS, makruhWindows, missedExcuse, onFinished, owedMakeUps, PRAYER_IMPORTANCE, PURPOSE_DEFAULTS, prayerWindows, pressureReachedAt, promise, proposeGoals, RAMADAN_DEFAULTS, ramadanFast, ramadanMeals, release as releaseCommitment, retimeCommitments, retirePaidMakeUps, revisePurposes, scheduleMakeUp, spanMeetsWindow, suhoorWindow, timesFor, violatesAbstention, } from './agenda/index.js';
|
package/dist/memory/memory.js
CHANGED
|
@@ -576,7 +576,8 @@ function updateExpectation(p, key, aff, success, realized, felt) {
|
|
|
576
576
|
export function learnOutcome(p, aff, outcome, realized, felt) {
|
|
577
577
|
if (outcome.status === 'interrupted')
|
|
578
578
|
return;
|
|
579
|
-
|
|
579
|
+
// A declared voiding purpose succeeds only when the actual outcome confirms it.
|
|
580
|
+
const success = outcome.status === 'completed' && (aff.voidsBladder !== true || outcome.voidedBladder === true);
|
|
580
581
|
const touched = new Set([expectationKey({ action: aff.action })]);
|
|
581
582
|
if (aff.targetId !== undefined)
|
|
582
583
|
touched.add(expectationKey(aff));
|
package/dist/migrate.js
CHANGED
|
@@ -41,6 +41,11 @@ function renameAmbient(json) {
|
|
|
41
41
|
}
|
|
42
42
|
/** The chain, keyed by the version each step upgrades from. */
|
|
43
43
|
export const MIGRATIONS = {
|
|
44
|
+
'2.17.0': {
|
|
45
|
+
to: '2.21.0',
|
|
46
|
+
note: 'doze and bladder state start absent; old callers retain their continuation',
|
|
47
|
+
apply: stamp,
|
|
48
|
+
},
|
|
44
49
|
'1.4.0': {
|
|
45
50
|
to: '1.5.0',
|
|
46
51
|
note: 'person shape unchanged; standing advice is heard for the running activity from now on',
|
package/dist/narrate/lexicon.js
CHANGED
|
@@ -23,6 +23,7 @@ export const EN_LINES = {
|
|
|
23
23
|
],
|
|
24
24
|
'need:rest': ['I need to sit down for a while.', 'A rest first, then the rest.'],
|
|
25
25
|
'need:relief': ['This pain needs seeing to.', "I can't think past the pain."],
|
|
26
|
+
'need:bladder': ['I need to use the toilet.'],
|
|
26
27
|
'need:belonging': ["I've been alone too long. I want company.", 'Let me find someone to talk to.'],
|
|
27
28
|
'need:leisure': ['I need a break from all this.', 'Time for something easier.'],
|
|
28
29
|
'need:meaning': ['I want to do something that matters.', 'Something worthwhile, for once.'],
|
|
@@ -102,6 +103,7 @@ export const EN_LINES = {
|
|
|
102
103
|
'intention:sleep': ['to sleep'],
|
|
103
104
|
'intention:rest': ['to rest'],
|
|
104
105
|
'intention:relief': ['to ease the pain'],
|
|
106
|
+
'intention:bladder': ['to use the toilet'],
|
|
105
107
|
'intention:belonging': ['to be with people'],
|
|
106
108
|
'intention:leisure': ['to enjoy myself'],
|
|
107
109
|
'intention:meaning': ['to do something that matters'],
|
package/dist/narrate/plain.js
CHANGED
|
@@ -22,6 +22,7 @@ export const PLAIN_EN_LINES = {
|
|
|
22
22
|
'I need to sleep before I do anything else.',
|
|
23
23
|
],
|
|
24
24
|
'need:rest': ['I need to sit down for a while.', 'I will rest before I do anything else.'],
|
|
25
|
+
'need:bladder': ['I need to use the toilet.'],
|
|
25
26
|
'need:relief': [
|
|
26
27
|
'I need to do something about this pain.',
|
|
27
28
|
'The pain makes it hard to think about anything else.',
|
|
@@ -116,6 +117,7 @@ export const PLAIN_EN_LINES = {
|
|
|
116
117
|
'intention:water': ['to drink'],
|
|
117
118
|
'intention:sleep': ['to sleep'],
|
|
118
119
|
'intention:rest': ['to rest'],
|
|
120
|
+
'intention:bladder': ['to use the toilet'],
|
|
119
121
|
'intention:relief': ['to ease the pain'],
|
|
120
122
|
'intention:belonging': ['to be with people'],
|
|
121
123
|
'intention:leisure': ['to enjoy myself'],
|
package/dist/person.js
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
import { actionTendencies, advanceAffect, appraise, CRISIS_DEFAULTS, checkCrisis, createAffect, easeBreak, emotionLevel, feel, readAffect, regulate, release, skipAffect, skipCrisis, strain, tendencyEmotions, } from './affect/index.js';
|
|
23
23
|
import { advanceAgenda, agendaTermsWithGoalEstimates, createAgenda, onFinished, promise, proposeGoals, revisePurposes, } from './agenda/index.js';
|
|
24
24
|
import { advanceBeliefs, applyContradiction, attend, believe, confirm, credence, noteProvenance, trustOf, } from './beliefs/index.js';
|
|
25
|
+
import { enableBladder, voidBladder } from './body/bladder.js';
|
|
26
|
+
import { checkDoze, DOZE_DEFAULTS, enableDoze, enterVoluntarySleep, wakeDoze } from './body/doze.js';
|
|
25
27
|
import { advanceBody, BODY_DEFAULTS, checkDowned, clearDowned, consume, createBody, downedAllows, enableFatigueStore, enableFullness, expose, injure, nextBodyThreshold, readBody, setDowned, sicken, skipBody, } from './body/index.js';
|
|
26
28
|
import { ageCharacter, enableCharacterChange, noteCharacterDay, noteCharacterSocial, } from './character/index.js';
|
|
27
29
|
import { closeDay, enableYearbook, endDay, noteAnswer, noteCommitments, noteDecision, noteMood, noteOutcome, trimChronicle, } from './chronicle/index.js';
|
|
@@ -258,6 +260,10 @@ export function createPerson(spec) {
|
|
|
258
260
|
enablePromptings(p, on.promptings);
|
|
259
261
|
if (on?.review)
|
|
260
262
|
enableReview(p, on.review);
|
|
263
|
+
if (on?.doze)
|
|
264
|
+
enableDoze(p, on.doze);
|
|
265
|
+
if (on?.bladder)
|
|
266
|
+
enableBladder(p);
|
|
261
267
|
return p;
|
|
262
268
|
}
|
|
263
269
|
/**
|
|
@@ -473,10 +479,10 @@ export function tick(p, now) {
|
|
|
473
479
|
const seg = PERSON_DEFAULTS.maxSegment;
|
|
474
480
|
while (p.now < now) {
|
|
475
481
|
const act = p.activity;
|
|
476
|
-
const active = act !== null && p.now < act.endsAt;
|
|
482
|
+
const active = act !== null && act.dozedAt === undefined && p.body.doze?.since === undefined && p.now < act.endsAt;
|
|
477
483
|
// Segment boundaries sit on an absolute grid, so how a host chunks its calls does not change the result.
|
|
478
484
|
const grid = (Math.floor(p.now / seg) + 1) * seg;
|
|
479
|
-
const end = Math.min(active ? Math.min(now, act.endsAt) : now, grid);
|
|
485
|
+
const end = Math.min(active ? Math.min(now, act.endsAt) : now, grid, p.body.doze ? Math.floor(p.now) + 1 : Number.POSITIVE_INFINITY);
|
|
480
486
|
const load = active && act ? { effort: act.effort, focus: act.focus, mode: act.mode } : PERSON_DEFAULTS.idleLoad;
|
|
481
487
|
// A minute so large that the grid step rounds away cannot advance (security review H2): stop, never spin.
|
|
482
488
|
if (!(end > p.now)) {
|
|
@@ -488,6 +494,12 @@ export function tick(p, now) {
|
|
|
488
494
|
}
|
|
489
495
|
advanceSegment(p, end - p.now, load, active && act ? act.affordance : undefined);
|
|
490
496
|
p.now = end;
|
|
497
|
+
if (Number.isInteger(end) && p.body.doze && !(act && act.endsAt === end) && checkDoze(p, end)) {
|
|
498
|
+
if (act)
|
|
499
|
+
act.dozedAt ??= end;
|
|
500
|
+
// Physical onset must not erase a host/percept reason awaiting resolution.
|
|
501
|
+
interrupt(p, end, act?.interrupt?.reason ?? 'doze');
|
|
502
|
+
}
|
|
491
503
|
pruneConsequences(p);
|
|
492
504
|
if (p.pursuit)
|
|
493
505
|
releasePursuit(p);
|
|
@@ -852,6 +864,18 @@ function judgeDeed(p, actor, pc, at) {
|
|
|
852
864
|
export function perceive(p, percepts, options = {}) {
|
|
853
865
|
if (percepts.length === 0 || !p.body.alive)
|
|
854
866
|
return [];
|
|
867
|
+
if (p.body.doze?.since !== undefined) {
|
|
868
|
+
const wakes = percepts.some((pc) => Number.isFinite(pc.at) &&
|
|
869
|
+
pc.at === p.now &&
|
|
870
|
+
Number.isFinite(pc.salience) &&
|
|
871
|
+
pc.salience >= DOZE_DEFAULTS.wakeSalience &&
|
|
872
|
+
pc.near !== false &&
|
|
873
|
+
(pc.targetId === p.id || pc.near === true) &&
|
|
874
|
+
(pc.channel === 'heard' || pc.channel === 'felt' || pc.channel === 'social'));
|
|
875
|
+
if (!wakes)
|
|
876
|
+
return [];
|
|
877
|
+
wakeDoze(p, p.now);
|
|
878
|
+
}
|
|
855
879
|
const body = readBodyOf(p);
|
|
856
880
|
const attended = attend(p, [...percepts], {
|
|
857
881
|
focus: p.activity?.focus ?? 0,
|
|
@@ -1149,6 +1173,8 @@ function thresholdFor(p, load, review) {
|
|
|
1149
1173
|
export function begin(p, aff, record, opts = {}) {
|
|
1150
1174
|
if (!p.body.alive)
|
|
1151
1175
|
return null;
|
|
1176
|
+
if (p.body.doze?.since !== undefined && aff.mode !== 'sleep')
|
|
1177
|
+
return null;
|
|
1152
1178
|
const now = p.now;
|
|
1153
1179
|
if (p.pursuit) {
|
|
1154
1180
|
const considered = record.considered.find((c) => c.affordanceId === aff.id);
|
|
@@ -1224,6 +1250,8 @@ export function begin(p, aff, record, opts = {}) {
|
|
|
1224
1250
|
activity.commitmentId = c.id;
|
|
1225
1251
|
}
|
|
1226
1252
|
p.activity = activity;
|
|
1253
|
+
if (load.mode === 'sleep')
|
|
1254
|
+
enterVoluntarySleep(p);
|
|
1227
1255
|
advanceBody(p, 0, load, mods); // apply the sleep/wake switch now
|
|
1228
1256
|
const review = load.mode === 'sleep' ? PERSON_DEFAULTS.sleepReviewInterval : PERSON_DEFAULTS.reviewInterval;
|
|
1229
1257
|
const thresholdAt = thresholdFor(p, load, review);
|
|
@@ -1448,7 +1476,7 @@ export function finish(p, outcome, opts = {}) {
|
|
|
1448
1476
|
const now = p.now;
|
|
1449
1477
|
const aff = act.affordance;
|
|
1450
1478
|
const completed = outcome.status === 'completed';
|
|
1451
|
-
const minutes = Math.max(0, now - act.startedAt);
|
|
1479
|
+
const minutes = Math.max(0, Math.min(now, act.dozedAt ?? now) - act.startedAt);
|
|
1452
1480
|
const mods = lifeModifiers(p);
|
|
1453
1481
|
const D = PERSON_DEFAULTS;
|
|
1454
1482
|
const before = readNeeds(p, readBodyOf(p));
|
|
@@ -1461,6 +1489,8 @@ export function finish(p, outcome, opts = {}) {
|
|
|
1461
1489
|
if (given.relief)
|
|
1462
1490
|
bodyDeltas.relief = given.relief;
|
|
1463
1491
|
consume(p, bodyDeltas);
|
|
1492
|
+
if (completed && outcome.voidedBladder === true)
|
|
1493
|
+
voidBladder(p);
|
|
1464
1494
|
const psych = {};
|
|
1465
1495
|
for (const id of PSYCHOLOGICAL_NEEDS)
|
|
1466
1496
|
if (given[id] !== undefined)
|
package/dist/persona.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* identity, age, family and health compatibility before returning a new Person. Neither input is mutated.
|
|
5
5
|
* This is not a world migration, identity registry, transport, hash, authentication or general calendar mapper.
|
|
6
6
|
* v1 admits zero-duration travel and healthy compatible bodies only; family/bonds remain host-authored and must
|
|
7
|
-
* match source provenance. Portable v1 accepts its published 2.14.1
|
|
8
|
-
* the
|
|
7
|
+
* match source provenance. Portable v1 accepts its published 2.14.1 and 2.17.0 stamps and the current engine;
|
|
8
|
+
* the bodily opt-ins change no portable field semantics. Pregnancy and nonempty temporal bonds are unsupported in v1. All other world-dependent stores stay with the source and are disclosed as omitted.
|
|
9
9
|
*/
|
|
10
10
|
import { hasUnresolvedMakeUps, sameChain } from './agenda/agenda.js';
|
|
11
11
|
import { createPerson } from './person.js';
|
|
@@ -186,8 +186,10 @@ export function validatePersonaProjection(input) {
|
|
|
186
186
|
text(input.mappingVersion);
|
|
187
187
|
text(input.name);
|
|
188
188
|
keys(input.source, ['schema', 'engine', 'id', 'now']);
|
|
189
|
-
//
|
|
190
|
-
const compatibleEngine = input.source.engine === ENGINE_VERSION ||
|
|
189
|
+
// The 2.14.1 and 2.17.0 portable fields retain their semantics across the bodily opt-ins.
|
|
190
|
+
const compatibleEngine = input.source.engine === ENGINE_VERSION ||
|
|
191
|
+
input.source.engine === '2.17.0' ||
|
|
192
|
+
input.source.engine === '2.14.1';
|
|
191
193
|
if (input.source.schema !== PERSON_SCHEMA || !compatibleEngine)
|
|
192
194
|
fail('unsupported source schema/engine; migrate the source before projection');
|
|
193
195
|
text(input.source.id);
|
package/dist/sim/sim.d.ts
CHANGED
|
@@ -245,7 +245,7 @@ export declare function birth(c: Community, a: Person, b: Person, spec: ChildSpe
|
|
|
245
245
|
export declare function tellDeath(c: Community, dead: Person, at: Minute, opts?: {
|
|
246
246
|
late?: 'throw' | 'now';
|
|
247
247
|
}): void;
|
|
248
|
-
/**
|
|
248
|
+
/** Pure scheduling query, including passive bodily checkpoints during joint waiting. */
|
|
249
249
|
export declare function nextEventAt(c: Community, p: Person): Minute;
|
|
250
250
|
/**
|
|
251
251
|
* How this person would answer `suggestion` now, without changing state (UI telegraph; wraps `predict`). Pass the
|
package/dist/sim/sim.js
CHANGED
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
*/
|
|
51
51
|
import { appraise } from '../affect/index.js';
|
|
52
52
|
import { matchesCommitment, spanMeetsWindow } from '../agenda/index.js';
|
|
53
|
-
import { BODY_DEFAULTS, contagionRoll, die, readBody, sicken } from '../body/index.js';
|
|
53
|
+
import { BODY_DEFAULTS, contagionRoll, die, nextDozeCheck, readBody, sicken } from '../body/index.js';
|
|
54
54
|
import { chronicleBetween } from '../chronicle/index.js';
|
|
55
55
|
import { recordDeed } from '../conscience/index.js';
|
|
56
56
|
import { converse } from '../conversation/index.js';
|
|
@@ -274,7 +274,7 @@ function awaitingJoint(c, id) {
|
|
|
274
274
|
return pendingProposals(c).some((x) => x.proposerId === id || x.accepted.includes(id));
|
|
275
275
|
}
|
|
276
276
|
/** Next minute at which this person needs attention. */
|
|
277
|
-
|
|
277
|
+
function ordinaryEventAt(c, p) {
|
|
278
278
|
if (!p.body.alive)
|
|
279
279
|
return Number.POSITIVE_INFINITY;
|
|
280
280
|
if (awaitingJoint(c, p.id))
|
|
@@ -284,6 +284,12 @@ export function nextEventAt(c, p) {
|
|
|
284
284
|
return Math.max(p.now, Math.min(act.endsAt, act.reviewAt));
|
|
285
285
|
return Math.max(p.now, c.idleUntil[p.id] ?? p.now);
|
|
286
286
|
}
|
|
287
|
+
/** Pure scheduling query, including passive bodily checkpoints during joint waiting. */
|
|
288
|
+
export function nextEventAt(c, p) {
|
|
289
|
+
if (p.body.doze?.since !== undefined && awaitingJoint(c, p.id))
|
|
290
|
+
return p.now;
|
|
291
|
+
return Math.min(ordinaryEventAt(c, p), nextDozeCheck(p));
|
|
292
|
+
}
|
|
287
293
|
/**
|
|
288
294
|
* How this person would answer `suggestion` now, without changing state (UI telegraph; wraps `predict`). Pass the
|
|
289
295
|
* world's `scarcityFor(p)` as `scarcity`, as the step does, or the telegraph can promise what the step refuses.
|
|
@@ -415,7 +421,6 @@ export function acceptJoint(c, world, proposal, partner, partnerRecord, writeLog
|
|
|
415
421
|
proposal.partnerRecords[partner.id] = partnerRecord;
|
|
416
422
|
if (proposal.accepted.length < proposal.partnerIds.length)
|
|
417
423
|
return [];
|
|
418
|
-
settle(c, proposal, 'accepted');
|
|
419
424
|
const proposer = personById(c, proposal.proposerId);
|
|
420
425
|
const participants = [];
|
|
421
426
|
if (proposer)
|
|
@@ -427,6 +432,19 @@ export function acceptJoint(c, world, proposal, partner, partnerRecord, writeLog
|
|
|
427
432
|
participants.push({ person: q, offer: mirrorFor(world, q, proposal), record: rec });
|
|
428
433
|
}
|
|
429
434
|
const at = Math.max(...participants.map((x) => x.person.now));
|
|
435
|
+
// Opted-in catch-up can physically stop a participant. Validate everyone before any begin.
|
|
436
|
+
if (participants.some(({ person }) => person.body.doze !== undefined)) {
|
|
437
|
+
for (const { person } of participants)
|
|
438
|
+
if (person.now < at)
|
|
439
|
+
tick(person, at);
|
|
440
|
+
if (participants.some(({ person }) => person.body.doze?.since !== undefined)) {
|
|
441
|
+
declineJoint(c, proposal);
|
|
442
|
+
for (const { person } of participants)
|
|
443
|
+
interruptPerson(c, person, at, person.activity?.interrupt?.reason ?? `declined:${proposal.id}`);
|
|
444
|
+
return [];
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
settle(c, proposal, 'accepted');
|
|
430
448
|
const begun = [];
|
|
431
449
|
for (const { person, offer, record } of participants) {
|
|
432
450
|
if (person.now < at)
|
|
@@ -770,6 +788,26 @@ export function stepCommunity(c, world, until, opts = {}) {
|
|
|
770
788
|
c.queued ??= {};
|
|
771
789
|
c.dayDone ??= {};
|
|
772
790
|
synchronizeCommands(c, opts, log);
|
|
791
|
+
// Cancellation releases clocks AND review deadlines. Keep pending causes, including simultaneous doze.
|
|
792
|
+
const cancelDozeWaiting = (p, t) => {
|
|
793
|
+
for (const x of pendingProposals(c)) {
|
|
794
|
+
if (x.proposerId !== p.id && !x.partnerIds.includes(p.id))
|
|
795
|
+
continue;
|
|
796
|
+
const waiters = [x.proposerId, ...x.accepted]
|
|
797
|
+
.map((id) => personById(c, id))
|
|
798
|
+
.filter((q) => q !== undefined);
|
|
799
|
+
for (const q of waiters)
|
|
800
|
+
if (q.now < t)
|
|
801
|
+
tick(q, t);
|
|
802
|
+
const reasons = new Map(waiters.map((q) => [q.id, q.activity?.interrupt?.reason ?? c.interrupts[q.id]?.reason]));
|
|
803
|
+
if (x.proposerId === p.id)
|
|
804
|
+
settle(c, x, 'declined');
|
|
805
|
+
else
|
|
806
|
+
declineJoint(c, x);
|
|
807
|
+
for (const q of waiters)
|
|
808
|
+
interruptPerson(c, q, t, reasons.get(q.id) ?? `declined:${x.id}`);
|
|
809
|
+
}
|
|
810
|
+
};
|
|
773
811
|
// Pairs whose shared activity already ran its conversation and contagion at `pairMinute` (both partners
|
|
774
812
|
// finishing the same joint activity at the same minute would otherwise run them twice).
|
|
775
813
|
let pairMinute = Number.NEGATIVE_INFINITY;
|
|
@@ -787,7 +825,32 @@ export function stepCommunity(c, world, until, opts = {}) {
|
|
|
787
825
|
if (!next || t > until)
|
|
788
826
|
break;
|
|
789
827
|
const p = next;
|
|
828
|
+
const ordinaryDue = ordinaryEventAt(c, p) <= t;
|
|
790
829
|
tick(p, t);
|
|
830
|
+
const dozed = p.activity?.dozedAt !== undefined || p.body.doze?.since !== undefined;
|
|
831
|
+
if (dozed) {
|
|
832
|
+
cancelDozeWaiting(p, t);
|
|
833
|
+
const act = p.activity;
|
|
834
|
+
if (act?.dozedAt !== undefined) {
|
|
835
|
+
if (act.interrupt)
|
|
836
|
+
c.interrupts[p.id] ??= { ...act.interrupt };
|
|
837
|
+
const outcome = world.resolve(p, act, 'interrupted');
|
|
838
|
+
finish(p, outcome, finishOpts(world));
|
|
839
|
+
log({
|
|
840
|
+
at: t,
|
|
841
|
+
personId: p.id,
|
|
842
|
+
kind: 'finish',
|
|
843
|
+
detail: `${act.action} interrupted by doze`,
|
|
844
|
+
action: act.action,
|
|
845
|
+
status: outcome.status,
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
c.idleUntil[p.id] = t + quantum;
|
|
849
|
+
if (!ordinaryDue)
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
if (!ordinaryDue && p.body.alive)
|
|
853
|
+
continue;
|
|
791
854
|
bookkeepDay(p, world, t, c.dayDone, opts);
|
|
792
855
|
if (!p.body.alive) {
|
|
793
856
|
log({ at: t, personId: p.id, kind: 'died', detail: `${p.name} died` });
|
|
@@ -821,6 +884,11 @@ export function stepCommunity(c, world, until, opts = {}) {
|
|
|
821
884
|
}
|
|
822
885
|
}
|
|
823
886
|
}
|
|
887
|
+
if (p.body.doze?.since !== undefined) {
|
|
888
|
+
cancelDozeWaiting(p, t);
|
|
889
|
+
c.idleUntil[p.id] = t + quantum;
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
824
892
|
let act = p.activity;
|
|
825
893
|
// An interrupt on an activity that ends at this event still reaches the next decision.
|
|
826
894
|
perceptReason ??= act?.interrupt?.reason;
|
package/dist/types.d.ts
CHANGED
|
@@ -65,7 +65,22 @@ export interface Illness {
|
|
|
65
65
|
/** Exposure kinds (keys of `BodyState.exposures`) whose recent load raises this chronic condition. */
|
|
66
66
|
aggravatedBy?: string[];
|
|
67
67
|
}
|
|
68
|
+
export interface DozeContext {
|
|
69
|
+
posture: 'upright' | 'seated' | 'reclined';
|
|
70
|
+
/** Host-declared alerting demand, not volume. */
|
|
71
|
+
stimulation: Unit;
|
|
72
|
+
}
|
|
73
|
+
export interface DozeState {
|
|
74
|
+
context: DozeContext;
|
|
75
|
+
since?: Minute;
|
|
76
|
+
}
|
|
77
|
+
export interface BladderState {
|
|
78
|
+
fill: Unit;
|
|
79
|
+
pending: number;
|
|
80
|
+
}
|
|
68
81
|
export interface BodyState {
|
|
82
|
+
doze?: DozeState;
|
|
83
|
+
bladder?: BladderState;
|
|
69
84
|
/** 1 = fully fed, 0 = starving. */
|
|
70
85
|
satiety: Unit;
|
|
71
86
|
/** 1 = fully hydrated. */
|
|
@@ -848,6 +863,8 @@ export interface Activity {
|
|
|
848
863
|
at: Minute;
|
|
849
864
|
reason: string;
|
|
850
865
|
};
|
|
866
|
+
/** Involuntary doze stopped this activity here. Work and practice stay stopped until host resolution. */
|
|
867
|
+
dozedAt?: Minute;
|
|
851
868
|
/**
|
|
852
869
|
* Set by `begin` when the chosen act was possible only because necessity / the capacity bound lifted a
|
|
853
870
|
* conscience veto (eating forbidden food, or drinking during the fast, in extremity). `finish` then records no
|
|
@@ -898,7 +915,7 @@ export declare const PERSON_SCHEMA = "human/person@1";
|
|
|
898
915
|
* and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
|
|
899
916
|
* `dutyReviewAt`). Person shape unchanged.
|
|
900
917
|
*/
|
|
901
|
-
export declare const ENGINE_VERSION = "2.
|
|
918
|
+
export declare const ENGINE_VERSION = "2.21.0";
|
|
902
919
|
/** A pregnancy carried by this person (host-driven; see `family/`). */
|
|
903
920
|
export interface Pregnancy {
|
|
904
921
|
fatherId: PersonId;
|
|
@@ -1181,6 +1198,8 @@ export interface Affordance {
|
|
|
1181
1198
|
};
|
|
1182
1199
|
/** Advertised need deltas over the whole activity (positive = satisfies). */
|
|
1183
1200
|
advertises: Partial<Record<NeedId, number>>;
|
|
1201
|
+
/** Intended urination: actual voiding, not completion alone, defines expectation success. */
|
|
1202
|
+
voidsBladder?: boolean;
|
|
1184
1203
|
/** Free tags: 'work', 'leisure', 'social', 'worship', 'risky', 'outdoors', ... */
|
|
1185
1204
|
tags?: string[];
|
|
1186
1205
|
norms?: NormTag[];
|
|
@@ -1335,6 +1354,8 @@ export interface Outcome {
|
|
|
1335
1354
|
at: Minute;
|
|
1336
1355
|
/** Realized need deltas from world effects (e.g. food eaten). Body exertion is computed by the framework. */
|
|
1337
1356
|
needs?: Partial<Record<NeedId, number>>;
|
|
1357
|
+
/** Host-confirmed urination; only a completed activity clears stored bladder fill. */
|
|
1358
|
+
voidedBladder?: boolean;
|
|
1338
1359
|
material?: number;
|
|
1339
1360
|
/** Harm suffered. */
|
|
1340
1361
|
injury?: Omit<Injury, 'id' | 'since'>;
|
|
@@ -1538,6 +1559,8 @@ export interface PersonSpec {
|
|
|
1538
1559
|
* `enableCharacterChange` and `enableSkillRetention` right after `createPerson`. Each is off when absent.
|
|
1539
1560
|
*/
|
|
1540
1561
|
enable?: {
|
|
1562
|
+
doze?: DozeContext;
|
|
1563
|
+
bladder?: boolean;
|
|
1541
1564
|
/** Read-only suggestion score explanations, as with `enableSuggestionReasons(p)`. */
|
|
1542
1565
|
suggestionReasons?: boolean;
|
|
1543
1566
|
gists?: boolean;
|
package/dist/types.js
CHANGED
|
@@ -120,4 +120,4 @@ export const PERSON_SCHEMA = 'human/person@1';
|
|
|
120
120
|
* and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
|
|
121
121
|
* `dutyReviewAt`). Person shape unchanged.
|
|
122
122
|
*/
|
|
123
|
-
export const ENGINE_VERSION = '2.
|
|
123
|
+
export const ENGINE_VERSION = '2.21.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adam0white/human-framework",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.21.0",
|
|
4
4
|
"description": "Deterministic, explainable simulated people: body, needs, affect, memory, beliefs, skills, habits, relationships, values and a will that can refuse a suggestion",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"simulation",
|