@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 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`, now `2.14.1`) and `FRAMEWORK_VERSION`, the same
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: bladder, temperature, specific nutrients, endocrine and immune detail,
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
+ }
@@ -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';
@@ -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,8 @@
1
+ import type { Person } from '../types.ts';
2
+ export declare function estimateQuantity(p: Person, input: {
3
+ truth: number;
4
+ difficulty: number;
5
+ }): {
6
+ value: number;
7
+ confidence: number;
8
+ };
@@ -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.20.0";
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.20.0';
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';
@@ -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
- const success = outcome.status === 'completed';
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',
@@ -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'],
@@ -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 stamp and the current engine;
8
- * the 2.14.1 to 2.17.0 migration changes 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.
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
- // Both published passport releases used 2.14.1; the next engine upgrade is stamp-only here.
190
- const compatibleEngine = input.source.engine === ENGINE_VERSION || input.source.engine === '2.14.1';
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
- /** Next minute at which this person needs attention. */
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
- export function nextEventAt(c, p) {
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.17.0";
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.17.0';
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.20.0",
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",