@adam0white/human-framework 2.4.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.
- package/dist/body/body.d.ts +28 -0
- package/dist/body/body.js +67 -1
- package/dist/cognition/cognition.d.ts +3 -0
- package/dist/cognition/cognition.js +7 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/migrate.js +11 -1
- package/dist/person.d.ts +9 -0
- package/dist/person.js +15 -2
- package/dist/prospection/forecast.d.ts +45 -0
- package/dist/prospection/forecast.js +103 -0
- package/dist/prospection/index.d.ts +1 -0
- package/dist/prospection/index.js +1 -0
- package/dist/types.d.ts +24 -2
- package/dist/types.js +1 -1
- package/package.json +1 -1
package/dist/body/body.d.ts
CHANGED
|
@@ -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
|
|
@@ -189,3 +200,20 @@ export interface BodyThresholds {
|
|
|
189
200
|
* then refines minute by minute inside the crossing step. Does not mutate `p`.
|
|
190
201
|
*/
|
|
191
202
|
export declare function nextBodyThreshold(p: Person, load: BodyLoad, mods: LifeModifiers, thresholds?: BodyThresholds, horizon?: number, params?: BodyParams, perception?: BodyPerceptionContext): number;
|
|
203
|
+
/** One forecast span: where it ends and the worst perceived readout during it. */
|
|
204
|
+
export interface BodyForecastSpan {
|
|
205
|
+
label: string;
|
|
206
|
+
endsAt: Minute;
|
|
207
|
+
/** Worst perceived fatigue, sleepiness, hunger or thirst during the span (0..1), and which. */
|
|
208
|
+
worst: Unit;
|
|
209
|
+
worstOf: 'fatigue' | 'sleepiness' | 'hunger' | 'thirst';
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The person's own forecast of their body through consecutive spans of known exertion, read as they perceive it
|
|
213
|
+
* (2.5.0). Simulates a cloned body in steps of at most `step` minutes; pure (does not mutate `p`).
|
|
214
|
+
*/
|
|
215
|
+
export declare function forecastBody(p: Person, spans: readonly {
|
|
216
|
+
label: string;
|
|
217
|
+
duration: number;
|
|
218
|
+
load: BodyLoad;
|
|
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
|
|
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);
|
|
@@ -763,3 +792,40 @@ export function nextBodyThreshold(p, load, mods, thresholds = { hunger: 0.7, thi
|
|
|
763
792
|
}
|
|
764
793
|
return Number.POSITIVE_INFINITY;
|
|
765
794
|
}
|
|
795
|
+
/**
|
|
796
|
+
* The person's own forecast of their body through consecutive spans of known exertion, read as they perceive it
|
|
797
|
+
* (2.5.0). Simulates a cloned body in steps of at most `step` minutes; pure (does not mutate `p`).
|
|
798
|
+
*/
|
|
799
|
+
export function forecastBody(p, spans, mods, params = BODY_DEFAULTS, step = 30, perception) {
|
|
800
|
+
const out = [];
|
|
801
|
+
const sim = { ...p, body: cloneBody(p.body) };
|
|
802
|
+
for (const s of spans) {
|
|
803
|
+
const span = { label: s.label, endsAt: sim.now, worst: 0, worstOf: 'fatigue' };
|
|
804
|
+
// Hard internal cap (a week) so a hostile span cannot stall a decision.
|
|
805
|
+
let left = Math.min(7 * MINUTES_PER_DAY, Math.max(0, Number.isFinite(s.duration) ? s.duration : 0));
|
|
806
|
+
advanceBody(sim, 0, s.load, mods, params);
|
|
807
|
+
const r0 = readBody(sim, params, perception).perceived;
|
|
808
|
+
for (const k of ['fatigue', 'sleepiness', 'hunger', 'thirst'])
|
|
809
|
+
if (r0[k] > span.worst) {
|
|
810
|
+
span.worst = r0[k];
|
|
811
|
+
span.worstOf = k;
|
|
812
|
+
}
|
|
813
|
+
while (left > 0 && sim.body.alive) {
|
|
814
|
+
const dt = Math.min(step, left);
|
|
815
|
+
advanceBody(sim, dt, s.load, mods, params);
|
|
816
|
+
sim.now += dt;
|
|
817
|
+
left -= dt;
|
|
818
|
+
const r = readBody(sim, params, perception).perceived;
|
|
819
|
+
for (const k of ['fatigue', 'sleepiness', 'hunger', 'thirst'])
|
|
820
|
+
if (r[k] > span.worst) {
|
|
821
|
+
span.worst = r[k];
|
|
822
|
+
span.worstOf = k;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (!sim.body.alive)
|
|
826
|
+
span.worst = 1;
|
|
827
|
+
span.endsAt = sim.now;
|
|
828
|
+
out.push(span);
|
|
829
|
+
}
|
|
830
|
+
return out;
|
|
831
|
+
}
|
|
@@ -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
|
|
@@ -35,7 +35,7 @@ import { methodEvidence } from '../methods/index.js';
|
|
|
35
35
|
import { urgencyOf } from '../needs/index.js';
|
|
36
36
|
import { partneringTerms } from '../partnering/index.js';
|
|
37
37
|
import { promptIn, promptingTerms } from '../promptings/index.js';
|
|
38
|
-
import { receivedGoalRoute } from '../prospection/index.js';
|
|
38
|
+
import { bodyForecastTerm, receivedGoalRoute } from '../prospection/index.js';
|
|
39
39
|
import { companionSteadiness, socialTerms } from '../social/index.js';
|
|
40
40
|
import { PHYSIOLOGICAL_NEEDS, VALUE_KEYS } from '../types.js';
|
|
41
41
|
import { adviceWeight, resolveChoice, safeUtility, standingAdvice, suggestionTargets, voicesIn, } from '../will/index.js';
|
|
@@ -349,6 +349,12 @@ export function consider(p, aff, ctx) {
|
|
|
349
349
|
const effort = effortValue(p, aff, ctx);
|
|
350
350
|
if (effort < 0)
|
|
351
351
|
push('effort', effort);
|
|
352
|
+
// Bodily forecast (2.5.0): only offers that declare known follow-on exertion.
|
|
353
|
+
if (aff.followedBy?.length) {
|
|
354
|
+
const t = bodyForecastTerm(p, aff, ctx.mods, ctx.perception);
|
|
355
|
+
if (t)
|
|
356
|
+
push(t.source, t.value);
|
|
357
|
+
}
|
|
352
358
|
// Risk: chance × severity × (fear + emotionality).
|
|
353
359
|
if (aff.risk && aff.risk.chance > 0 && aff.risk.severity > 0) {
|
|
354
360
|
const fear = emotionLevel(p, 'fear');
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Human Framework v1 public surface. */
|
|
2
|
-
export declare const FRAMEWORK_VERSION = "2.
|
|
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';
|
|
@@ -30,7 +30,7 @@ export * from './partnering/index.ts';
|
|
|
30
30
|
export * from './person.ts';
|
|
31
31
|
export * from './promptings/index.ts';
|
|
32
32
|
export type { ConsequenceEvidence } from './prospection/index.ts';
|
|
33
|
-
export { consequenceEvidence, PROSPECTION_DEFAULTS } from './prospection/index.ts';
|
|
33
|
+
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, PROSPECTION_DEFAULTS, } from './prospection/index.ts';
|
|
34
34
|
export * from './restore.ts';
|
|
35
35
|
export * from './scenarios/index.ts';
|
|
36
36
|
export * from './sim/index.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Human Framework v1 public surface. */
|
|
2
|
-
export const FRAMEWORK_VERSION = '2.
|
|
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';
|
|
@@ -26,7 +26,7 @@ export * from './needs/index.js';
|
|
|
26
26
|
export * from './partnering/index.js';
|
|
27
27
|
export * from './person.js';
|
|
28
28
|
export * from './promptings/index.js';
|
|
29
|
-
export { consequenceEvidence, PROSPECTION_DEFAULTS } from './prospection/index.js';
|
|
29
|
+
export { bodyForecastFor, bodyForecastTerm, consequenceEvidence, FORECAST_DEFAULTS, forecastLine, PROSPECTION_DEFAULTS, } from './prospection/index.js';
|
|
30
30
|
export * from './restore.js';
|
|
31
31
|
export * from './scenarios/index.js';
|
|
32
32
|
export * from './sim/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. 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,16 @@ 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
|
+
},
|
|
66
|
+
'2.4.0': {
|
|
67
|
+
to: '2.5.0',
|
|
68
|
+
note: 'person shape unchanged; offers may declare followedBy spans for a bodily forecast; offers without it are unchanged',
|
|
69
|
+
apply: stamp,
|
|
70
|
+
},
|
|
61
71
|
'2.3.0': {
|
|
62
72
|
to: '2.4.0',
|
|
63
73
|
note: 'promptings are optional and start absent; existing hosts retain exact behavior',
|
package/dist/person.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type BodyForecastSpan } from './body/index.ts';
|
|
1
2
|
import { type NafsPattern } from './promptings/index.ts';
|
|
2
3
|
import { type SkillTransfer } from './skills/index.ts';
|
|
3
4
|
import type { Activity, Affordance, BodyLoad, BodyReadout, Command, Commitment, DecisionRecord, LearningDomain, Minute, NeedId, NeedReading, NormDefinition, Outcome, Percept, Person, PersonSpec, Retention, Suggestion, SuggestionResolution, Unit } from './types.ts';
|
|
@@ -236,3 +237,11 @@ export declare function setRetention(p: Person, retention: Retention): void;
|
|
|
236
237
|
* 75:2, 89:27), each with the matching evidence in existing state. Descriptions, never a level or a rank.
|
|
237
238
|
*/
|
|
238
239
|
export declare function nafsReading(p: Person): NafsPattern[];
|
|
240
|
+
/**
|
|
241
|
+
* The person's own forecast of their body through an offer and its `followedBy` spans (2.5.0, read-only), with a
|
|
242
|
+
* first-person line ("I can do it, but I'll be spent for the descent."). Empty spans without `followedBy`.
|
|
243
|
+
*/
|
|
244
|
+
export declare function bodyForecast(p: Person, aff: Affordance): {
|
|
245
|
+
spans: BodyForecastSpan[];
|
|
246
|
+
line: string;
|
|
247
|
+
};
|
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';
|
|
@@ -37,7 +37,7 @@ import { intentionFor, narrateDecision, voiceLine } from './narrate/index.js';
|
|
|
37
37
|
import { advanceNeeds, createNeeds, levelOf, meanSatisfaction, readNeeds, satisfy, urgencyOf, } from './needs/index.js';
|
|
38
38
|
import { widow } from './partnering/index.js';
|
|
39
39
|
import { enablePromptings, nafsPatterns, notePromptings } from './promptings/index.js';
|
|
40
|
-
import { consumeConsequences, pruneConsequences, receiveConsequences } from './prospection/index.js';
|
|
40
|
+
import { bodyForecastFor, consumeConsequences, forecastLine, pruneConsequences, receiveConsequences, } from './prospection/index.js';
|
|
41
41
|
import { enableSkillRetention, learnByWatching, practise, seedSkills, } from './skills/index.js';
|
|
42
42
|
import { advanceSocial, applyReputationBelief, careFor, closeness, judge, markDeceased, relationshipWith, seedRelationships, socialEvent, threatAppraisal, } from './social/index.js';
|
|
43
43
|
import { ENGINE_VERSION, MAX_MINUTE, PERSON_SCHEMA, PHYSIOLOGICAL_NEEDS, PSYCHOLOGICAL_NEEDS, TRAIT_KEYS, VALUE_KEYS, } from './types.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.
|
|
@@ -1410,3 +1415,11 @@ export function nafsReading(p) {
|
|
|
1410
1415
|
guilt = Math.max(guilt, e.intensity);
|
|
1411
1416
|
return nafsPatterns(p, { guiltLoad: guiltLoad(p), guilt });
|
|
1412
1417
|
}
|
|
1418
|
+
/**
|
|
1419
|
+
* The person's own forecast of their body through an offer and its `followedBy` spans (2.5.0, read-only), with a
|
|
1420
|
+
* first-person line ("I can do it, but I'll be spent for the descent."). Empty spans without `followedBy`.
|
|
1421
|
+
*/
|
|
1422
|
+
export function bodyForecast(p, aff) {
|
|
1423
|
+
const spans = bodyForecastFor(p, aff, lifeModifiers(p), fastingCtx(p));
|
|
1424
|
+
return { spans, line: forecastLine(spans) };
|
|
1425
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCOPE (bodily forecast, 2.5.0): a person anticipates their own body during exertion they know must follow an
|
|
3
|
+
* act ("I can reach the top, but I'll be spent for the descent"). The host declares the follow-on spans on the
|
|
4
|
+
* offer (`Affordance.followedBy`); the person simulates a copy of their body through the act and those spans with
|
|
5
|
+
* the ordinary body model, reads it as they perceive it, and weighs the worst perceived fatigue, sleepiness,
|
|
6
|
+
* hunger or thirst during the follow-on spans. One term, `forecast:body` = -forecastScale × smoothstep(comfortAt,
|
|
7
|
+
* spentAt, worst), joins the offer's utility; it is pure and draws no randomness. Offers without `followedBy`
|
|
8
|
+
* get no forecast, so existing hosts continue exactly.
|
|
9
|
+
*
|
|
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
|
+
* modifiers are, but not `ambient` parameters). Existing bleeding, illness and healing run in the copy, so an
|
|
16
|
+
* open wound can read as being spent, and death in the forecast reads as fully spent. The term is absolute
|
|
17
|
+
* (see `bodyForecastTerm`). It does not plan, search routes, schedule rests on the way or
|
|
18
|
+
* predict injuries, and it is not a calibrated estimate of human anticipation. Every number is an engineering
|
|
19
|
+
* default.
|
|
20
|
+
*/
|
|
21
|
+
import { type BodyForecastSpan, type BodyPerceptionContext } from '../body/index.ts';
|
|
22
|
+
import type { Affordance, LifeModifiers, Person, Term } from '../types.ts';
|
|
23
|
+
export declare const FORECAST_DEFAULTS: {
|
|
24
|
+
/** Worst perceived readout during a follow-on from which the forecast starts to weigh. */
|
|
25
|
+
readonly comfortAt: 0.8;
|
|
26
|
+
/** Worst perceived readout at which the person expects to be spent (full weight). */
|
|
27
|
+
readonly spentAt: 0.97;
|
|
28
|
+
/** Weight at full expected spending (utility units; the effort term is on the order of 0.1–1). */
|
|
29
|
+
readonly forecastScale: 1.2;
|
|
30
|
+
/** At most this many follow-on spans are read, and at most this many minutes in total. */
|
|
31
|
+
readonly maxSpans: 4;
|
|
32
|
+
readonly maxMinutes: number;
|
|
33
|
+
};
|
|
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, perception?: BodyPerceptionContext): BodyForecastSpan[];
|
|
36
|
+
/**
|
|
37
|
+
* The `forecast:body` term for an offer, or undefined (no follow-on, or not expected to weigh). Absolute by
|
|
38
|
+
* design: it weighs being in that state *while doing* the follow-on, so a person already sleepy or thirsty is
|
|
39
|
+
* charged for exerting so, while an offer with no declared follow-on is not (a night hike with a declared descent
|
|
40
|
+
* costs more than staying home). A resting baseline was tried and rejected: thirst and sleepiness rise at rest
|
|
41
|
+
* too, which cancelled the very case the forecast exists for. Pure.
|
|
42
|
+
*/
|
|
43
|
+
export declare function bodyForecastTerm(p: Person, aff: Affordance, mods: LifeModifiers, perception?: BodyPerceptionContext): Term | undefined;
|
|
44
|
+
/** A first-person line for the forecast (narration only). */
|
|
45
|
+
export declare function forecastLine(spans: readonly BodyForecastSpan[]): string;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCOPE (bodily forecast, 2.5.0): a person anticipates their own body during exertion they know must follow an
|
|
3
|
+
* act ("I can reach the top, but I'll be spent for the descent"). The host declares the follow-on spans on the
|
|
4
|
+
* offer (`Affordance.followedBy`); the person simulates a copy of their body through the act and those spans with
|
|
5
|
+
* the ordinary body model, reads it as they perceive it, and weighs the worst perceived fatigue, sleepiness,
|
|
6
|
+
* hunger or thirst during the follow-on spans. One term, `forecast:body` = -forecastScale × smoothstep(comfortAt,
|
|
7
|
+
* spentAt, worst), joins the offer's utility; it is pure and draws no randomness. Offers without `followedBy`
|
|
8
|
+
* get no forecast, so existing hosts continue exactly.
|
|
9
|
+
*
|
|
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
|
+
* modifiers are, but not `ambient` parameters). Existing bleeding, illness and healing run in the copy, so an
|
|
16
|
+
* open wound can read as being spent, and death in the forecast reads as fully spent. The term is absolute
|
|
17
|
+
* (see `bodyForecastTerm`). It does not plan, search routes, schedule rests on the way or
|
|
18
|
+
* predict injuries, and it is not a calibrated estimate of human anticipation. Every number is an engineering
|
|
19
|
+
* default.
|
|
20
|
+
*/
|
|
21
|
+
import { BODY_DEFAULTS, forecastBody, } from '../body/index.js';
|
|
22
|
+
import { clamp01, round } from '../core/index.js';
|
|
23
|
+
export const FORECAST_DEFAULTS = {
|
|
24
|
+
/** Worst perceived readout during a follow-on from which the forecast starts to weigh. */
|
|
25
|
+
comfortAt: 0.8,
|
|
26
|
+
/** Worst perceived readout at which the person expects to be spent (full weight). */
|
|
27
|
+
spentAt: 0.97,
|
|
28
|
+
/** Weight at full expected spending (utility units; the effort term is on the order of 0.1–1). */
|
|
29
|
+
forecastScale: 1.2,
|
|
30
|
+
/** At most this many follow-on spans are read, and at most this many minutes in total. */
|
|
31
|
+
maxSpans: 4,
|
|
32
|
+
maxMinutes: 24 * 60,
|
|
33
|
+
};
|
|
34
|
+
const smooth = (a, b, x) => {
|
|
35
|
+
const t = clamp01((x - a) / (b - a));
|
|
36
|
+
return t * t * (3 - 2 * t);
|
|
37
|
+
};
|
|
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, perception) {
|
|
40
|
+
const follow = aff.followedBy;
|
|
41
|
+
if (!Array.isArray(follow) || follow.length === 0 || !p.body.alive)
|
|
42
|
+
return [];
|
|
43
|
+
const D = FORECAST_DEFAULTS;
|
|
44
|
+
const d0 = Number.isFinite(aff.duration) ? Math.min(D.maxMinutes, Math.max(0, aff.duration)) : 0;
|
|
45
|
+
const spans = [
|
|
46
|
+
{
|
|
47
|
+
label: aff.label,
|
|
48
|
+
duration: d0,
|
|
49
|
+
load: {
|
|
50
|
+
effort: clamp01(aff.effort),
|
|
51
|
+
focus: clamp01(aff.focus ?? 0),
|
|
52
|
+
mode: aff.mode ?? 'awake',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
let total = spans[0]?.duration ?? 0;
|
|
57
|
+
for (const f of follow.slice(0, D.maxSpans)) {
|
|
58
|
+
if (!f || typeof f !== 'object')
|
|
59
|
+
continue;
|
|
60
|
+
const d = Number.isFinite(f.duration) ? Math.max(0, f.duration) : 0;
|
|
61
|
+
if (total + d > D.maxMinutes)
|
|
62
|
+
break;
|
|
63
|
+
total += d;
|
|
64
|
+
spans.push({
|
|
65
|
+
label: f.label ?? 'after',
|
|
66
|
+
duration: d,
|
|
67
|
+
load: { effort: clamp01(f.effort), focus: clamp01(f.focus ?? 0), mode: 'awake' },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return forecastBody(p, spans, mods, BODY_DEFAULTS, 30, perception);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The `forecast:body` term for an offer, or undefined (no follow-on, or not expected to weigh). Absolute by
|
|
74
|
+
* design: it weighs being in that state *while doing* the follow-on, so a person already sleepy or thirsty is
|
|
75
|
+
* charged for exerting so, while an offer with no declared follow-on is not (a night hike with a declared descent
|
|
76
|
+
* costs more than staying home). A resting baseline was tried and rejected: thirst and sleepiness rise at rest
|
|
77
|
+
* too, which cancelled the very case the forecast exists for. Pure.
|
|
78
|
+
*/
|
|
79
|
+
export function bodyForecastTerm(p, aff, mods, perception) {
|
|
80
|
+
const spans = bodyForecastFor(p, aff, mods, perception).slice(1);
|
|
81
|
+
if (spans.length === 0)
|
|
82
|
+
return undefined;
|
|
83
|
+
const D = FORECAST_DEFAULTS;
|
|
84
|
+
const worst = Math.max(...spans.map((s) => s.worst));
|
|
85
|
+
const v = round(-D.forecastScale * smooth(D.comfortAt, D.spentAt, worst));
|
|
86
|
+
return v < 0 ? { source: 'forecast:body', value: v } : undefined;
|
|
87
|
+
}
|
|
88
|
+
/** A first-person line for the forecast (narration only). */
|
|
89
|
+
export function forecastLine(spans) {
|
|
90
|
+
const after = spans.slice(1);
|
|
91
|
+
if (after.length === 0)
|
|
92
|
+
return '';
|
|
93
|
+
let w = after[0];
|
|
94
|
+
for (const s of after)
|
|
95
|
+
if (s.worst > w.worst)
|
|
96
|
+
w = s;
|
|
97
|
+
const what = { fatigue: 'spent', sleepiness: 'too sleepy', hunger: 'too hungry', thirst: 'too thirsty' }[w.worstOf];
|
|
98
|
+
if (w.worst >= FORECAST_DEFAULTS.spentAt)
|
|
99
|
+
return `I can do it, but I'll be ${what} for ${w.label}.`;
|
|
100
|
+
if (w.worst >= FORECAST_DEFAULTS.comfortAt)
|
|
101
|
+
return `I can do it, though ${w.label} will be hard.`;
|
|
102
|
+
return `I'll still have enough for ${w.label}.`;
|
|
103
|
+
}
|
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.
|
|
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;
|
|
@@ -1092,6 +1097,12 @@ export interface Affordance {
|
|
|
1092
1097
|
severity: Unit;
|
|
1093
1098
|
kind: string;
|
|
1094
1099
|
};
|
|
1100
|
+
/**
|
|
1101
|
+
* Exertion the person knows must follow this act (2.5.0): the way back down, carrying the load home. The
|
|
1102
|
+
* person forecasts their own body through the act and these spans (`prospection/` forecast) and weighs being
|
|
1103
|
+
* spent during them. Absent = no forecast, exactly as before.
|
|
1104
|
+
*/
|
|
1105
|
+
followedBy?: FollowOn[];
|
|
1095
1106
|
/** Set on a partner's mirror offer by the joint protocol in `sim/` (the proposal id). */
|
|
1096
1107
|
jointId?: string;
|
|
1097
1108
|
/** Material gain/cost in host units (e.g. coins). Valued via security/achievement/power values. */
|
|
@@ -1298,7 +1309,8 @@ export interface Term {
|
|
|
1298
1309
|
/** 'need:food', 'norm:salah', 'commitment:c3', 'commitment' (promise inertia of the running activity),
|
|
1299
1310
|
* 'goal:g1', 'habit', 'emotion:fear:risky' (emotion id, then the tag it acts through), 'effort', 'risk',
|
|
1300
1311
|
* 'suggestion:player', 'social:p2', 'expectation', 'autonomy' (reactance to a pushing voice),
|
|
1301
|
-
* 'promptings:turn' (a practised turning-away), '
|
|
1312
|
+
* 'promptings:turn' (a practised turning-away), 'forecast:body' (expected
|
|
1313
|
+
* bodily state during `followedBy` spans), 'promptings:echo' (recurrence under the `fault` reading) */
|
|
1302
1314
|
source: string;
|
|
1303
1315
|
value: number;
|
|
1304
1316
|
}
|
|
@@ -1386,10 +1398,20 @@ export interface PersonSpec {
|
|
|
1386
1398
|
yearbook?: boolean;
|
|
1387
1399
|
character?: boolean;
|
|
1388
1400
|
skillRetention?: boolean;
|
|
1401
|
+
/** Accumulated exertion (2.6.0): the same as `enableFatigueStore(p)` after creation. */
|
|
1402
|
+
fatigueStore?: boolean;
|
|
1389
1403
|
/** Promptings and turning away (2.4.0): the same as `enablePromptings(p, spec)` after creation. */
|
|
1390
1404
|
promptings?: PromptingsSpec;
|
|
1391
1405
|
};
|
|
1392
1406
|
}
|
|
1407
|
+
/** A known span of exertion after an act (see `Affordance.followedBy`). */
|
|
1408
|
+
export interface FollowOn {
|
|
1409
|
+
label?: string;
|
|
1410
|
+
/** Minutes. */
|
|
1411
|
+
duration: number;
|
|
1412
|
+
effort: Unit;
|
|
1413
|
+
focus?: Unit;
|
|
1414
|
+
}
|
|
1393
1415
|
/**
|
|
1394
1416
|
* How a person understands their own unwanted inner pulls (a held reading, set by the host, with provenance).
|
|
1395
1417
|
* `whisper`, `inclination` and `intrusion` share mechanics and differ only in vocabulary; `fault` (the scrupulous
|
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.6.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adam0white/human-framework",
|
|
3
|
-
"version": "2.
|
|
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",
|