diff options
| author | Bobby <[email protected]> | 2026-04-22 07:17:45 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2026-04-22 07:17:45 +0530 |
| commit | 26e0faa0c5d84705b67016d38ccdcca3fa601756 (patch) | |
| tree | 989e2f711e47be991b96f04c26b3d139835e7961 | |
| parent | d7679cff3a0e54e27e4415e5b340b16afeedbf86 (diff) | |
| download | hollowdark-26e0faa0c5d84705b67016d38ccdcca3fa601756.tar.xz hollowdark-26e0faa0c5d84705b67016d38ccdcca3fa601756.zip | |
Define core entity interfaces
38 files changed, 1254 insertions, 0 deletions
diff --git a/engine/career/career.ts b/engine/career/career.ts new file mode 100644 index 0000000..b235f6e --- /dev/null +++ b/engine/career/career.ts @@ -0,0 +1,32 @@ +import type { GameTime } from 'time' +import type { InstitutionId } from '../entities/base' + +export type CareerTrajectory = 'rising' | 'steady' | 'declining' | 'stalled' + +export interface CareerPerformance { + readonly quality: number + readonly reputationAtEmployer: number + readonly yearsSinceLastRecognition: number +} + +export interface CareerHistoryEntry { + readonly careerId: string + readonly employerId: InstitutionId | null + readonly role: string + readonly startedAt: GameTime + readonly endedAt: GameTime | null + readonly reasonEnded: string | null + readonly notableEvents: readonly string[] +} + +export interface CareerState { + readonly currentCareerId: string | null + readonly currentRole: string + readonly currentEmployerId: InstitutionId | null + readonly yearsInCurrentRole: number + readonly yearsInCurrentCareer: number + readonly careerHistory: readonly CareerHistoryEntry[] + readonly performance: CareerPerformance + readonly trajectory: CareerTrajectory + readonly jobSatisfaction: number +} diff --git a/engine/career/index.ts b/engine/career/index.ts new file mode 100644 index 0000000..48daad7 --- /dev/null +++ b/engine/career/index.ts @@ -0,0 +1,6 @@ +export type { + CareerHistoryEntry, + CareerPerformance, + CareerState, + CareerTrajectory +} from './career' diff --git a/engine/economics/affordability.ts b/engine/economics/affordability.ts new file mode 100644 index 0000000..ef57d3f --- /dev/null +++ b/engine/economics/affordability.ts @@ -0,0 +1,22 @@ +/** + * The "What you could afford" surface on the Money screen. Hidden numbers + * resolve into qualitative feasibility labels so the player answers + * "can I afford this?" without ever seeing a number (docs/09-economy.md + * §"The 'What you could afford' design"). + */ +export type Feasibility = + | 'comfortable' + | 'with_some_sacrifice' + | 'only_by_stretching' + | 'not_without_changing_careers' + | 'not_realistically' + +export interface PossiblePurchase { + readonly description: string + readonly feasibility: Feasibility + readonly contextual: boolean +} + +export interface AffordabilityContext { + readonly possiblePurchases: readonly PossiblePurchase[] +} diff --git a/engine/economics/economic.ts b/engine/economics/economic.ts new file mode 100644 index 0000000..1197cde --- /dev/null +++ b/engine/economics/economic.ts @@ -0,0 +1,56 @@ +/** + * Economic state. All numbers in "marks" (the world's universal currency). + * The player never sees any of these — they surface as prose and qualitative + * affordability context (docs/09-economy.md, ARCHITECTURE.md §14). + */ + +export type EconomicClass = + | 'destitute' + | 'struggling' + | 'working' + | 'lower_middle' + | 'middle' + | 'upper_middle' + | 'wealthy' + | 'very_wealthy' + | 'old_money' + | 'generationally_wealthy' + +export type AccountKind = 'checking' | 'savings' | 'brokerage' | 'retirement' + +export interface Account { + readonly id: string + readonly kind: AccountKind + readonly balance: number +} + +export type AssetKind = 'home' | 'vehicle' | 'business' | 'collectible' | 'real_estate' + +export interface Asset { + readonly id: string + readonly kind: AssetKind + readonly description: string + readonly estimatedValue: number + readonly encumbered: boolean +} + +export type DebtKind = 'mortgage' | 'auto' | 'student' | 'credit_card' | 'personal' | 'medical' + +export interface Debt { + readonly id: string + readonly kind: DebtKind + readonly balance: number + readonly monthlyPayment: number + readonly apr: number +} + +export interface EconomicState { + readonly cashOnHand: number + readonly accounts: readonly Account[] + readonly assets: readonly Asset[] + readonly debts: readonly Debt[] + readonly monthlyIncome: number + readonly monthlyExpenses: number + readonly netWorth: number + readonly classification: EconomicClass +} diff --git a/engine/economics/index.ts b/engine/economics/index.ts new file mode 100644 index 0000000..511c42b --- /dev/null +++ b/engine/economics/index.ts @@ -0,0 +1,12 @@ +export type { + Account, + AccountKind, + Asset, + AssetKind, + Debt, + DebtKind, + EconomicClass, + EconomicState +} from './economic' + +export type { AffordabilityContext, Feasibility, PossiblePurchase } from './affordability' diff --git a/engine/entities/base.ts b/engine/entities/base.ts new file mode 100644 index 0000000..cc7c9ed --- /dev/null +++ b/engine/entities/base.ts @@ -0,0 +1,37 @@ +import type { GameTime } from 'time' +import type { Brand } from 'utils' + +/** + * Branded IDs — string at runtime, distinct at compile time so a PersonId + * can't silently flow into a slot that expects a RelationshipId. + * (utils/types/brand.ts) + */ +export type PersonId = Brand<string, 'PersonId'> +export type RelationshipId = Brand<string, 'RelationshipId'> +export type PlaceId = Brand<string, 'PlaceId'> +export type InstitutionId = Brand<string, 'InstitutionId'> +export type WorldEventId = Brand<string, 'WorldEventId'> +export type WorldId = Brand<string, 'WorldId'> +export type EventLogEntryId = Brand<string, 'EventLogEntryId'> +export type RoutineId = Brand<string, 'RoutineId'> +export type ScheduledEventId = Brand<string, 'ScheduledEventId'> +export type FlowEntryId = Brand<string, 'FlowEntryId'> +export type MemoirId = Brand<string, 'MemoirId'> + +/** Every simulation entity wears one of these kind tags. */ +export type EntityKind = 'person' | 'relationship' | 'institution' | 'place' | 'world_event' + +/** + * The common shape every entity carries. Concrete entities extend this with + * their specific id/kind pair — `Person extends BaseEntity<PersonId, 'person'>`. + * `deterministicSeed` powers lazy Tier 3 regeneration: given the seed, the + * entity's trajectory is fully reproducible without persisted state. + * (ARCHITECTURE.md §26) + */ +export interface BaseEntity<Id extends string = string, K extends EntityKind = EntityKind> { + readonly id: Id + readonly kind: K + readonly createdAt: GameTime + readonly destroyedAt: GameTime | null + readonly deterministicSeed: number +} diff --git a/engine/entities/event-log-entry.ts b/engine/entities/event-log-entry.ts new file mode 100644 index 0000000..ba0b6e2 --- /dev/null +++ b/engine/entities/event-log-entry.ts @@ -0,0 +1,33 @@ +import type { GameTime } from 'time' +import type { EventLogEntryId, PersonId } from './base' + +/** + * Consequences logged against a specific event resolution. The shape is + * kept deliberately open — events may mutate a wide range of state — and + * the consequence writer (events/consequence.ts, later) fills it in. + */ +export interface AppliedConsequence { + readonly targetPath: string + readonly operation: 'set' | 'add' | 'multiply' | 'append' + readonly value: unknown +} + +/** + * One event's entry in a character's event log. Entry IDs are stable so + * memoir generation and History views can reference specific moments. + * Passage text lives in the content registry, keyed by renderedPassageRef, + * so logs stay small. + */ +export interface EventLogEntry { + readonly id: EventLogEntryId + readonly personId: PersonId + readonly time: GameTime + readonly eventTypeId: string + readonly participants: readonly PersonId[] + readonly emotionalWeight: number + readonly themes: readonly string[] + readonly consequences: readonly AppliedConsequence[] + readonly renderedPassageRef: string + readonly playerChoice: string | null + readonly memorableMarker: boolean +} diff --git a/engine/entities/flow-entry.ts b/engine/entities/flow-entry.ts new file mode 100644 index 0000000..ff2e624 --- /dev/null +++ b/engine/entities/flow-entry.ts @@ -0,0 +1,25 @@ +import type { GameTime } from 'time' +import type { FlowEntryId, PersonId, PlaceId, WorldEventId } from './base' + +/** + * A compact snapshot of the context that produced a flow passage. Kept + * alongside the entry so passages can be regenerated deterministically + * if content or voice-modulation changes — no reliance on re-simulating + * the full world to reproduce a specific week's prose. + */ +export interface FlowContextSnapshot { + readonly season: string + readonly placeId: PlaceId + readonly activeWorldEventIds: readonly WorldEventId[] + readonly moodBucket: string + readonly dominantTraitMarkers: readonly string[] +} + +export interface FlowEntry { + readonly id: FlowEntryId + readonly personId: PersonId + readonly time: GameTime + readonly passageText: string + readonly passageRef: string + readonly contextSnapshot: FlowContextSnapshot +} diff --git a/engine/entities/index.ts b/engine/entities/index.ts new file mode 100644 index 0000000..9b520ac --- /dev/null +++ b/engine/entities/index.ts @@ -0,0 +1,67 @@ +export type { + BaseEntity, + EntityKind, + EventLogEntryId, + FlowEntryId, + InstitutionId, + MemoirId, + PersonId, + PlaceId, + RelationshipId, + RoutineId, + ScheduledEventId, + WorldEventId, + WorldId +} from './base' + +export type { AppliedConsequence, EventLogEntry } from './event-log-entry' +export type { FlowContextSnapshot, FlowEntry } from './flow-entry' +export type { Institution, InstitutionEvent, InstitutionType } from './institution' +export type { Memoir, MemoirChapter } from './memoir' +export type { + BirthRecord, + DeathMode, + DeathRecord, + Person, + SimulationTier +} from './person' +export type { PersonName } from './person-name' +export type { + ClimateDescriptor, + CultureDescriptor, + EconomicCharacter, + Place, + PlaceType, + PoliticalCharacter +} from './place' +export type { + ConflictEvent, + FamilyRelation, + InfidelityEvent, + IntimacyAxes, + LoveLanguageMatrix, + LoveLanguageProfile, + Relationship, + RelationshipPerception, + RelationshipState, + RelationType, + RomanticPhase, + SexualActivityState, + SharedExperience, + Tension, + TrustEvent, + WorkRelation +} from './relationship' +export type { ReputationProfile } from './reputation' +export type { ResidenceEntry } from './residence' +export type { Routine, RoutineCategory, RoutineEffect, RoutineItem } from './routine' +export type { ConditionalTrigger, ScheduledEvent } from './scheduled-event' +export type { SocioeconomicTier, StatusDescriptor } from './status' +export type { + CrisisState, + MacroEconomicState, + RegionPoliticalState, + World, + WorldSettings +} from './world' +export type { EventCategory, SeverityLevel, WorldEvent } from './world-event' diff --git a/engine/entities/institution.ts b/engine/entities/institution.ts new file mode 100644 index 0000000..2bb582d --- /dev/null +++ b/engine/entities/institution.ts @@ -0,0 +1,40 @@ +import type { GameTime } from 'time' +import type { BaseEntity, InstitutionId, PersonId, PlaceId } from './base' +import type { CultureDescriptor } from './place' + +export type InstitutionType = + | 'company' + | 'government' + | 'university' + | 'school' + | 'hospital' + | 'religious' + | 'nonprofit' + | 'news' + | 'cultural' + | 'criminal' + | 'military' + +export interface InstitutionEvent { + readonly at: GameTime + readonly kind: string + readonly summary: string +} + +export interface Institution extends BaseEntity<InstitutionId, 'institution'> { + readonly name: string + readonly type: InstitutionType + readonly placeId: PlaceId + readonly foundedAt: GameTime + + readonly culture: CultureDescriptor + + /** Role tag (e.g., "manager", "ceo", "teacher") → person IDs holding it. */ + readonly members: ReadonlyMap<string, readonly PersonId[]> + + readonly prosperity: number + readonly reputation: number + readonly stability: number + + readonly majorEvents: readonly InstitutionEvent[] +} diff --git a/engine/entities/memoir.ts b/engine/entities/memoir.ts new file mode 100644 index 0000000..c6dcf57 --- /dev/null +++ b/engine/entities/memoir.ts @@ -0,0 +1,28 @@ +import type { GameTime } from 'time' +import type { EventLogEntryId, MemoirId, PersonId } from './base' + +export interface MemoirChapter { + readonly order: number + readonly title: string + readonly prose: string + readonly startAge: number + readonly endAge: number + readonly keyEventIds: readonly EventLogEntryId[] + readonly themeEmphasis: readonly string[] +} + +/** + * Generated at character death. 15,000–30,000 words, 8–15 chapters. Persists + * forever in the world's archive; descendants may find it on the Memoirs + * shelf or referenced in flow (docs/14-memoirs.md, ARCHITECTURE.md §17). + */ +export interface Memoir { + readonly id: MemoirId + readonly personId: PersonId + readonly generatedAt: GameTime + readonly chapters: readonly MemoirChapter[] + readonly totalWordCount: number + readonly themes: readonly string[] + readonly publicVersion: boolean + readonly publishedAt: GameTime | null +} diff --git a/engine/entities/person-name.ts b/engine/entities/person-name.ts new file mode 100644 index 0000000..5f84b41 --- /dev/null +++ b/engine/entities/person-name.ts @@ -0,0 +1,14 @@ +/** + * A character's name as tracked by the simulation. Surnames follow + * inheritance rules per region/era (docs/19-names.md) — including keep- + * maiden, hyphenate, matrilineal, and ad-hoc changes — so the structure + * preserves both the current surname and the maiden form when relevant. + */ +export interface PersonName { + readonly given: string + readonly middle: string | null + readonly surname: string + readonly maidenSurname: string | null + readonly nicknames: readonly string[] + readonly preferredName: string | null +} diff --git a/engine/entities/person.ts b/engine/entities/person.ts new file mode 100644 index 0000000..db86c47 --- /dev/null +++ b/engine/entities/person.ts @@ -0,0 +1,138 @@ +import type { GameTime } from 'time' +import type { CareerState } from '../career' +import type { EconomicState } from '../economics' +import type { HealthState, MentalHealthState, Condition } from '../health' +import type { Dependency, MoodState, SatisfactionProfile, Scar } from '../state' +import type { + AttachmentDistribution, + BigFiveProfile, + CoreBeliefs, + DarkTriadProfile, + SexualOrientation, + ValuesOrientation +} from '../traits' +import type { BaseEntity, PersonId, PlaceId, RelationshipId } from './base' +import type { EventLogEntry } from './event-log-entry' +import type { PersonName } from './person-name' +import type { ReputationProfile } from './reputation' +import type { ResidenceEntry } from './residence' +import type { StatusDescriptor } from './status' + +/** Modes of death per docs/20-death-textures.md. */ +export type DeathMode = + | 'expected_old_age' + | 'sudden_accident' + | 'terminal_illness' + | 'suicide' + | 'violent_death' + | 'during_sleep' + | 'childbirth' + | 'war' + | 'child_or_infant' + +/** + * The defining demographic facts of a birth, captured in prose-ready form. + * The familyContext string is the one-paragraph summary the opening scene + * draws on (docs/17-first-hour.md §"Birth moment"). + */ +export interface BirthRecord { + readonly date: GameTime + readonly placeId: PlaceId + readonly familyContext: string +} + +export interface DeathRecord { + readonly date: GameTime + readonly mode: DeathMode + readonly placeId: PlaceId + readonly sceneSummary: string +} + +/** + * NPC simulation fidelity tier. 1 = full weekly simulation (~10-30 entities), + * 2 = quarterly compressed, 3 = generated on demand from seed. + * See docs/06-autonomy.md §"Tiered simulation fidelity". + */ +export type SimulationTier = 1 | 2 | 3 + +/** + * Person — the main simulated entity. Players and NPCs share this shape. + * Fields are grouped by trait layer (docs/04-traits.md): + * + * Layer 1 temperament (Big Five + Dark Triad) — mostly stable + * Layer 2 developmental (attachment, core beliefs, values, orientation) + * Layer 3 state (mood, stress, energy, trauma load, satisfaction) + * Layer 4 acquired (skills, knowledge, habits, dependencies, scars) + * Layer 5 social (reputation, status, network capital) + * + * Numeric fields stay hidden from the player; everything surfaces as prose. + */ +export interface Person extends BaseEntity<PersonId, 'person'> { + readonly name: PersonName + + readonly birth: BirthRecord + readonly death: DeathRecord | null + + // Layer 1 — temperament + readonly bigFive: BigFiveProfile + readonly darkTriad: DarkTriadProfile + + // Layer 2 — developmental + readonly attachment: AttachmentDistribution + readonly coreBeliefs: CoreBeliefs + readonly conscienceCapacity: number + readonly values: ValuesOrientation + readonly orientation: SexualOrientation + + // Layer 3 — fluctuating state + readonly mood: MoodState + readonly stress: number + readonly energy: number + readonly traumaLoad: number + readonly satisfaction: SatisfactionProfile + + // Layer 4 — acquired + readonly skills: ReadonlyMap<string, number> + readonly knowledge: ReadonlyMap<string, number> + readonly habits: readonly string[] + readonly dependencies: readonly Dependency[] + readonly scars: readonly Scar[] + + // Layer 5 — social + readonly reputation: ReputationProfile + readonly status: StatusDescriptor + readonly networkCapital: number + + // Health + readonly health: HealthState + readonly chronicConditions: readonly Condition[] + readonly mentalHealthState: MentalHealthState + + // Economic / career + readonly economic: EconomicState + readonly career: CareerState + + // Relationships + location + readonly relationshipIds: readonly RelationshipId[] + readonly currentPlaceId: PlaceId + readonly residenceHistory: readonly ResidenceEntry[] + + // Simulation metadata + readonly tier: SimulationTier + readonly lastSimulatedAt: GameTime + + // Event history + readonly eventLog: readonly EventLogEntry[] + readonly memorableMoments: readonly string[] + + // Player-character flags + readonly isPlayerCharacter: boolean + readonly playerCharacterStartedAt: GameTime | null + readonly playerCharacterEndedAt: GameTime | null + + // Family (denormalised for fast access; the authoritative source is the + // Relationship entities keyed family_parent / family_child / family_spouse) + readonly parentIds: readonly [PersonId | null, PersonId | null] + readonly childIds: readonly PersonId[] + readonly spouseIds: readonly PersonId[] +} diff --git a/engine/entities/place.ts b/engine/entities/place.ts new file mode 100644 index 0000000..2a38c41 --- /dev/null +++ b/engine/entities/place.ts @@ -0,0 +1,55 @@ +import type { BaseEntity, PersonId, PlaceId } from './base' + +export type PlaceType = 'region' | 'city' | 'neighborhood' | 'specific_location' + +/** + * Cultural character of a region or city — drives event textures, + * stereotypes, attitudes. Open-ended because cultures differ on many axes; + * specific named attributes live alongside this via content references. + */ +export interface CultureDescriptor { + readonly dominantFaithId: string | null + readonly cosmopolitanism: number + readonly conservatism: number + readonly hospitality: number + readonly tags: readonly string[] +} + +export interface ClimateDescriptor { + readonly latitudeBand: 'tropical' | 'temperate' | 'continental' | 'arid' | 'alpine' | 'tundra' + readonly summerSeverity: number + readonly winterSeverity: number + readonly rainfall: number + readonly notes: string +} + +export interface EconomicCharacter { + readonly primaryIndustries: readonly string[] + readonly inequality: number + readonly prosperity: number +} + +export interface PoliticalCharacter { + readonly form: 'democracy' | 'mixed' | 'authoritarian' | 'oligarchy' | 'other' + readonly stability: number + readonly currentTensions: readonly string[] +} + +export interface Place extends BaseEntity<PlaceId, 'place'> { + readonly name: string + readonly type: PlaceType + readonly parentPlaceId: PlaceId | null + + // Present for regions and cities; null for specific locations. + readonly culture: CultureDescriptor | null + readonly climate: ClimateDescriptor | null + readonly economy: EconomicCharacter | null + readonly politics: PoliticalCharacter | null + + readonly population: number + + // Present for specific locations (houses, workplaces); null for containers. + readonly ownerId: PersonId | null + readonly currentResidents: readonly PersonId[] + readonly propertyValue: number | null +} diff --git a/engine/entities/relationship.ts b/engine/entities/relationship.ts new file mode 100644 index 0000000..c6a771c --- /dev/null +++ b/engine/entities/relationship.ts @@ -0,0 +1,176 @@ +import type { GameTime } from 'time' +import type { BaseEntity, PersonId, RelationshipId } from './base' + +export type RelationType = + | 'family_parent' + | 'family_child' + | 'family_sibling' + | 'family_cousin' + | 'family_grandparent' + | 'family_grandchild' + | 'family_inlaw' + | 'romantic_dating' + | 'romantic_married' + | 'romantic_divorced' + | 'romantic_ex' + | 'romantic_affair' + | 'friend_close' + | 'friend_casual' + | 'friend_drifted' + | 'professional_colleague' + | 'professional_boss' + | 'professional_report' + | 'professional_client' + | 'professional_mentor' + | 'professional_mentee' + | 'acquaintance' + | 'stranger_known' + | 'enemy' + +export type RelationshipState = 'warm' | 'neutral' | 'strained' | 'ruptured' | 'dormant' + +/** + * The four axes of intimacy (docs/07-relationships.md §"Relationship state + * vector"). Each 0–1. Tracked separately because a marriage can be high + * on practical and low on emotional — that's a specific life texture, not + * a compatibility score. + */ +export interface IntimacyAxes { + readonly emotional: number + readonly intellectual: number + readonly physical: number + readonly practical: number +} + +export interface TrustEvent { + readonly at: GameTime + readonly delta: number + readonly eventRef: string +} + +export interface ConflictEvent { + readonly at: GameTime + readonly topic: string + readonly intensity: number + readonly resolved: boolean + readonly residue: number +} + +export interface SharedExperience { + readonly at: GameTime + readonly kind: string + readonly emotionalWeight: number + readonly summary: string +} + +export interface Tension { + readonly id: string + readonly topic: string + readonly since: GameTime + readonly intensity: number +} + +/** The five love languages, matched per side — what each person gives vs. + * what they need from the other. Misalignment is a real relationship shape. */ +export interface LoveLanguageProfile { + readonly wordsOfAffirmation: number + readonly actsOfService: number + readonly receivingGifts: number + readonly qualityTime: number + readonly physicalTouch: number +} + +export interface LoveLanguageMatrix { + readonly aGives: LoveLanguageProfile + readonly aNeeds: LoveLanguageProfile + readonly bGives: LoveLanguageProfile + readonly bNeeds: LoveLanguageProfile +} + +/** + * Asymmetric perception: A and B each have their own mental model of the + * relationship. Large asymmetries fire scenes (confession, rebuff, shocked + * realisation). See docs/07-relationships.md §"Asymmetric perception". + */ +export interface RelationshipPerception { + readonly perceivedType: RelationType + readonly perceivedIntimacy: IntimacyAxes + readonly perceivedTrust: number + readonly lastUpdated: GameTime +} + +export type RomanticPhase = + | 'attraction' + | 'infatuation' + | 'deepening' + | 'committed' + | 'power_struggle' + | 'stability' + | 'mature_love' + | 'decline' + | 'rupture' + +export interface SexualActivityState { + readonly frequencyPerMonth: number + readonly mutualSatisfaction: number + readonly openness: number +} + +export interface InfidelityEvent { + readonly at: GameTime + readonly withPersonId: PersonId | null + readonly kind: 'emotional' | 'physical' | 'both' + readonly discoveredAt: GameTime | null +} + +export type FamilyRelation = + | 'parent' + | 'child' + | 'sibling' + | 'cousin' + | 'grandparent' + | 'grandchild' + | 'inlaw' + +export type WorkRelation = 'colleague' | 'boss' | 'report' | 'client' | 'mentor' | 'mentee' + +export interface Relationship extends BaseEntity<RelationshipId, 'relationship'> { + readonly personAId: PersonId + readonly personBId: PersonId + + readonly type: RelationType + readonly typeHistory: readonly { readonly type: RelationType; readonly from: GameTime }[] + + readonly startedAt: GameTime + + readonly intimacy: IntimacyAxes + readonly trust: number + readonly trustHistory: readonly TrustEvent[] + readonly powerBalance: number + + readonly conflicts: readonly ConflictEvent[] + readonly sharedExperiences: readonly SharedExperience[] + readonly unresolvedTensions: readonly Tension[] + + readonly loveLanguages: LoveLanguageMatrix + + readonly currentState: RelationshipState + + readonly aPerception: RelationshipPerception + readonly bPerception: RelationshipPerception + + readonly lastInteractionAt: GameTime + readonly interactionFrequency: number + + // Romantic-only details; null when the relationship isn't romantic. + readonly romanticPhase: RomanticPhase | null + readonly sexualActivity: SexualActivityState | null + readonly infidelityHistory: readonly InfidelityEvent[] + readonly commitmentLevel: number + + // Family-only. + readonly familyRelationType: FamilyRelation | null + + // Professional-only. + readonly workRelationType: WorkRelation | null +} diff --git a/engine/entities/reputation.ts b/engine/entities/reputation.ts new file mode 100644 index 0000000..07b747b --- /dev/null +++ b/engine/entities/reputation.ts @@ -0,0 +1,11 @@ +/** + * Reputation at three scopes. Local = neighbourhood / small community. + * Professional = workplace and industry. Broader = public / civic; + * usually 0 for most characters and non-zero for notable figures. + * Each -1 (disrepute) to 1 (esteem). + */ +export interface ReputationProfile { + readonly local: number + readonly professional: number + readonly broader: number +} diff --git a/engine/entities/residence.ts b/engine/entities/residence.ts new file mode 100644 index 0000000..6a404f7 --- /dev/null +++ b/engine/entities/residence.ts @@ -0,0 +1,10 @@ +import type { GameTime } from 'time' +import type { PlaceId } from './base' + +/** A span of time a character lived at a specific place. Open-ended if the + * character still lives there. */ +export interface ResidenceEntry { + readonly placeId: PlaceId + readonly from: GameTime + readonly to: GameTime | null +} diff --git a/engine/entities/routine.ts b/engine/entities/routine.ts new file mode 100644 index 0000000..adb2b1d --- /dev/null +++ b/engine/entities/routine.ts @@ -0,0 +1,37 @@ +import type { GameTime } from 'time' +import type { PersonId, RoutineId } from './base' + +export type RoutineCategory = 'work' | 'relationships' | 'self' | 'home' | 'play' | 'service' + +/** + * How a routine item modifies state each week. `targetSkill` and + * `targetStatVariable` are mutually exclusive per effect — one effect + * either grows a skill or nudges a state variable, not both. + */ +export interface RoutineEffect { + readonly targetSkill: string | null + readonly targetStatVariable: string | null + readonly delta: number +} + +export interface RoutineItem { + readonly id: string + readonly category: RoutineCategory + readonly description: string + readonly effects: readonly RoutineEffect[] + readonly hoursPerWeek: number + readonly startedAt: GameTime + readonly endedAt: GameTime | null +} + +/** + * Persistent weekly commitments for a character. Routines run silently in + * the flow stream — the player sets them once, they keep running until + * changed (docs/05-time-system.md §"Routines and flow"). + */ +export interface Routine { + readonly id: RoutineId + readonly personId: PersonId + readonly items: readonly RoutineItem[] + readonly lastModifiedAt: GameTime +} diff --git a/engine/entities/scheduled-event.ts b/engine/entities/scheduled-event.ts new file mode 100644 index 0000000..5ea9609 --- /dev/null +++ b/engine/entities/scheduled-event.ts @@ -0,0 +1,24 @@ +import type { GameTime } from 'time' +import type { PersonId, ScheduledEventId } from './base' + +/** + * A future event trigger, either at a fixed time or when conditions hold. + * Taking a bribe at 30 might schedule a `bribery_audit` at +15 years with + * elevated weight (ARCHITECTURE.md §6 §"Scheduled events"). + */ +export interface ConditionalTrigger { + readonly kind: 'conditional' + readonly condition: string + readonly earliestTime: GameTime | null + readonly latestTime: GameTime | null +} + +export interface ScheduledEvent { + readonly id: ScheduledEventId + readonly eventTypeId: string + readonly scheduledFor: GameTime | ConditionalTrigger + readonly targetPersonId: PersonId + readonly context: Readonly<Record<string, unknown>> + readonly eligibilityRecheck: boolean + readonly priority: number +} diff --git a/engine/entities/status.ts b/engine/entities/status.ts new file mode 100644 index 0000000..5b1b823 --- /dev/null +++ b/engine/entities/status.ts @@ -0,0 +1,18 @@ +/** + * Status descriptor surfaces derived labels for display prose. Socioeconomic + * is drawn from the character's EconomicClass; professional and social come + * out of career + reputation. Never shown as numbers to the player. + */ +export type SocioeconomicTier = + | 'struggling' + | 'working' + | 'middle' + | 'upper_middle' + | 'wealthy' + | 'old_money' + +export interface StatusDescriptor { + readonly socioeconomic: SocioeconomicTier + readonly professional: string + readonly social: string +} diff --git a/engine/entities/world-event.ts b/engine/entities/world-event.ts new file mode 100644 index 0000000..e414bf4 --- /dev/null +++ b/engine/entities/world-event.ts @@ -0,0 +1,39 @@ +import type { GameTime } from 'time' +import type { BaseEntity, PersonId, PlaceId, WorldEventId } from './base' + +export type EventCategory = + | 'pandemic' + | 'war' + | 'economic' + | 'political' + | 'natural_disaster' + | 'cultural' + | 'scientific' + | 'religious' + | 'social_movement' + | 'crime' + | 'notable_individual' + +export type SeverityLevel = 'mild' | 'moderate' | 'severe' | 'catastrophic' + +/** + * A named macro event decorating the timeline. See docs/02-world-events.md. + * The character-level impact of each event is applied per-person via + * templates referenced by contentRef; this entity is the world-scale record. + */ +export interface WorldEvent extends BaseEntity<WorldEventId, 'world_event'> { + readonly contentRef: string + readonly nameFormal: string + readonly nameColloquial: string + readonly category: EventCategory + + readonly startedAt: GameTime + readonly endedAt: GameTime | null + + readonly affectedRegions: ReadonlyMap<PlaceId, SeverityLevel> + + readonly description: string + + /** IDs of people who have had this event's per-person impact applied. */ + readonly impactsApplied: readonly PersonId[] +} diff --git a/engine/entities/world.ts b/engine/entities/world.ts new file mode 100644 index 0000000..539a3a7 --- /dev/null +++ b/engine/entities/world.ts @@ -0,0 +1,62 @@ +import type { GameTime } from 'time' +import type { PersonId, PlaceId, WorldEventId, WorldId } from './base' +import type { ScheduledEvent } from './scheduled-event' + +/** + * Macro economic state tracked at the world scale. Individual characters' + * economics are derived against this background (docs/09-economy.md + * §"Macro economy"). + */ +export interface MacroEconomicState { + readonly inflationAnnual: number + readonly employmentRate: number + readonly marketIndex: number + readonly recessionDepth: number +} + +export interface RegionPoliticalState { + readonly stability: number + readonly currentRegime: string + readonly tensions: readonly string[] +} + +export interface WorldSettings { + readonly contentVersionAtCreation: string + readonly schemaVersion: number +} + +export interface CrisisState { + readonly active: boolean + readonly crisisEventId: string | null + readonly sceneIndex: number + readonly startedAt: GameTime | null +} + +/** + * The world container. One continuous world per player (ARCHITECTURE.md §16, + * docs/16-world-continuity.md). Time in this world never resets once + * created; successive player characters advance it forward. + */ +export interface World { + readonly id: WorldId + readonly seed: string + readonly createdAt: string // ISO timestamp, real-world clock — not a GameTime + + readonly currentGameTime: GameTime + + readonly currentPlayerCharacterId: PersonId | null + readonly playedCharacterIds: readonly PersonId[] + + readonly tierOneIds: readonly PersonId[] + readonly tierTwoIds: readonly PersonId[] + + readonly economy: MacroEconomicState + readonly politicsByRegion: ReadonlyMap<PlaceId, RegionPoliticalState> + + readonly activeEventIds: readonly WorldEventId[] + readonly scheduledEvents: readonly ScheduledEvent[] + + readonly crisisState: CrisisState + + readonly settings: WorldSettings +} diff --git a/engine/health/health.ts b/engine/health/health.ts new file mode 100644 index 0000000..b623710 --- /dev/null +++ b/engine/health/health.ts @@ -0,0 +1,77 @@ +import type { GameTime } from 'time' + +export type EatingPattern = 'poor' | 'irregular' | 'moderate' | 'good' | 'athletic' + +/** Alcohol, smoking, drugs, activity level — the background variables that + * compound into long-term health outcomes. */ +export interface LifestyleProfile { + readonly alcohol: number + readonly smoking: number + readonly drugs: number + readonly activity: number + readonly diet: number + readonly compositeRisk: number +} + +export interface SexualHealthState { + readonly activeInfections: readonly string[] + readonly pastInfections: readonly string[] + readonly contraceptionInUse: string | null + readonly fertility: number +} + +/** A symptom visible to the character (or to those around them). */ +export interface Symptom { + readonly id: string + readonly kind: string + readonly severity: number + readonly onsetAt: GameTime + readonly isChronic: boolean +} + +/** A condition the character knows about and has a name for. */ +export interface Condition { + readonly id: string + readonly kind: string + readonly diagnosedAt: GameTime | null + readonly severity: number + readonly isTerminal: boolean + readonly mortalityMultiplier: number + readonly managementInPlace: boolean +} + +/** + * A condition the simulation tracks but the character doesn't yet know about. + * Surface paths include worsening symptoms, doctor visits, routine screens, + * or incidental discovery (docs/08-mental-health.md §"Suicide risk" for the + * comparable mental-health pattern; this covers physical equivalents). + */ +export interface UndiagnosedCondition { + readonly id: string + readonly kind: string + readonly onsetAt: GameTime + readonly visibleSymptomIds: readonly string[] + readonly severity: number + readonly discoveryTriggers: readonly string[] +} + +/** A past medical event kept in the character's file. */ +export interface MedicalEvent { + readonly id: string + readonly kind: string + readonly occurredAt: GameTime + readonly summary: string +} + +export interface HealthState { + readonly overallQuality: number + readonly fitness: number + readonly sleepQuality: number + readonly eatingPattern: EatingPattern + readonly energyBaseline: number + readonly lifestyle: LifestyleProfile + readonly sexualHealth: SexualHealthState + readonly currentSymptoms: readonly Symptom[] + readonly undiagnosed: readonly UndiagnosedCondition[] + readonly medicalHistory: readonly MedicalEvent[] +} diff --git a/engine/health/index.ts b/engine/health/index.ts new file mode 100644 index 0000000..0577d7a --- /dev/null +++ b/engine/health/index.ts @@ -0,0 +1,20 @@ +export type { + Condition, + EatingPattern, + HealthState, + LifestyleProfile, + MedicalEvent, + SexualHealthState, + Symptom, + UndiagnosedCondition +} from './health' + +export type { + CopingStrategy, + HistoricalCondition, + Medication, + MentalCondition, + MentalHealthState, + SuicidalRisk, + TraumaPattern +} from './mental-health' diff --git a/engine/health/mental-health.ts b/engine/health/mental-health.ts new file mode 100644 index 0000000..2f86947 --- /dev/null +++ b/engine/health/mental-health.ts @@ -0,0 +1,67 @@ +import type { GameTime } from 'time' + +/** Currently-active mental-health condition. Specific disorder vocabulary + * lives in content; the runtime state is shape + kind tag. */ +export interface MentalCondition { + readonly id: string + readonly kind: string + readonly onsetAt: GameTime + readonly severity: number + readonly inTreatment: boolean +} + +/** A condition the character has had and no longer meets active criteria for, + * but which still informs relapse probability and event eligibility. */ +export interface HistoricalCondition { + readonly id: string + readonly kind: string + readonly startedAt: GameTime + readonly endedAt: GameTime + readonly peakSeverity: number + readonly inRemission: boolean +} + +export interface TraumaPattern { + readonly id: string + readonly source: string + readonly firstOnsetAt: GameTime + readonly hypervigilance: number + readonly avoidance: number + readonly intrusions: number +} + +export interface CopingStrategy { + readonly id: string + readonly kind: string + readonly adaptive: boolean + readonly reliance: number +} + +export interface Medication { + readonly id: string + readonly name: string + readonly startedAt: GameTime + readonly endedAt: GameTime | null + readonly dosage: number + readonly effectiveness: number +} + +/** + * Hidden even from the character until crisis. The simulation knows; the + * prose surfaces behaviour, not numbers (docs/08-mental-health.md §"Suicide"). + */ +export interface SuicidalRisk { + readonly current: number + readonly history: readonly { readonly at: GameTime; readonly value: number }[] + readonly lastAssessedAt: GameTime +} + +export interface MentalHealthState { + readonly activeConditions: readonly MentalCondition[] + readonly historyOfConditions: readonly HistoricalCondition[] + readonly traumaPatterns: readonly TraumaPattern[] + readonly copingStrategies: readonly CopingStrategy[] + readonly inTherapy: boolean + readonly medication: readonly Medication[] + readonly suicidalRisk: SuicidalRisk +} diff --git a/engine/index.ts b/engine/index.ts new file mode 100644 index 0000000..d3a11cb --- /dev/null +++ b/engine/index.ts @@ -0,0 +1,6 @@ +export * from './career' +export * from './economics' +export * from './entities' +export * from './health' +export * from './state' +export * from './traits' diff --git a/engine/state/dependency.ts b/engine/state/dependency.ts new file mode 100644 index 0000000..53ab4de --- /dev/null +++ b/engine/state/dependency.ts @@ -0,0 +1,25 @@ +import type { GameTime } from 'time' + +/** + * A substance or behavioural dependency at a given stage of progression. + * Stages mirror docs/08-mental-health.md §"Addiction modeled honestly": + * experimentation → regular use → problem use → dependence → crisis → + * recovery | chronic | death. + */ +export type DependencyStage = + | 'experimentation' + | 'regular_use' + | 'problem_use' + | 'dependence' + | 'crisis' + | 'in_recovery' + | 'chronic' + +export interface Dependency { + readonly id: string + readonly substance: string + readonly stage: DependencyStage + readonly severity: number + readonly startedAt: GameTime + readonly lastRelapseAt: GameTime | null +} diff --git a/engine/state/index.ts b/engine/state/index.ts new file mode 100644 index 0000000..1641d3b --- /dev/null +++ b/engine/state/index.ts @@ -0,0 +1,4 @@ +export type { Dependency, DependencyStage } from './dependency' +export type { MoodState } from './mood' +export type { SatisfactionProfile } from './satisfaction' +export type { Scar } from './scar' diff --git a/engine/state/mood.ts b/engine/state/mood.ts new file mode 100644 index 0000000..1686292 --- /dev/null +++ b/engine/state/mood.ts @@ -0,0 +1,13 @@ +import type { GameTime } from 'time' + +/** + * Current-week emotional state on the valence × arousal plane. + * valence -1 (negative) to +1 (positive) + * arousal -1 (calm) to +1 (activated) + * See docs/08-mental-health.md §"Separate variables". + */ +export interface MoodState { + readonly valence: number + readonly arousal: number + readonly lastUpdated: GameTime +} diff --git a/engine/state/satisfaction.ts b/engine/state/satisfaction.ts new file mode 100644 index 0000000..a10421b --- /dev/null +++ b/engine/state/satisfaction.ts @@ -0,0 +1,10 @@ +/** + * Life satisfaction split into hedonic (day-to-day pleasure) and eudaimonic + * (sense of meaning / purpose). They can diverge, and eudaimonic matters + * more for end-of-life peace (docs/13-spirituality.md §"Life satisfaction + * is distinct from happiness"). Each 0–1. + */ +export interface SatisfactionProfile { + readonly hedonic: number + readonly eudaimonic: number +} diff --git a/engine/state/scar.ts b/engine/state/scar.ts new file mode 100644 index 0000000..3cd682a --- /dev/null +++ b/engine/state/scar.ts @@ -0,0 +1,14 @@ +import type { GameTime } from 'time' + +/** + * A lasting mark left by a high-weight event. Scars don't decay the way + * mood or stress do — they persist and colour future trait drift, + * relationship patterns, and event eligibility. + */ +export interface Scar { + readonly id: string + readonly kind: string + readonly occurredAt: GameTime + readonly severity: number + readonly summary: string +} diff --git a/engine/traits/attachment.ts b/engine/traits/attachment.ts new file mode 100644 index 0000000..7af4c1b --- /dev/null +++ b/engine/traits/attachment.ts @@ -0,0 +1,11 @@ +/** + * Attachment is tracked as a distribution, not a single tag. Fractions sum + * to 1.0; a character is rarely purely one style. Set by age 3 based on + * caregiver behaviour, mostly fixed thereafter (docs/04-traits.md §"Layer 2"). + */ +export interface AttachmentDistribution { + readonly secure: number + readonly anxious: number + readonly avoidant: number + readonly disorganized: number +} diff --git a/engine/traits/big-five.ts b/engine/traits/big-five.ts new file mode 100644 index 0000000..425c91c --- /dev/null +++ b/engine/traits/big-five.ts @@ -0,0 +1,11 @@ +/** + * Big Five temperament profile, each 0–100. Mostly stable across a life + * with slow drift under sustained conditions. See docs/04-traits.md §"Layer 1". + */ +export interface BigFiveProfile { + readonly openness: number + readonly conscientiousness: number + readonly extraversion: number + readonly agreeableness: number + readonly neuroticism: number +} diff --git a/engine/traits/core-beliefs.ts b/engine/traits/core-beliefs.ts new file mode 100644 index 0000000..4e3b85a --- /dev/null +++ b/engine/traits/core-beliefs.ts @@ -0,0 +1,10 @@ +/** + * Core beliefs shape how adult events are interpreted (docs/04-traits.md + * §"Layer 2"). Each 0–100. Formed in early childhood, harder to shift after. + */ +export interface CoreBeliefs { + readonly selfWorth: number + readonly worldSafety: number + readonly othersTrustworthy: number + readonly agency: number +} diff --git a/engine/traits/dark-triad.ts b/engine/traits/dark-triad.ts new file mode 100644 index 0000000..e8e1450 --- /dev/null +++ b/engine/traits/dark-triad.ts @@ -0,0 +1,10 @@ +/** + * Dark Triad profile, each 0–100. Distribution is skewed low — most + * characters sit in the 20–40 range; a small minority above 60 drives + * specific life shapes (docs/04-traits.md §"Dark Triad characters in play"). + */ +export interface DarkTriadProfile { + readonly narcissism: number + readonly machiavellianism: number + readonly psychopathy: number +} diff --git a/engine/traits/index.ts b/engine/traits/index.ts new file mode 100644 index 0000000..c743af8 --- /dev/null +++ b/engine/traits/index.ts @@ -0,0 +1,6 @@ +export type { AttachmentDistribution } from './attachment' +export type { BigFiveProfile } from './big-five' +export type { CoreBeliefs } from './core-beliefs' +export type { DarkTriadProfile } from './dark-triad' +export type { Gender, SexualOrientation } from './orientation' +export type { ValuesOrientation } from './values' diff --git a/engine/traits/orientation.ts b/engine/traits/orientation.ts new file mode 100644 index 0000000..60decd9 --- /dev/null +++ b/engine/traits/orientation.ts @@ -0,0 +1,16 @@ +/** + * Gender identity and sexual orientation. Continuous axes plus a discrete + * gender tag. See docs/04-traits.md §"Layer 2" and docs/10-sexuality.md. + * + * sexualAttraction 0 = hetero-exclusive, 100 = homo-exclusive + * romanticAttraction independent continuous axis + * sexualityAwareness 0 = pre-discovery, 1 = fully integrated self-knowledge + */ +export type Gender = 'male' | 'female' | 'nonbinary' | 'trans-male' | 'trans-female' + +export interface SexualOrientation { + readonly sexualAttraction: number + readonly romanticAttraction: number + readonly gender: Gender + readonly sexualityAwareness: number +} diff --git a/engine/traits/values.ts b/engine/traits/values.ts new file mode 100644 index 0000000..0d67d16 --- /dev/null +++ b/engine/traits/values.ts @@ -0,0 +1,12 @@ +/** + * Values orientation. Each axis is 0–100 where 0 anchors the left-named + * pole and 100 the right (e.g., tradition_vs_novelty: 0 = deeply traditional, + * 100 = novelty-seeking). See docs/04-traits.md §"Layer 2". + */ +export interface ValuesOrientation { + readonly tradition_vs_novelty: number + readonly selfDirection_vs_conformity: number + readonly hedonism_vs_duty: number + readonly universalism_vs_ingroup: number + readonly power_vs_egalitarian: number +} |
