@adam0white/human-framework 2.5.0 → 2.6.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.
@@ -30,6 +30,15 @@ export declare const BODY_DEFAULTS: {
30
30
  exertionRiseHalfLife: number;
31
31
  exertionRecoverHalfLife: number;
32
32
  exertionRecoverHalfLifeAsleep: number;
33
+ /** Fatigue store (2.6.0, opt-in): depletion per hour at full effort (× (1 - 0.5 fitness) × (1 - depletion)). */
34
+ depletionPerHour: number;
35
+ /** Effort below which the store does not fill and recovers while awake. */
36
+ depletionFloor: number;
37
+ /** Half-life of depletion at rest awake and asleep (minutes, divided by the life-course recovery factor). */
38
+ depletionRestHalfLife: number;
39
+ depletionSleepHalfLife: number;
40
+ /** How much a full store adds to felt fatigue: fatigue = 1 - (1 - exertion)(1 - 0.5 s)(1 - weight × depletion). */
41
+ depletionFatigueWeight: number;
33
42
  /** Effort above which fitness trains. */
34
43
  trainingEffort: number;
35
44
  /** Detraining half-life (min) toward fitnessFloor. */
@@ -116,6 +125,8 @@ export interface BodyPerceptionContext {
116
125
  fastingDays: number;
117
126
  }
118
127
  export type BodyParams = typeof BODY_DEFAULTS;
128
+ /** Turn on the fatigue store (2.6.0): accumulated exertion that recovers with rest and sleep. Idempotent. */
129
+ export declare function enableFatigueStore(p: Person): void;
119
130
  export declare function createBody(init: PersonSpec['body'] | undefined, now: Minute): BodyState;
120
131
  /**
121
132
  * Advance `p.body` over `dt` minutes under `load`. Call with `p.now` at the start of the interval: a mode
@@ -205,4 +216,4 @@ export declare function forecastBody(p: Person, spans: readonly {
205
216
  label: string;
206
217
  duration: number;
207
218
  load: BodyLoad;
208
- }[], mods: LifeModifiers, params?: BodyParams, step?: number): BodyForecastSpan[];
219
+ }[], mods: LifeModifiers, params?: BodyParams, step?: number, perception?: BodyPerceptionContext): BodyForecastSpan[];
package/dist/body/body.js CHANGED
@@ -76,6 +76,15 @@ export const BODY_DEFAULTS = {
76
76
  exertionRiseHalfLife: 40,
77
77
  exertionRecoverHalfLife: 45,
78
78
  exertionRecoverHalfLifeAsleep: 25,
79
+ /** Fatigue store (2.6.0, opt-in): depletion per hour at full effort (× (1 - 0.5 fitness) × (1 - depletion)). */
80
+ depletionPerHour: 0.3,
81
+ /** Effort below which the store does not fill and recovers while awake. */
82
+ depletionFloor: 0.3,
83
+ /** Half-life of depletion at rest awake and asleep (minutes, divided by the life-course recovery factor). */
84
+ depletionRestHalfLife: 8 * 60,
85
+ depletionSleepHalfLife: 3 * 60,
86
+ /** How much a full store adds to felt fatigue: fatigue = 1 - (1 - exertion)(1 - 0.5 s)(1 - weight × depletion). */
87
+ depletionFatigueWeight: 1,
79
88
  /** Effort above which fitness trains. */
80
89
  trainingEffort: 0.4,
81
90
  /** Detraining half-life (min) toward fitnessFloor. */
@@ -229,6 +238,13 @@ export function sanitizeBody(b, now) {
229
238
  delete b.lastSleep;
230
239
  if (b.lastDowned !== undefined && !span(b.lastDowned))
231
240
  delete b.lastDowned;
241
+ if (b.depletion !== undefined)
242
+ b.depletion = typeof b.depletion === 'number' && Number.isFinite(b.depletion) ? clamp01(b.depletion) : 0;
243
+ }
244
+ /** Turn on the fatigue store (2.6.0): accumulated exertion that recovers with rest and sleep. Idempotent. */
245
+ export function enableFatigueStore(p) {
246
+ if (p.body.depletion === undefined)
247
+ p.body.depletion = 0;
232
248
  }
233
249
  /**
234
250
  * Repair the optional chronic fields of saved illnesses in place: a non-finite or out-of-range `baseline` is
@@ -367,6 +383,18 @@ function subStep(b, h, load, mods, P) {
367
383
  ? P.exertionRiseHalfLife
368
384
  : (asleep ? P.exertionRecoverHalfLifeAsleep : P.exertionRecoverHalfLife) / recovery;
369
385
  b.exertion = clamp01(decay(b.exertion, h, hl, target));
386
+ // Fatigue store (2.6.0, opt-in): fills with sustained effort above the floor, empties with rest and sleep.
387
+ if (b.depletion !== undefined) {
388
+ const d = clamp01(Number.isFinite(b.depletion) ? b.depletion : 0);
389
+ if (effortUsed > P.depletionFloor) {
390
+ const rate = (P.depletionPerHour * (effortUsed - P.depletionFloor)) / (1 - P.depletionFloor);
391
+ b.depletion = clamp01(d + (rate * (1 - 0.5 * b.fitness) * (1 - d) * h) / 60);
392
+ }
393
+ else {
394
+ const half = (asleep ? P.depletionSleepHalfLife : P.depletionRestHalfLife) / recovery;
395
+ b.depletion = clamp01(decay(d, h, half, 0));
396
+ }
397
+ }
370
398
  // Fitness: dF/dt = -kd (F - floor) + kt (max - F), exact for constant load.
371
399
  const maxFit = clamp01(mods.maxFitness);
372
400
  const kd = LN2 / P.detrainHalfLife;
@@ -501,7 +529,8 @@ export function readBody(p, params = BODY_DEFAULTS, ctx) {
501
529
  const sleepiness = clamp01(params.sleepinessPressureWeight * b.sleepPressure +
502
530
  params.sleepinessCircadianWeight * (1 - alertness) +
503
531
  debtTerm);
504
- const fatigueOf = (s) => clamp01(1 - (1 - b.exertion) * (1 - 0.5 * s));
532
+ const store = b.depletion === undefined ? 1 : 1 - params.depletionFatigueWeight * clamp01(b.depletion);
533
+ const fatigueOf = (s) => clamp01(1 - (1 - b.exertion) * (1 - 0.5 * s) * store);
505
534
  const fatigue = fatigueOf(sleepiness);
506
535
  const hunger = clamp01(1 - b.satiety);
507
536
  const thirst = clamp01(1 - b.hydration);
@@ -767,7 +796,7 @@ export function nextBodyThreshold(p, load, mods, thresholds = { hunger: 0.7, thi
767
796
  * The person's own forecast of their body through consecutive spans of known exertion, read as they perceive it
768
797
  * (2.5.0). Simulates a cloned body in steps of at most `step` minutes; pure (does not mutate `p`).
769
798
  */
770
- export function forecastBody(p, spans, mods, params = BODY_DEFAULTS, step = 30) {
799
+ export function forecastBody(p, spans, mods, params = BODY_DEFAULTS, step = 30, perception) {
771
800
  const out = [];
772
801
  const sim = { ...p, body: cloneBody(p.body) };
773
802
  for (const s of spans) {
@@ -775,7 +804,7 @@ export function forecastBody(p, spans, mods, params = BODY_DEFAULTS, step = 30)
775
804
  // Hard internal cap (a week) so a hostile span cannot stall a decision.
776
805
  let left = Math.min(7 * MINUTES_PER_DAY, Math.max(0, Number.isFinite(s.duration) ? s.duration : 0));
777
806
  advanceBody(sim, 0, s.load, mods, params);
778
- const r0 = readBody(sim, params).perceived;
807
+ const r0 = readBody(sim, params, perception).perceived;
779
808
  for (const k of ['fatigue', 'sleepiness', 'hunger', 'thirst'])
780
809
  if (r0[k] > span.worst) {
781
810
  span.worst = r0[k];
@@ -786,7 +815,7 @@ export function forecastBody(p, spans, mods, params = BODY_DEFAULTS, step = 30)
786
815
  advanceBody(sim, dt, s.load, mods, params);
787
816
  sim.now += dt;
788
817
  left -= dt;
789
- const r = readBody(sim, params).perceived;
818
+ const r = readBody(sim, params, perception).perceived;
790
819
  for (const k of ['fatigue', 'sleepiness', 'hunger', 'thirst'])
791
820
  if (r[k] > span.worst) {
792
821
  span.worst = r[k];
@@ -1,3 +1,4 @@
1
+ import type { BodyPerceptionContext } from '../body/index.ts';
1
2
  import { type HabitContext } from '../habits/index.ts';
2
3
  import { type PromptingSeen } from '../promptings/index.ts';
3
4
  import type { Affordance, BodyReadout, Command, Considered, DecisionRecord, LifeModifiers, Minute, NeedReading, Person, Suggestion, Term, Unit } from '../types.ts';
@@ -100,6 +101,8 @@ export interface ConsiderContext {
100
101
  * leaves the material and safety terms exactly as without it.
101
102
  */
102
103
  scarcity?: Unit;
104
+ /** The body's perception context now (an open fast), for the bodily forecast (2.6.0). */
105
+ perception?: BodyPerceptionContext;
103
106
  }
104
107
  /**
105
108
  * Standing advice terms for one option: per source, the strongest matching entry's weight × trust in the source
@@ -351,7 +351,7 @@ export function consider(p, aff, ctx) {
351
351
  push('effort', effort);
352
352
  // Bodily forecast (2.5.0): only offers that declare known follow-on exertion.
353
353
  if (aff.followedBy?.length) {
354
- const t = bodyForecastTerm(p, aff, ctx.mods);
354
+ const t = bodyForecastTerm(p, aff, ctx.mods, ctx.perception);
355
355
  if (t)
356
356
  push(t.source, t.value);
357
357
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework v1 public surface. */
2
- export declare const FRAMEWORK_VERSION = "2.5.0";
2
+ export declare const FRAMEWORK_VERSION = "2.6.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, PrayerCalendar, PrayerSchedule, PrayerTimes, Retimer, } from './agenda/index.ts';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Human Framework v1 public surface. */
2
- export const FRAMEWORK_VERSION = '2.5.0';
2
+ export const FRAMEWORK_VERSION = '2.6.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, isUnderWay, MAKRUH_DEFAULTS, makruhWindows, missedExcuse, onFinished, owedMakeUps, PRAYER_IMPORTANCE, PURPOSE_DEFAULTS, prayerWindows, pressureReachedAt, promise, proposeGoals, RAMADAN_DEFAULTS, ramadanFast, ramadanMeals, release as releaseCommitment, retimeCommitments, revisePurposes, scheduleMakeUp, spanMeetsWindow, suhoorWindow, timesFor, violatesAbstention, } from './agenda/index.js';
package/dist/migrate.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * field of the optional surroundings slice (`ambient.now` → `ambient.percept`) and changes no behaviour (the optional `social.impressions` and `social.reserve`, like `retention`, are optional and start
9
9
  * absent in a migrated save); `restore` then fills and sanitizes
10
10
  * the slices as for any save. 2.0.0 to 2.1.0 adds optional conditional method state, absent
11
- * until explicitly used, so existing hosts retain their previous behavior; that step stamps the version. 2.1.0 to 2.2.0 preserves optional method records and stamps the version; successful alternatives now retain independent support. 2.2.0 to 2.3.0 stamps the version; received consequences and temporal goals remain absent unless explicitly supplied. 2.3.0 to 2.4.0 stamps the version; promptings remain absent unless enabled. 2.4.0 to 2.5.0 stamps the version; the bodily forecast reads only offers that declare `followedBy`. A migrated save continues under the current engine's rules: it restores and runs,
11
+ * until explicitly used, so existing hosts retain their previous behavior; that step stamps the version. 2.1.0 to 2.2.0 preserves optional method records and stamps the version; successful alternatives now retain independent support. 2.2.0 to 2.3.0 stamps the version; received consequences and temporal goals remain absent unless explicitly supplied. 2.3.0 to 2.4.0 stamps the version; promptings remain absent unless enabled. 2.4.0 to 2.5.0 stamps the version; the bodily forecast reads only offers that declare `followedBy`. 2.5.0 to 2.6.0 stamps the version; the fatigue store stays absent unless enabled. A migrated save continues under the current engine's rules: it restores and runs,
12
12
  * but it does not reproduce what the old engine would have done next where the rules changed (1.4.0 to 1.5.0
13
13
  * changed how standing advice is heard; 1.8.0 to 1.9.0 changed the omission rule). Saves older than 1.4.0 and unknown versions are refused with an error.
14
14
  *
@@ -58,6 +58,11 @@ export const MIGRATIONS = {
58
58
  note: 'the surroundings field `ambient.now` is renamed `ambient.percept`; per-person retention is optional and starts absent; behaviour unchanged',
59
59
  apply: renameAmbient,
60
60
  },
61
+ '2.5.0': {
62
+ to: '2.6.0',
63
+ note: "the fatigue store (body.depletion) is optional and starts absent; the bodily forecast now applies an open fast's hunger damping, which changes only offers that declare followedBy",
64
+ apply: stamp,
65
+ },
61
66
  '2.4.0': {
62
67
  to: '2.5.0',
63
68
  note: 'person shape unchanged; offers may declare followedBy spans for a bodily forecast; offers without it are unchanged',
package/dist/person.js CHANGED
@@ -22,7 +22,7 @@
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, createAgenda, onFinished, promise, proposeGoals, revisePurposes, } from './agenda/index.js';
24
24
  import { advanceBeliefs, attend, believe, confirm, credence } from './beliefs/index.js';
25
- import { advanceBody, BODY_DEFAULTS, checkDowned, clearDowned, consume, createBody, downedAllows, expose, injure, nextBodyThreshold, readBody, setDowned, sicken, skipBody, } from './body/index.js';
25
+ import { advanceBody, BODY_DEFAULTS, checkDowned, clearDowned, consume, createBody, downedAllows, enableFatigueStore, expose, injure, nextBodyThreshold, readBody, setDowned, sicken, skipBody, } from './body/index.js';
26
26
  import { ageCharacter, enableCharacterChange, noteCharacterDay, noteCharacterSocial, } from './character/index.js';
27
27
  import { closeDay, enableYearbook, endDay, noteAnswer, noteCommitments, noteDecision, noteMood, noteOutcome, trimChronicle, } from './chronicle/index.js';
28
28
  import { decide as cognitionDecide, desperationOf, scoreAll } from './cognition/index.js';
@@ -154,6 +154,8 @@ export function createPerson(spec) {
154
154
  enableCharacterChange(p);
155
155
  if (on?.skillRetention)
156
156
  enableSkillRetention(p);
157
+ if (on?.fatigueStore)
158
+ enableFatigueStore(p);
157
159
  if (on?.promptings)
158
160
  enablePromptings(p, on.promptings);
159
161
  return p;
@@ -732,6 +734,9 @@ function decisionInputs(p, affordances, id, opts) {
732
734
  necessity: opts.necessity ?? true,
733
735
  tendencyEmotions: tendencyEmotions(p),
734
736
  };
737
+ const perception = fastingCtx(p);
738
+ if (perception)
739
+ ctx.perception = perception;
735
740
  if (opts.suggestion)
736
741
  ctx.suggestion = opts.suggestion;
737
742
  // Only set when voices were given this way, so single-voice records keep their shape.
@@ -1415,6 +1420,6 @@ export function nafsReading(p) {
1415
1420
  * first-person line ("I can do it, but I'll be spent for the descent."). Empty spans without `followedBy`.
1416
1421
  */
1417
1422
  export function bodyForecast(p, aff) {
1418
- const spans = bodyForecastFor(p, aff, lifeModifiers(p));
1423
+ const spans = bodyForecastFor(p, aff, lifeModifiers(p), fastingCtx(p));
1419
1424
  return { spans, line: forecastLine(spans) };
1420
1425
  }
@@ -7,18 +7,18 @@
7
7
  * spentAt, worst), joins the offer's utility; it is pure and draws no randomness. Offers without `followedBy`
8
8
  * get no forecast, so existing hosts continue exactly.
9
9
  *
10
- * The forecast inherits the body model's limits and the person's misperceptions: HF exertion relaxes toward the
11
- * current effort (no cumulative glycogen or muscle-damage store), so a long climb does not by itself leave a
12
- * person more spent for the descent unless sleepiness, hunger, thirst or the descent's own effort do; unfelt
13
- * sleep debt stays unfelt in the forecast; a fast's damped hunger and thirst are not applied (so a fasting
14
- * person forecasts them undamped); host surroundings are not applied (the person's own body rates and life
10
+ * The forecast inherits the body model's limits and the person's misperceptions. Exertion relaxes toward the
11
+ * current effort; only a person with the opt-in fatigue store (2.6.0, `enableFatigueStore`) carries accumulated
12
+ * exertion, so only then does a long climb by itself leave them more spent for the descent; unfelt
13
+ * sleep debt stays unfelt in the forecast; an open fast's damping of hunger and thirst applies (2.6.0) as it is
14
+ * now, held for the whole forecast even if the fast would end during it; host surroundings are not applied (the person's own body rates and life
15
15
  * modifiers are, but not `ambient` parameters). Existing bleeding, illness and healing run in the copy, so an
16
16
  * open wound can read as being spent, and death in the forecast reads as fully spent. The term is absolute
17
17
  * (see `bodyForecastTerm`). It does not plan, search routes, schedule rests on the way or
18
18
  * predict injuries, and it is not a calibrated estimate of human anticipation. Every number is an engineering
19
19
  * default.
20
20
  */
21
- import { type BodyForecastSpan } from '../body/index.ts';
21
+ import { type BodyForecastSpan, type BodyPerceptionContext } from '../body/index.ts';
22
22
  import type { Affordance, LifeModifiers, Person, Term } from '../types.ts';
23
23
  export declare const FORECAST_DEFAULTS: {
24
24
  /** Worst perceived readout during a follow-on from which the forecast starts to weigh. */
@@ -32,7 +32,7 @@ export declare const FORECAST_DEFAULTS: {
32
32
  readonly maxMinutes: number;
33
33
  };
34
34
  /** The person's forecast for an offer: the act, then its follow-on spans. Empty without `followedBy`. Pure. */
35
- export declare function bodyForecastFor(p: Person, aff: Affordance, mods: LifeModifiers): BodyForecastSpan[];
35
+ export declare function bodyForecastFor(p: Person, aff: Affordance, mods: LifeModifiers, perception?: BodyPerceptionContext): BodyForecastSpan[];
36
36
  /**
37
37
  * The `forecast:body` term for an offer, or undefined (no follow-on, or not expected to weigh). Absolute by
38
38
  * design: it weighs being in that state *while doing* the follow-on, so a person already sleepy or thirsty is
@@ -40,6 +40,6 @@ export declare function bodyForecastFor(p: Person, aff: Affordance, mods: LifeMo
40
40
  * costs more than staying home). A resting baseline was tried and rejected: thirst and sleepiness rise at rest
41
41
  * too, which cancelled the very case the forecast exists for. Pure.
42
42
  */
43
- export declare function bodyForecastTerm(p: Person, aff: Affordance, mods: LifeModifiers): Term | undefined;
43
+ export declare function bodyForecastTerm(p: Person, aff: Affordance, mods: LifeModifiers, perception?: BodyPerceptionContext): Term | undefined;
44
44
  /** A first-person line for the forecast (narration only). */
45
45
  export declare function forecastLine(spans: readonly BodyForecastSpan[]): string;
@@ -7,18 +7,18 @@
7
7
  * spentAt, worst), joins the offer's utility; it is pure and draws no randomness. Offers without `followedBy`
8
8
  * get no forecast, so existing hosts continue exactly.
9
9
  *
10
- * The forecast inherits the body model's limits and the person's misperceptions: HF exertion relaxes toward the
11
- * current effort (no cumulative glycogen or muscle-damage store), so a long climb does not by itself leave a
12
- * person more spent for the descent unless sleepiness, hunger, thirst or the descent's own effort do; unfelt
13
- * sleep debt stays unfelt in the forecast; a fast's damped hunger and thirst are not applied (so a fasting
14
- * person forecasts them undamped); host surroundings are not applied (the person's own body rates and life
10
+ * The forecast inherits the body model's limits and the person's misperceptions. Exertion relaxes toward the
11
+ * current effort; only a person with the opt-in fatigue store (2.6.0, `enableFatigueStore`) carries accumulated
12
+ * exertion, so only then does a long climb by itself leave them more spent for the descent; unfelt
13
+ * sleep debt stays unfelt in the forecast; an open fast's damping of hunger and thirst applies (2.6.0) as it is
14
+ * now, held for the whole forecast even if the fast would end during it; host surroundings are not applied (the person's own body rates and life
15
15
  * modifiers are, but not `ambient` parameters). Existing bleeding, illness and healing run in the copy, so an
16
16
  * open wound can read as being spent, and death in the forecast reads as fully spent. The term is absolute
17
17
  * (see `bodyForecastTerm`). It does not plan, search routes, schedule rests on the way or
18
18
  * predict injuries, and it is not a calibrated estimate of human anticipation. Every number is an engineering
19
19
  * default.
20
20
  */
21
- import { forecastBody } from '../body/index.js';
21
+ import { BODY_DEFAULTS, forecastBody, } from '../body/index.js';
22
22
  import { clamp01, round } from '../core/index.js';
23
23
  export const FORECAST_DEFAULTS = {
24
24
  /** Worst perceived readout during a follow-on from which the forecast starts to weigh. */
@@ -36,7 +36,7 @@ const smooth = (a, b, x) => {
36
36
  return t * t * (3 - 2 * t);
37
37
  };
38
38
  /** The person's forecast for an offer: the act, then its follow-on spans. Empty without `followedBy`. Pure. */
39
- export function bodyForecastFor(p, aff, mods) {
39
+ export function bodyForecastFor(p, aff, mods, perception) {
40
40
  const follow = aff.followedBy;
41
41
  if (!Array.isArray(follow) || follow.length === 0 || !p.body.alive)
42
42
  return [];
@@ -67,7 +67,7 @@ export function bodyForecastFor(p, aff, mods) {
67
67
  load: { effort: clamp01(f.effort), focus: clamp01(f.focus ?? 0), mode: 'awake' },
68
68
  });
69
69
  }
70
- return forecastBody(p, spans, mods);
70
+ return forecastBody(p, spans, mods, BODY_DEFAULTS, 30, perception);
71
71
  }
72
72
  /**
73
73
  * The `forecast:body` term for an offer, or undefined (no follow-on, or not expected to weigh). Absolute by
@@ -76,8 +76,8 @@ export function bodyForecastFor(p, aff, mods) {
76
76
  * costs more than staying home). A resting baseline was tried and rejected: thirst and sleepiness rise at rest
77
77
  * too, which cancelled the very case the forecast exists for. Pure.
78
78
  */
79
- export function bodyForecastTerm(p, aff, mods) {
80
- const spans = bodyForecastFor(p, aff, mods).slice(1);
79
+ export function bodyForecastTerm(p, aff, mods, perception) {
80
+ const spans = bodyForecastFor(p, aff, mods, perception).slice(1);
81
81
  if (spans.length === 0)
82
82
  return undefined;
83
83
  const D = FORECAST_DEFAULTS;
package/dist/types.d.ts CHANGED
@@ -86,6 +86,11 @@ export interface BodyState {
86
86
  illnesses: Illness[];
87
87
  /** Accumulated sleep debt in hours (chronic restriction); under-perceived by the person. */
88
88
  sleepDebt: number;
89
+ /**
90
+ * Accumulated exertion (2.6.0, opt-in via `enableFatigueStore`): 0..1, rising slowly with sustained effort and
91
+ * recovering with rest and faster with sleep. Raises felt fatigue and lowers capacity. Absent = no store.
92
+ */
93
+ depletion?: Unit;
89
94
  asleep: boolean;
90
95
  /** Minute the current sleep or wake period began. */
91
96
  since: Minute;
@@ -827,7 +832,7 @@ export declare const PERSON_SCHEMA = "human/person@1";
827
832
  * and an activity that would cover a closing stretch is reviewed when the stretch begins (`isUnderWay`,
828
833
  * `dutyReviewAt`). Person shape unchanged.
829
834
  */
830
- export declare const ENGINE_VERSION = "2.5.0";
835
+ export declare const ENGINE_VERSION = "2.6.0";
831
836
  /** A pregnancy carried by this person (host-driven; see `family/`). */
832
837
  export interface Pregnancy {
833
838
  fatherId: PersonId;
@@ -1393,6 +1398,8 @@ export interface PersonSpec {
1393
1398
  yearbook?: boolean;
1394
1399
  character?: boolean;
1395
1400
  skillRetention?: boolean;
1401
+ /** Accumulated exertion (2.6.0): the same as `enableFatigueStore(p)` after creation. */
1402
+ fatigueStore?: boolean;
1396
1403
  /** Promptings and turning away (2.4.0): the same as `enablePromptings(p, spec)` after creation. */
1397
1404
  promptings?: PromptingsSpec;
1398
1405
  };
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.5.0';
123
+ export const ENGINE_VERSION = '2.6.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adam0white/human-framework",
3
- "version": "2.5.0",
3
+ "version": "2.6.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",