aboutsummaryrefslogtreecommitdiff
path: root/engine
diff options
context:
space:
mode:
Diffstat (limited to 'engine')
-rw-r--r--engine/career/career.ts32
-rw-r--r--engine/career/index.ts6
-rw-r--r--engine/economics/affordability.ts22
-rw-r--r--engine/economics/economic.ts56
-rw-r--r--engine/economics/index.ts12
-rw-r--r--engine/entities/base.ts37
-rw-r--r--engine/entities/event-log-entry.ts33
-rw-r--r--engine/entities/flow-entry.ts25
-rw-r--r--engine/entities/index.ts67
-rw-r--r--engine/entities/institution.ts40
-rw-r--r--engine/entities/memoir.ts28
-rw-r--r--engine/entities/person-name.ts14
-rw-r--r--engine/entities/person.ts138
-rw-r--r--engine/entities/place.ts55
-rw-r--r--engine/entities/relationship.ts176
-rw-r--r--engine/entities/reputation.ts11
-rw-r--r--engine/entities/residence.ts10
-rw-r--r--engine/entities/routine.ts37
-rw-r--r--engine/entities/scheduled-event.ts24
-rw-r--r--engine/entities/status.ts18
-rw-r--r--engine/entities/world-event.ts39
-rw-r--r--engine/entities/world.ts62
-rw-r--r--engine/health/health.ts77
-rw-r--r--engine/health/index.ts20
-rw-r--r--engine/health/mental-health.ts67
-rw-r--r--engine/index.ts6
-rw-r--r--engine/state/dependency.ts25
-rw-r--r--engine/state/index.ts4
-rw-r--r--engine/state/mood.ts13
-rw-r--r--engine/state/satisfaction.ts10
-rw-r--r--engine/state/scar.ts14
-rw-r--r--engine/traits/attachment.ts11
-rw-r--r--engine/traits/big-five.ts11
-rw-r--r--engine/traits/core-beliefs.ts10
-rw-r--r--engine/traits/dark-triad.ts10
-rw-r--r--engine/traits/index.ts6
-rw-r--r--engine/traits/orientation.ts16
-rw-r--r--engine/traits/values.ts12
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
+}