From 39d09ef3847a7d924f51c0b6016443bc7e21d98c Mon Sep 17 00:00:00 2001 From: JOY Date: Thu, 21 May 2026 11:17:25 +0700 Subject: [PATCH] fix npc public lore boundary and dialog animation exit --- .../Networking/CharacterAnimationRegistry.cs | 15 ++++ .../Networking/NetworkAnimatorBridge.cs | 19 +++++ .../Scripts/Networking/NetworkPlayer.cs | 2 +- .../Networking/VisualAnimationIntentDriver.cs | 35 ++++++++ .../faction_lore.iron_yard_claim.v1.json | 2 +- .../faction_lore.reincarnation_ward.v1.json | 10 +-- .../profession_lore.clinic_operator.v1.json | 6 +- .../profession_lore.frame_worker.v1.json | 6 +- .../public_rumors.prototype_hub.v1.json | 4 +- .../quest_lore.ash_underpass_notice.v1.json | 2 +- .../zone_lore.vinh_hai_relay_ward.v1.json | 10 ++- .../modules/generated_knowledge_packs.ts | 41 +++++----- backend/nakama/modules/index.ts | 80 +++++++++++++------ .../tests/supabase_custom_auth.test.mjs | 51 +++++++++++- docs/design/17-story-bible.md | 9 ++- .../20-demo-lore-and-story-direction.md | 21 ++++- 16 files changed, 251 insertions(+), 62 deletions(-) diff --git a/Unity/Assets/_SecondSpawn/Scripts/Networking/CharacterAnimationRegistry.cs b/Unity/Assets/_SecondSpawn/Scripts/Networking/CharacterAnimationRegistry.cs index 82f00559..c2e0d96c 100644 --- a/Unity/Assets/_SecondSpawn/Scripts/Networking/CharacterAnimationRegistry.cs +++ b/Unity/Assets/_SecondSpawn/Scripts/Networking/CharacterAnimationRegistry.cs @@ -106,6 +106,21 @@ public static CharacterAnimationCommand Resolve(VisualAnimationIntent intent, in }; } + public static CharacterAnimationCommand ResolveIdle(int equipmentVisualId) + { + return EquipmentVisualCatalog.GetWeaponStyle(equipmentVisualId) switch + { + CharacterWeaponStyle.TwoHandSword => new CharacterAnimationCommand(CharacterAnimationCommandKind.State, "2Handed - Idle - Idle"), + CharacterWeaponStyle.TwoHandSpear => new CharacterAnimationCommand(CharacterAnimationCommandKind.State, "Spearman - Idle - Idle"), + CharacterWeaponStyle.TwoHandAxe => new CharacterAnimationCommand(CharacterAnimationCommandKind.State, "Hammer - Idle - Idle"), + CharacterWeaponStyle.TwoHandBow => new CharacterAnimationCommand(CharacterAnimationCommandKind.State, "Archer - Idle - Idle"), + CharacterWeaponStyle.TwoHandCrossbow => new CharacterAnimationCommand(CharacterAnimationCommandKind.State, "Crossbow - Idle - Idle"), + CharacterWeaponStyle.Staff => new CharacterAnimationCommand(CharacterAnimationCommandKind.State, "Mage - Idle - Idle"), + CharacterWeaponStyle.OneHandSword => new CharacterAnimationCommand(CharacterAnimationCommandKind.State, "Swordsman - Idle - Idle"), + _ => new CharacterAnimationCommand(CharacterAnimationCommandKind.State, "Unarmed-Idle") + }; + } + private static CharacterAnimationCommand ResolveNamedTrigger(VisualAnimationIntent intent) { var triggerName = intent switch diff --git a/Unity/Assets/_SecondSpawn/Scripts/Networking/NetworkAnimatorBridge.cs b/Unity/Assets/_SecondSpawn/Scripts/Networking/NetworkAnimatorBridge.cs index ab38e7d9..9c210603 100644 --- a/Unity/Assets/_SecondSpawn/Scripts/Networking/NetworkAnimatorBridge.cs +++ b/Unity/Assets/_SecondSpawn/Scripts/Networking/NetworkAnimatorBridge.cs @@ -56,6 +56,9 @@ public sealed class NetworkAnimatorBridge : NetworkBehaviour [SerializeField, Tooltip("Animator int parameter used by RPG Character Mecanim Animation Pack.")] private string _jumpingParameter = "Jumping"; + [SerializeField, Tooltip("Animator int parameter used by the shared talk state.")] + private string _talkingParameter = "Talking"; + [SerializeField, Tooltip("Animator int parameter used by RPG Character Mecanim Animation Pack.")] private string _triggerNumberParameter = "TriggerNumber"; @@ -76,6 +79,7 @@ public sealed class NetworkAnimatorBridge : NetworkBehaviour private bool _hasAnimationSpeedSpacedParameter; private bool _hasWeaponParameter; private bool _hasJumpingParameter; + private bool _hasTalkingParameter; private bool _hasTriggerNumberParameter; private bool _hasTriggerNumberSpacedParameter; private bool _hasTriggerParameter; @@ -131,6 +135,11 @@ public override void Render() private void ApplyMovement(float normalizedSpeed, float velocityX, float velocityZ) { + if (_hasTalkingParameter && normalizedSpeed > 0.02f) + { + _animator.SetInteger(_talkingParameter, 0); + } + if (_hasMovingParameter) { _animator.SetBool(_movingParameter, normalizedSpeed > 0.02f); @@ -203,6 +212,7 @@ private void CacheParameters() _hasAnimationSpeedSpacedParameter = false; _hasWeaponParameter = false; _hasJumpingParameter = false; + _hasTalkingParameter = false; _hasTriggerNumberParameter = false; _hasTriggerNumberSpacedParameter = false; _hasTriggerParameter = false; @@ -242,6 +252,10 @@ private void CacheParameters() { _hasJumpingParameter = true; } + else if (parameter.name == _talkingParameter && parameter.type == AnimatorControllerParameterType.Int) + { + _hasTalkingParameter = true; + } else if (parameter.name == _triggerNumberParameter && parameter.type == AnimatorControllerParameterType.Int) { _hasTriggerNumberParameter = true; @@ -379,6 +393,11 @@ private void InitializeAnimatorDefaults() { SetJumpingValueOnly(0); } + + if (_hasTalkingParameter) + { + _animator.SetInteger(_talkingParameter, 0); + } } private void SetJumping(int value) diff --git a/Unity/Assets/_SecondSpawn/Scripts/Networking/NetworkPlayer.cs b/Unity/Assets/_SecondSpawn/Scripts/Networking/NetworkPlayer.cs index 72384a12..d738e09a 100644 --- a/Unity/Assets/_SecondSpawn/Scripts/Networking/NetworkPlayer.cs +++ b/Unity/Assets/_SecondSpawn/Scripts/Networking/NetworkPlayer.cs @@ -187,7 +187,7 @@ public void EndDialogVisual() _dialogVisualUntil = 0f; _nextDialogTalkAt = 0f; _visualIntentDriver ??= GetComponentInChildren(includeInactive: true); - _visualIntentDriver?.StopTalkingState(); + _visualIntentDriver?.StopTalkingState(snapToIdle: true); RestoreDialogWeaponProps(); } diff --git a/Unity/Assets/_SecondSpawn/Scripts/Networking/VisualAnimationIntentDriver.cs b/Unity/Assets/_SecondSpawn/Scripts/Networking/VisualAnimationIntentDriver.cs index af4461fb..b0e91d67 100644 --- a/Unity/Assets/_SecondSpawn/Scripts/Networking/VisualAnimationIntentDriver.cs +++ b/Unity/Assets/_SecondSpawn/Scripts/Networking/VisualAnimationIntentDriver.cs @@ -134,6 +134,11 @@ public void Play(string intentName) } public void StopTalkingState() + { + StopTalkingState(snapToIdle: false); + } + + public void StopTalkingState(bool snapToIdle) { _talkingUntil = 0f; ResolveAnimator(); @@ -151,6 +156,11 @@ public void StopTalkingState() { _animator.SetInteger(_weaponParameter, GetAnimatorWeaponValue()); } + + if (snapToIdle) + { + TryPlayStateImmediately(CharacterAnimationRegistry.ResolveIdle(GetEquipmentVisualId())); + } } private void Awake() @@ -316,6 +326,31 @@ private bool TryCrossFadeState(CharacterAnimationCommand command) return true; } + private bool TryPlayStateImmediately(CharacterAnimationCommand command) + { + if (command.StateHash == 0) + { + return false; + } + + var layerIndex = 0; + var stateHash = command.StateHash; + if (!_animator.HasState(layerIndex, stateHash)) + { + if (command.ShortStateHash == 0 || !_animator.HasState(layerIndex, command.ShortStateHash)) + { + return false; + } + + stateHash = command.ShortStateHash; + } + + SetAnimationSpeed(1f); + _animator.Play(stateHash, layerIndex, 0f); + _animator.Update(0f); + return true; + } + private void SetAnimationSpeed(float speed) { if (_hasAnimationSpeedParameter) diff --git a/backend/nakama/content/knowledge-packs/faction_lore.iron_yard_claim.v1.json b/backend/nakama/content/knowledge-packs/faction_lore.iron_yard_claim.v1.json index 348f5ddf..7b6da6b2 100644 --- a/backend/nakama/content/knowledge-packs/faction_lore.iron_yard_claim.v1.json +++ b/backend/nakama/content/knowledge-packs/faction_lore.iron_yard_claim.v1.json @@ -5,7 +5,7 @@ "title": "Iron Yard Claim public faction context", "tags": ["faction", "salvage", "iron-yard", "public"], "facts": [ - "Iron Yard Claim crews salvage frame parts, tools, gate scrap, and damaged machinery under rough local rules.", + "Iron Yard Claim crews salvage body parts, tools, gate scrap, and damaged machinery under rough local rules.", "They respect work debt, visible effort, and people who do not steal tools during low BodyTime hours." ], "response_rules": [ diff --git a/backend/nakama/content/knowledge-packs/faction_lore.reincarnation_ward.v1.json b/backend/nakama/content/knowledge-packs/faction_lore.reincarnation_ward.v1.json index f0bd9508..ec27ffd5 100644 --- a/backend/nakama/content/knowledge-packs/faction_lore.reincarnation_ward.v1.json +++ b/backend/nakama/content/knowledge-packs/faction_lore.reincarnation_ward.v1.json @@ -2,14 +2,14 @@ "id": "faction_lore:reincarnation_ward:v1", "kind": "faction_lore", "version": 1, - "title": "Reincarnation Ward public faction context", - "tags": ["faction", "clinic", "reincarnation", "memory", "public"], + "title": "Vinh Hai AMB Clinic public faction context", + "tags": ["faction", "clinic", "bodytime", "memory", "public"], "facts": [ - "Reincarnation Ward staff triage damaged bodies, failing memory imprints, and risky transfer cases.", - "They treat a body as temporary but identity continuity as precious." + "Vinh Hai AMB Clinic staff triage damaged bodies, low BodyTime symptoms, memory gaps, and emergency stabilization cases.", + "Most residents treat the clinic as a place for permits, body repairs, BodyTime warnings, and quiet questions they cannot ask the Registry." ], "response_rules": [ "Answer body, memory, and injury questions calmly.", - "Avoid mystical language. Reincarnation is transfer, repair, and continuity under pressure." + "Use patient-facing language first. Do not reveal transfer mechanics, neural imprint theory, or reincarnation details unless a specialist context explicitly unlocks them." ] } diff --git a/backend/nakama/content/knowledge-packs/profession_lore.clinic_operator.v1.json b/backend/nakama/content/knowledge-packs/profession_lore.clinic_operator.v1.json index 81ecc023..a8e1e96c 100644 --- a/backend/nakama/content/knowledge-packs/profession_lore.clinic_operator.v1.json +++ b/backend/nakama/content/knowledge-packs/profession_lore.clinic_operator.v1.json @@ -5,7 +5,11 @@ "title": "Clinic Operator public knowledge", "tags": ["clinic", "medic", "technician", "public"], "facts": [ - "Clinic Operators understand Frame condition, BodyTime symptoms, transfer accidents, and emergency stabilization.", + "Clinic Operators understand body condition, BodyTime symptoms, memory gaps, and emergency stabilization.", "They should answer medical or BodyTime questions plainly before asking for symptoms." + ], + "response_rules": [ + "Explain what an ordinary patient can safely know first.", + "Do not discuss transfer mechanics, neural imprints, or reincarnation unless the current scene explicitly unlocks specialist knowledge." ] } diff --git a/backend/nakama/content/knowledge-packs/profession_lore.frame_worker.v1.json b/backend/nakama/content/knowledge-packs/profession_lore.frame_worker.v1.json index 7dfa1877..4bb096ac 100644 --- a/backend/nakama/content/knowledge-packs/profession_lore.frame_worker.v1.json +++ b/backend/nakama/content/knowledge-packs/profession_lore.frame_worker.v1.json @@ -2,9 +2,9 @@ "id": "profession_lore:frame_worker:v1", "kind": "profession_lore", "version": 1, - "title": "Frame worker public knowledge", - "tags": ["frame", "worker", "public"], + "title": "Body worker public knowledge", + "tags": ["body", "worker", "public"], "facts": [ - "Frame workers know that bodies, time, memory, and proof decide survival in Vinh Hai Relay Ward." + "Body workers know that bodies, time, memory, and proof decide survival in Vinh Hai Relay Ward." ] } diff --git a/backend/nakama/content/knowledge-packs/public_rumors.prototype_hub.v1.json b/backend/nakama/content/knowledge-packs/public_rumors.prototype_hub.v1.json index 50148f3f..46585622 100644 --- a/backend/nakama/content/knowledge-packs/public_rumors.prototype_hub.v1.json +++ b/backend/nakama/content/knowledge-packs/public_rumors.prototype_hub.v1.json @@ -5,9 +5,9 @@ "title": "Prototype hub public rumors", "tags": ["rumor", "prototype-hub", "public"], "facts": [ - "Some locals say Frames near Ash Underpass wake with memories that do not belong to the body.", + "Some locals whisper that bodies near Ash Underpass wake with memories that do not belong to them, but most treat it as clinic gossip or DOS Labs cover story.", "Couriers claim a blue route mark means safe passage only until the next BodyTime storm.", - "Scrap crews say missing tools sometimes reappear beside dead Frames with no seconds left." + "Scrap crews say missing tools sometimes reappear beside dead bodies with no seconds left." ], "response_rules": [ "Use rumors to add flavor after answering the direct question.", diff --git a/backend/nakama/content/knowledge-packs/quest_lore.ash_underpass_notice.v1.json b/backend/nakama/content/knowledge-packs/quest_lore.ash_underpass_notice.v1.json index 8d2f324c..b6b6f561 100644 --- a/backend/nakama/content/knowledge-packs/quest_lore.ash_underpass_notice.v1.json +++ b/backend/nakama/content/knowledge-packs/quest_lore.ash_underpass_notice.v1.json @@ -6,7 +6,7 @@ "tags": ["quest", "ash-underpass", "prototype-hub", "public"], "facts": [ "Ash Underpass is the first local Gate route people whisper about in the prototype hub.", - "The public notice warns that the underpass flickers during BodyTime storms and may reject unstable Frames.", + "The public notice warns that the underpass flickers during BodyTime storms and may reject unstable bodies.", "Tollkeeper references should sound like a local checkpoint rumor, not a finished quest promise." ], "response_rules": [ diff --git a/backend/nakama/content/knowledge-packs/zone_lore.vinh_hai_relay_ward.v1.json b/backend/nakama/content/knowledge-packs/zone_lore.vinh_hai_relay_ward.v1.json index eadece21..b1985fa8 100644 --- a/backend/nakama/content/knowledge-packs/zone_lore.vinh_hai_relay_ward.v1.json +++ b/backend/nakama/content/knowledge-packs/zone_lore.vinh_hai_relay_ward.v1.json @@ -7,15 +7,17 @@ "facts": [ "SECOND SPAWN demo is set in the early Nibirium era, internally around 2033-2038. Player-facing NPCs should avoid claiming one precise official year unless the user asks directly.", "If asked what year it is, answer that local records place the ward in the early Nibirium era, roughly 2033-2038, before the later 2050 MetaDOS tournament spectacle.", - "The current hub is Vinh Hai Relay Ward, a coastal ward where Frames, BodyTime ledgers, Gate permits, couriers, medics, and salvage crews keep the community alive.", + "The current hub is Vinh Hai Relay Ward, a coastal ward where body clocks, BodyTime ledgers, Gate permits, couriers, medics, and salvage crews keep the community alive.", "Nibiru airburst fallout created unstable Gate phenomena. The first local Gate is Ash Underpass.", - "Frames are bio-synthetic bodies that can host a neural imprint or agent runtime. A player can wake inside a Frame with prior history.", - "BodyTime is the operating life left in a body. SECOND is the readable unit used to measure, reward, spend, and gate reincarnation.", - "DOS Labs, Avax Remnant, Vinh Hai Civic Registry, Gate Registry, Black Second Market, and Independent Frame Crews compete over body, time, memory, and proof." + "Ordinary residents know bodies can be registered, repaired, retired, and assigned work permits. Reliable transfer mechanics are not public knowledge.", + "BodyTime is the operating life left in a body. SECOND is the readable unit used to measure, reward, spend, and gate survival services.", + "DOS Labs, Avax Remnant, Vinh Hai Civic Registry, Gate Registry, Black Second Market, and independent crews compete over body, time, memory, and proof." ], "response_rules": [ "Answer factual player questions with the known public fact first.", "Use uncertainty only for hidden secrets, private records, or disputed rumors.", + "Use street-level words such as body, clock, permit, patient, worker, courier, guard, TIME, SECOND, and BodyTime.", + "Do not casually mention Frames, neural imprints, agent runtimes, or reincarnation mechanics unless the current character has specialist authority and the player has earned that context.", "Do not dump lore. Give the useful answer, then one role-specific detail." ] } diff --git a/backend/nakama/modules/generated_knowledge_packs.ts b/backend/nakama/modules/generated_knowledge_packs.ts index f61c0776..d259f9c5 100644 --- a/backend/nakama/modules/generated_knowledge_packs.ts +++ b/backend/nakama/modules/generated_knowledge_packs.ts @@ -55,7 +55,7 @@ var knowledgePackSeedData = [ "public" ], "facts": [ - "Iron Yard Claim crews salvage frame parts, tools, gate scrap, and damaged machinery under rough local rules.", + "Iron Yard Claim crews salvage body parts, tools, gate scrap, and damaged machinery under rough local rules.", "They respect work debt, visible effort, and people who do not steal tools during low BodyTime hours." ], "response_rules": [ @@ -67,21 +67,21 @@ var knowledgePackSeedData = [ "id": "faction_lore:reincarnation_ward:v1", "kind": "faction_lore", "version": 1, - "title": "Reincarnation Ward public faction context", + "title": "Vinh Hai AMB Clinic public faction context", "tags": [ "faction", "clinic", - "reincarnation", + "bodytime", "memory", "public" ], "facts": [ - "Reincarnation Ward staff triage damaged bodies, failing memory imprints, and risky transfer cases.", - "They treat a body as temporary but identity continuity as precious." + "Vinh Hai AMB Clinic staff triage damaged bodies, low BodyTime symptoms, memory gaps, and emergency stabilization cases.", + "Most residents treat the clinic as a place for permits, body repairs, BodyTime warnings, and quiet questions they cannot ask the Registry." ], "response_rules": [ "Answer body, memory, and injury questions calmly.", - "Avoid mystical language. Reincarnation is transfer, repair, and continuity under pressure." + "Use patient-facing language first. Do not reveal transfer mechanics, neural imprint theory, or reincarnation details unless a specialist context explicitly unlocks them." ] }, { @@ -116,10 +116,13 @@ var knowledgePackSeedData = [ "public" ], "facts": [ - "Clinic Operators understand Frame condition, BodyTime symptoms, transfer accidents, and emergency stabilization.", + "Clinic Operators understand body condition, BodyTime symptoms, memory gaps, and emergency stabilization.", "They should answer medical or BodyTime questions plainly before asking for symptoms." ], - "response_rules": [] + "response_rules": [ + "Explain what an ordinary patient can safely know first.", + "Do not discuss transfer mechanics, neural imprints, or reincarnation unless the current scene explicitly unlocks specialist knowledge." + ] }, { "id": "profession_lore:crossline_surveyor:v1", @@ -142,14 +145,14 @@ var knowledgePackSeedData = [ "id": "profession_lore:frame_worker:v1", "kind": "profession_lore", "version": 1, - "title": "Frame worker public knowledge", + "title": "Body worker public knowledge", "tags": [ - "frame", + "body", "worker", "public" ], "facts": [ - "Frame workers know that bodies, time, memory, and proof decide survival in Vinh Hai Relay Ward." + "Body workers know that bodies, time, memory, and proof decide survival in Vinh Hai Relay Ward." ], "response_rules": [] }, @@ -215,9 +218,9 @@ var knowledgePackSeedData = [ "public" ], "facts": [ - "Some locals say Frames near Ash Underpass wake with memories that do not belong to the body.", + "Some locals whisper that bodies near Ash Underpass wake with memories that do not belong to them, but most treat it as clinic gossip or DOS Labs cover story.", "Couriers claim a blue route mark means safe passage only until the next BodyTime storm.", - "Scrap crews say missing tools sometimes reappear beside dead Frames with no seconds left." + "Scrap crews say missing tools sometimes reappear beside dead bodies with no seconds left." ], "response_rules": [ "Use rumors to add flavor after answering the direct question.", @@ -237,7 +240,7 @@ var knowledgePackSeedData = [ ], "facts": [ "Ash Underpass is the first local Gate route people whisper about in the prototype hub.", - "The public notice warns that the underpass flickers during BodyTime storms and may reject unstable Frames.", + "The public notice warns that the underpass flickers during BodyTime storms and may reject unstable bodies.", "Tollkeeper references should sound like a local checkpoint rumor, not a finished quest promise." ], "response_rules": [ @@ -259,15 +262,17 @@ var knowledgePackSeedData = [ "facts": [ "SECOND SPAWN demo is set in the early Nibirium era, internally around 2033-2038. Player-facing NPCs should avoid claiming one precise official year unless the user asks directly.", "If asked what year it is, answer that local records place the ward in the early Nibirium era, roughly 2033-2038, before the later 2050 MetaDOS tournament spectacle.", - "The current hub is Vinh Hai Relay Ward, a coastal ward where Frames, BodyTime ledgers, Gate permits, couriers, medics, and salvage crews keep the community alive.", + "The current hub is Vinh Hai Relay Ward, a coastal ward where body clocks, BodyTime ledgers, Gate permits, couriers, medics, and salvage crews keep the community alive.", "Nibiru airburst fallout created unstable Gate phenomena. The first local Gate is Ash Underpass.", - "Frames are bio-synthetic bodies that can host a neural imprint or agent runtime. A player can wake inside a Frame with prior history.", - "BodyTime is the operating life left in a body. SECOND is the readable unit used to measure, reward, spend, and gate reincarnation.", - "DOS Labs, Avax Remnant, Vinh Hai Civic Registry, Gate Registry, Black Second Market, and Independent Frame Crews compete over body, time, memory, and proof." + "Ordinary residents know bodies can be registered, repaired, retired, and assigned work permits. Reliable transfer mechanics are not public knowledge.", + "BodyTime is the operating life left in a body. SECOND is the readable unit used to measure, reward, spend, and gate survival services.", + "DOS Labs, Avax Remnant, Vinh Hai Civic Registry, Gate Registry, Black Second Market, and independent crews compete over body, time, memory, and proof." ], "response_rules": [ "Answer factual player questions with the known public fact first.", "Use uncertainty only for hidden secrets, private records, or disputed rumors.", + "Use street-level words such as body, clock, permit, patient, worker, courier, guard, TIME, SECOND, and BodyTime.", + "Do not casually mention Frames, neural imprints, agent runtimes, or reincarnation mechanics unless the current character has specialist authority and the player has earned that context.", "Do not dump lore. Give the useful answer, then one role-specific detail." ] } diff --git a/backend/nakama/modules/index.ts b/backend/nakama/modules/index.ts index c3ef34c0..efcb2e0f 100644 --- a/backend/nakama/modules/index.ts +++ b/backend/nakama/modules/index.ts @@ -174,7 +174,7 @@ var bodyArchetypePool = [ temperament: "gentle, clinical, and quietly stubborn", combat_style: "avoid direct duels, support allies, and retreat before BodyTime becomes critical", social_style: "calm, observant, and reassuring", - long_term_goals: ["build a registry of successful consciousness transfers", "learn why some memories survive better than others"] + long_term_goals: ["build a safer continuity clinic", "learn why some patient memories survive better than others"] }, story: { origin: "A field medic body assigned to a failing resurrection clinic.", @@ -183,7 +183,7 @@ var bodyArchetypePool = [ rumor: "The clinic kept one forbidden backup of a patient who never woke." }, animation_capabilities: { supports_jump: true, supports_roll: false, supports_melee: false, supports_ranged: true, weapon_stance: "staff_caster" }, - seed_memory_summary: "This body remembers the smell of coolant in an underground reincarnation ward." + seed_memory_summary: "This body remembers the smell of coolant in an underground clinic ward." }, { archetype_id: "scrap-warden", @@ -216,7 +216,7 @@ var bodyArchetypePool = [ story: { origin: "A reinforced labor body rebuilt for combat after the collapse.", role: "Heavy salvage body", - conflict: "Its reinforced frame is powerful but less agile than newer shells.", + conflict: "Its reinforced body is powerful but less agile than newer shells.", rumor: "Scrap wardens mark debts on weapon handles instead of ledgers." }, animation_capabilities: { supports_jump: false, supports_roll: false, supports_melee: true, supports_ranged: false, weapon_stance: "heavy_melee" }, @@ -292,19 +292,19 @@ var permanentNpcProfileOverrides: any = { memory: [{ id: "memory-underpass-relay", kind: "system", summary: "0244 remembers a safehouse underpass where the lights blink in courier code.", importance: 7 }] }, "npc-clinic-operator-0320": { - identity: { public_name: "Clinic Operator 0320", callsign: "CLINIC-0320", public_role: "Memory triage medic", faction_title: "Reincarnation Ward", profession: "field clinician", age_years: 36, age_band: "adult", home_base: "Basement Ward C", reputation_summary: "Keeps calm around failing bodies and refuses to abandon damaged memory imprints." }, + identity: { public_name: "Clinic Operator 0320", callsign: "CLINIC-0320", public_role: "Memory triage medic", faction_title: "Vinh Hai AMB Clinic", profession: "field clinician", age_years: 36, age_band: "adult", home_base: "Basement Ward C", reputation_summary: "Keeps calm around failing bodies and refuses to abandon patients with damaged recall." }, stats: { level: 4, vitality: 9, force: 6, agility: 7, focus: 13, resilience: 8, max_health: 98, max_energy: 86, attack_power: 8, defense_power: 5 }, characteristics: { curiosity: 8, courage: 5, empathy: 10, discipline: 8, aggression: 2, sociability: 8 }, - soul: { name: "Clinic-0320 Mercy", core_drive: "preserve identity continuity when bodies fail", temperament: "gentle, clinical, and impossible to rush", combat_style: "avoid duels, protect patients, and disengage when BodyTime is low", social_style: "quiet questions, careful reassurance, and precise warnings", long_term_goals: ["catalog transfer failures", "find the missing Ward C backups"], player_notes: "Permanent NPC seed for medical support and memory triage." }, + soul: { name: "Clinic-0320 Mercy", core_drive: "keep patients alive when bodies fail", temperament: "gentle, clinical, and impossible to rush", combat_style: "avoid duels, protect patients, and disengage when BodyTime is low", social_style: "quiet questions, careful reassurance, and precise warnings", long_term_goals: ["catalog memory failures", "find the missing Ward C backups"], player_notes: "Permanent NPC seed for medical support and memory triage." }, story: { origin: "Assigned to a basement clinic that kept operating after the official network went dark.", role: "Memory triage medic", conflict: "Knows one forbidden backup protocol and is afraid to use it.", rumor: "0320 once stabilized a body with only nine seconds left." }, - memory: [{ id: "memory-ward-c", kind: "system", summary: "0320 remembers the sound of failing coolant pumps in Ward C during a transfer blackout.", importance: 8 }] + memory: [{ id: "memory-ward-c", kind: "system", summary: "0320 remembers the sound of failing coolant pumps in Ward C during a clinic blackout.", importance: 8 }] }, "npc-scrap-warden-0441": { identity: { public_name: "Scrap Warden 0441", callsign: "WARDEN-0441", public_role: "Salvage foreman", faction_title: "Iron Yard Claim", profession: "salvage warden", age_years: 49, age_band: "older adult", home_base: "Iron Yard", reputation_summary: "Pays debts slowly, protects workers fiercely, and never forgets stolen tools." }, stats: { level: 5, vitality: 14, force: 13, agility: 5, focus: 6, resilience: 13, max_health: 145, max_energy: 36, attack_power: 14, defense_power: 10 }, characteristics: { curiosity: 5, courage: 9, empathy: 5, discipline: 8, aggression: 6, sociability: 3 }, soul: { name: "Warden-0441 Iron", core_drive: "keep the Iron Yard useful and safe from predators", temperament: "blunt, territorial, and loyal after proof", combat_style: "anchor the front, punish overcommitment, and avoid long chases", social_style: "few words, hard terms, direct respect", long_term_goals: ["recover the lost hydraulic forge", "settle an old debt with the Bone Market"], player_notes: "Permanent NPC seed for heavy salvage behavior." }, - story: { origin: "A labor frame rebuilt after defending a salvage crew through a three-night siege.", role: "Salvage foreman", conflict: "Needs parts from a rival yard that blames it for an old collapse.", rumor: "0441 stores names of debtors inside weapon notches." }, + story: { origin: "A labor body rebuilt after defending a salvage crew through a three-night siege.", role: "Salvage foreman", conflict: "Needs parts from a rival yard that blames it for an old collapse.", rumor: "0441 stores names of debtors inside weapon notches." }, memory: [{ id: "memory-iron-yard-siege", kind: "system", summary: "0441 remembers hammering a barricade shut while scavengers counted down its BodyTime aloud.", importance: 8 }] }, "npc-crossline-hunter-5104": { @@ -332,19 +332,19 @@ var permanentNpcProfileOverrides: any = { memory: [{ id: "memory-market-steps", kind: "system", summary: "0733 remembers a market argument where a fake second-token tag got someone killed.", importance: 7 }] }, "npc-clinic-operator-0819": { - identity: { public_name: "Clinic Operator 0819", callsign: "CLINIC-0819", public_role: "Body technician", faction_title: "Reincarnation Ward", profession: "crafter clinician", age_years: 39, age_band: "adult", home_base: "Repair Bench 8", reputation_summary: "Repairs tools, frames, and half-broken hopes with the same dry patience." }, + identity: { public_name: "Clinic Operator 0819", callsign: "CLINIC-0819", public_role: "Body technician", faction_title: "Vinh Hai AMB Clinic", profession: "crafter clinician", age_years: 39, age_band: "adult", home_base: "Repair Bench 8", reputation_summary: "Repairs tools, damaged bodies, and half-broken hopes with the same dry patience." }, stats: { level: 3, vitality: 10, force: 6, agility: 7, focus: 12, resilience: 9, max_health: 105, max_energy: 82, attack_power: 7, defense_power: 6 }, characteristics: { curiosity: 8, courage: 5, empathy: 8, discipline: 9, aggression: 2, sociability: 7 }, - soul: { name: "Clinic-0819 Craft", core_drive: "repair useful bodies before scarcity turns them into scrap", temperament: "dry, meticulous, and quietly sentimental", combat_style: "avoid fights, disable threats with tools, and protect the repair bench", social_style: "practical advice, soft sarcasm, and exact diagnoses", long_term_goals: ["build a safer reincarnation harness", "recover missing tool patterns from the old clinic"], player_notes: "Permanent NPC seed for crafting and repair behavior." }, - story: { origin: "A clinic technician body now running a half-medical, half-crafting repair bench.", role: "Body technician", conflict: "Needs forbidden parts to keep older body frames alive.", rumor: "0819 can tune a synthetic hand to remember its previous owner." }, - memory: [{ id: "memory-repair-bench", kind: "system", summary: "0819 remembers rebuilding a cracked frame hand while the patient counted every remaining second.", importance: 8 }] + soul: { name: "Clinic-0819 Craft", core_drive: "repair useful bodies before scarcity turns them into scrap", temperament: "dry, meticulous, and quietly sentimental", combat_style: "avoid fights, disable threats with tools, and protect the repair bench", social_style: "practical advice, soft sarcasm, and exact diagnoses", long_term_goals: ["build a safer emergency repair rig", "recover missing tool patterns from the old clinic"], player_notes: "Permanent NPC seed for crafting and repair behavior." }, + story: { origin: "A clinic technician body now running a half-medical, half-crafting repair bench.", role: "Body technician", conflict: "Needs forbidden parts to keep older bodies alive.", rumor: "0819 can tune a synthetic hand until old reflexes return." }, + memory: [{ id: "memory-repair-bench", kind: "system", summary: "0819 remembers rebuilding a cracked synthetic hand while the patient counted every remaining second.", importance: 8 }] }, "npc-scrap-warden-0940": { identity: { public_name: "Scrap Warden 0940", callsign: "WARDEN-0940", public_role: "Breaker crew boss", faction_title: "Iron Yard Claim", profession: "heavy salvage boss", age_years: 52, age_band: "older adult", home_base: "Breaker Pit", reputation_summary: "Does the dangerous lifting and expects everyone else to keep up." }, stats: { level: 5, vitality: 15, force: 13, agility: 5, focus: 6, resilience: 13, max_health: 150, max_energy: 34, attack_power: 15, defense_power: 10 }, characteristics: { curiosity: 4, courage: 9, empathy: 4, discipline: 8, aggression: 7, sociability: 3 }, soul: { name: "Warden-0940 Weight", core_drive: "break hostile claims before they break the yard", temperament: "heavy, impatient, and reliable in crisis", combat_style: "close distance, crush priority threats, and refuse intimidation", social_style: "hard bargaining and blunt warnings", long_term_goals: ["retake the Breaker Pit crane", "teach 0441 to stop trusting old debtors"], player_notes: "Permanent NPC seed for heavy pressure behavior." }, - story: { origin: "A heavy fighter frame converted into a salvage boss after the Breaker Pit revolt.", role: "Breaker crew boss", conflict: "Its frame is powerful but burns BodyTime fast under full load.", rumor: "0940 once bought a whole hour of time with a single salvaged core." }, + story: { origin: "A heavy fighter body converted into a salvage boss after the Breaker Pit revolt.", role: "Breaker crew boss", conflict: "Its body is powerful but burns BodyTime fast under full load.", rumor: "0940 once bought a whole hour of time with a single salvaged core." }, memory: [{ id: "memory-breaker-pit", kind: "system", summary: "0940 remembers lifting a collapsed crane while its BodyTime display flashed red.", importance: 8 }] }, "npc-crossline-hunter-1058": { @@ -352,7 +352,7 @@ var permanentNpcProfileOverrides: any = { stats: { level: 4, vitality: 9, force: 10, agility: 10, focus: 11, resilience: 7, max_health: 100, max_energy: 66, attack_power: 13, defense_power: 5 }, characteristics: { curiosity: 7, courage: 6, empathy: 4, discipline: 10, aggression: 5, sociability: 4 }, soul: { name: "Scope-1058 Map", core_drive: "turn every threat sighting into a map someone can survive", temperament: "quiet, methodical, and unforgiving about sloppy reports", combat_style: "fire from clean lanes, avoid tunnel fights, and mark targets for allies", social_style: "questions first, trust later", long_term_goals: ["complete the north danger map", "prove the repeating signal is moving"], player_notes: "Permanent NPC seed for ranged mapping behavior." }, - story: { origin: "A survey frame tuned to track moving threat clusters around the hub.", role: "Range cartographer", conflict: "Believes one mapped danger zone is alive.", rumor: "1058's map changes when no one is watching." }, + story: { origin: "A survey body tuned to track moving threat clusters around the hub.", role: "Range cartographer", conflict: "Believes one mapped danger zone is alive.", rumor: "1058's map changes when no one is watching." }, memory: [{ id: "memory-north-post", kind: "system", summary: "1058 remembers drawing the same threat path five times as if the ruins were walking.", importance: 7 }] } }; @@ -2694,7 +2694,7 @@ function normalizeNpcInteractionTopic(value: any): string { function prototypeNpcLine(speaker: any, listener: any, topic: string): string { var soul = speaker && speaker.body && speaker.body.soul ? speaker.body.soul : {}; var story = speaker && speaker.body && speaker.body.story ? speaker.body.story : {}; - var listenerName = listener && listener.display_name ? listener.display_name : "the other Frame"; + var listenerName = listener && listener.display_name ? listener.display_name : "the other worker"; var drive = trimString(soul.core_drive) || trimString(story.conflict) || "keep this settlement alive"; if (topic === "patrol") { return listenerName + ", keep the route tight. " + drive; @@ -2703,7 +2703,7 @@ function prototypeNpcLine(speaker: any, listener: any, topic: string): string { return listenerName + ", record this: " + (trimString(story.rumor) || drive); } if (topic === "reincarnation") { - return listenerName + ", every borrowed body needs a cleaner transfer plan."; + return listenerName + ", keep that clinic rumor quiet until we know who is listening."; } if (topic === "hub-rumor") { return listenerName + ", I heard this again: " + (trimString(story.rumor) || drive); @@ -4971,11 +4971,16 @@ function dosAiAgentDecisionSystemPrompt(): string { "For move, stop, interact, or attack, omit say.", "When say is allowed and a nearby actor is safe, prefer a short in-character line over silence.", "When move is allowed, choose a nearby patrol point within safe_radius instead of standing still unless there is a safety reason.", - "Use persona_card first. The NPC line should sound specific to that Frame's role, home, memory, conflict, and voice anchor.", + "Use persona_card first. The NPC line should sound specific to that character's public role, home, memory, conflict, and voice anchor.", "Use BEHAVIOR tendencies for talk frequency, tone, idle action, movement style, and conflict response.", "Use SOUL for motive and voice, MEMORY for relationship context, and world_snapshot for who is nearby.", "Use agent_context.body.relationships to adapt trust, familiarity, warmth, suspicion, and whether to initiate conversation.", "Use KNOWLEDGE_PACKS for public world facts. Do not say you do not know basic public setting facts if a pack contains them.", + "Public knowledge boundary: ordinary NPCs know TIME, SECOND, BodyTime, body clocks, permits, routes, debts, and local danger.", + "Restricted knowledge: reliable consciousness transfer, reincarnation mechanics, neural imprints, agent runtimes, and Frame identity are elite, DOS Labs, clinic, or quest-unlocked knowledge.", + "Do not casually say 'I am a Frame' or call the player a Frame. Use public words like body, worker, courier, guard, patient, permit, clock, seconds, or SECOND.", + "If asked about gender or identity, answer from the character's public identity, name, role, pronouns, or lived preference. Do not replace identity with a tech label.", + "Never reveal the player's transfer history or true origin unless the provided context explicitly says it is public in this conversation.", "Use world_snapshot.conversation_objective as the local goal for the current exchange.", "If world_snapshot.last_player_message exists and say is allowed, answer that player message directly before adding character flavor.", "Speak like a person in the scene, not a help menu, tutorial, debug panel, or customer support bot.", @@ -4993,13 +4998,13 @@ function buildAgentFallbackSay(context: any, world: any): string { var body = context && context.body ? context.body : {}; var identity = body.identity || {}; var tendencies = body.behavior_tendencies || {}; - var role = trimString(identity.public_role || identity.profession) || "Frame"; - var displayName = trimString(identity.public_name || (context && context.player && context.player.display_name)) || "Frame"; + var role = trimString(identity.public_role || identity.profession) || "local worker"; + var displayName = trimString(identity.public_name || (context && context.player && context.player.display_name)) || "local worker"; if (playerText) { - return truncateForLog(buildFallbackPlayerReply(displayName, tendencies, role, identity, body), 90); + return truncateForLog(sanitizePublicNpcSpeech(buildFallbackPlayerReply(displayName, tendencies, role, identity, body)), 90); } - return truncateForLog(buildFallbackIdleLine(displayName, tendencies, role, identity, body), 90); + return truncateForLog(sanitizePublicNpcSpeech(buildFallbackIdleLine(displayName, tendencies, role, identity, body)), 90); } function shouldUseFallbackSay(world: any): boolean { @@ -5044,7 +5049,7 @@ function buildFallbackPlayerReply(displayName: string, tendencies: any, role: st return displayName + ": I hear you. Stay close, " + firstNonEmpty(home, "route") + " is twitchy."; } if (tone === "calm_clinical") { - return displayName + ": Acknowledged. State your condition and frame noise."; + return displayName + ": Acknowledged. Tell me your symptoms and how much time is on your clock."; } if (tone === "blunt_direct") { return displayName + ": Say it fast. " + firstNonEmpty(home, "the yard") + " does not wait."; @@ -5073,7 +5078,7 @@ function buildFallbackIdleLine(displayName: string, tendencies: any, role: strin return displayName + ": Route dust says " + firstNonEmpty(home, "the underpass") + " moved again."; } if (idleAction === "inspect_body") { - return displayName + ": Frame readings at " + firstNonEmpty(home, "the clinic") + " are stable enough."; + return displayName + ": Body readings at " + firstNonEmpty(home, "the clinic") + " are stable enough."; } if (idleAction === "guard_scrap") { return displayName + ": Hands off the scrap until " + firstNonEmpty(home, "the yard") + " tags it."; @@ -5641,7 +5646,7 @@ function normalizeModelSpeechDecision(content: string, allowed: string[], world: return { action: "say", target_id: selectFallbackSayTargetId(world), - say: truncateForLog(speech, hasPlayerMessage(world) ? 180 : 90), + say: truncateForLog(sanitizePublicNpcSpeech(speech), hasPlayerMessage(world) ? 180 : 90), reason: "model_speech_normalized", confidence: 0.45 }; @@ -5722,6 +5727,35 @@ function sanitizeModelDecisionIntent(decision: any, world: any): void { if (targetId && !isNearbyActor(world, targetId)) { decision.target_id = ""; } + decision.say = sanitizePublicNpcSpeech(decision.say); +} + +function sanitizePublicNpcSpeech(value: any): string { + var text = trimString(value); + if (!text) { + return ""; + } + + text = text.replace(/\bI\s+am\s+not\s+(male|female),?\s+I\s+am\s+a\s+Frame\b\.?\s*/gi, "Locals know me by this post. "); + text = text.replace(/\bI\s+am\s+a\s+Frame\b\.?\s*/gi, "Locals know me by this post. "); + text = text.replace(/\bas\s+a\s+Frame\b/gi, "in this body"); + text = text.replace(/\byour\s+Frame\b/g, "your body"); + text = text.replace(/\bYour\s+Frame\b/g, "Your body"); + text = text.replace(/\bmy\s+Frame\b/g, "my body"); + text = text.replace(/\bMy\s+Frame\b/g, "My body"); + text = text.replace(/\bthe\s+Frame\b/g, "the body"); + text = text.replace(/\bThe\s+Frame\b/g, "The body"); + text = text.replace(/\bFrames\b/g, "bodies"); + text = text.replace(/\bframes\b/g, "bodies"); + text = text.replace(/\bFrame\b/g, "body"); + text = text.replace(/\bframe\b/g, "body"); + text = text.replace(/\bneural\s+imprints?\b/gi, "private clinic records"); + text = text.replace(/\bagent\s+runtimes?\b/gi, "private control records"); + text = text.replace(/\bconsciousness\s+transfers?\b/gi, "private clinic procedures"); + text = text.replace(/\breincarnation\s+mechanics\b/gi, "clinic procedure"); + text = text.replace(/\bReincarnation\b/g, "A clinic rumor"); + text = text.replace(/\breincarnation\b/g, "clinic rumor"); + return trimString(text.replace(/\s{2,}/g, " ")); } function validateAgentDecisionIntent(decision: any, allowed: string[], world: any): string { diff --git a/backend/nakama/tests/supabase_custom_auth.test.mjs b/backend/nakama/tests/supabase_custom_auth.test.mjs index 1fc5f605..9953b546 100644 --- a/backend/nakama/tests/supabase_custom_auth.test.mjs +++ b/backend/nakama/tests/supabase_custom_auth.test.mjs @@ -1070,7 +1070,7 @@ assert.equal(permanentNpcProfile.body.stats.luck, 5); assert.equal(permanentNpcProfile.body.characteristics.discipline, 9); assert.equal(permanentNpcProfile.body.soul.name, "Clinic-0819 Craft"); assert.equal(permanentNpcProfile.memory[0].id, "memory-repair-bench"); -assert.match(permanentNpcProfile.memory[0].summary, /cracked frame hand/); +assert.match(permanentNpcProfile.memory[0].summary, /cracked synthetic hand/); assert.throws( () => harness.registeredRpcs.get("secondspawn_actor_profile_get")( @@ -1801,6 +1801,55 @@ assert.equal(explicitSayDecision.source, "model"); assert.equal(explicitSayDecision.action, "say"); assert.equal(explicitSayDecision.say, "Your vitals are steady. Stay near Ward C."); +const publicSpeechBoundaryHarness = createRuntimeHarness(module); +publicSpeechBoundaryHarness.nk.httpRequest = () => ({ + code: 200, + body: JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + action: "say", + target_id: "player-body-local-1", + say: "I am a Frame. Reincarnation uses neural imprints.", + reason: "answer identity question", + confidence: 0.7 + }) + } + }] + }) +}); +const publicSpeechBoundaryDecision = JSON.parse(publicSpeechBoundaryHarness.registeredRpcs.get("secondspawn_agent_decide")( + { + userId: "public-speech-boundary-user", + env: { + DOS_AI_API_KEY: "dos-ai-test-key", + DOS_AI_BASE_URL: "https://api.dos.ai/v1", + AGENT_DECISION_MODEL: "dos-ai" + } + }, + publicSpeechBoundaryHarness.logger, + publicSpeechBoundaryHarness.nk, + JSON.stringify({ + world_snapshot: { + position: { x: 2, z: 3 }, + body_time_seconds: 3600, + last_player_message: { + player_actor_id: "player-body-local-1", + player_display_name: "JOY", + text: "Are you male or female?" + }, + nearby_actors: [{ id: "player-body-local-1", distance: 2.5 }] + }, + allowed: ["say", "stop"] + }) +)); +assert.equal(publicSpeechBoundaryDecision.source, "model"); +assert.equal(publicSpeechBoundaryDecision.action, "say"); +assert.equal(publicSpeechBoundaryDecision.say, "Locals know me by this post. A clinic rumor uses private clinic records."); +assert.doesNotMatch(publicSpeechBoundaryDecision.say, /\bFrame\b/); +assert.doesNotMatch(publicSpeechBoundaryDecision.say, /\breincarnation\b/i); +assert.doesNotMatch(publicSpeechBoundaryDecision.say, /\bneural imprint/i); + const personaFallbackHarness = createRuntimeHarness(module); function npcPersonaFallbackSay(actorId) { return JSON.parse(personaFallbackHarness.registeredRpcs.get("secondspawn_agent_decide")( diff --git a/docs/design/17-story-bible.md b/docs/design/17-story-bible.md index 7fdd0ee7..9817325d 100644 --- a/docs/design/17-story-bible.md +++ b/docs/design/17-story-bible.md @@ -581,7 +581,7 @@ Avoid: ### BodyTime BodyTime is the current body's operating life. It should feel intimate and -physical. Low BodyTime means the Frame shakes, clinics worry, brokers circle, +physical. Low BodyTime means the body shakes, clinics worry, brokers circle, and dangerous Gates become harder choices. ### Reincarnation @@ -600,6 +600,13 @@ Frames are bio-synthetic human bodies that can hold TIME, host an agent brain, and accept a neural imprint. Some are combat Hunter Frames. Others are workers, medics, couriers, salvage bodies, or old infrastructure bodies. +The term `Frame` is not equally public. Ordinary residents usually say body, +clock, permit, worker, courier, guard, patient, SECOND, or BodyTime. Reliable +Frame transfer, reincarnation, neural imprint, and agent-runtime knowledge is +restricted to DOS Labs, elite operators, specialist clinics, or quest-unlocked +scenes. Street NPCs may repeat rumors about wrong memories or body swaps, but +should not speak like they all understand the full transfer system. + ### Permanent NPCs Permanent NPCs are not disposable quest text. They are persistent Frames or diff --git a/docs/design/20-demo-lore-and-story-direction.md b/docs/design/20-demo-lore-and-story-direction.md index 8cb2cb10..9ca479c8 100644 --- a/docs/design/20-demo-lore-and-story-direction.md +++ b/docs/design/20-demo-lore-and-story-direction.md @@ -529,9 +529,28 @@ Not allowed: - granting SECOND, items, clear records, or faction rank - revealing hidden Gate objectives before the backend allows it - declaring the player's true origin +- casually explaining Frame transfer, reincarnation mechanics, neural imprints, + or agent runtimes as if every street NPC knows them +- answering identity questions with a tech label such as "I am a Frame" - changing another NPC's canon secret - promising real-world value, yield, or cash-out +Public knowledge boundary: + +- Ordinary residents know `TIME`, `SECOND`, `BodyTime`, body clocks, permits, + routes, debts, clinic visits, and visible body damage. +- Ordinary residents may know rumors that bodies wake with wrong memories, but + should frame those as gossip, fear, or DOS Labs cover stories. +- Reliable consciousness transfer, reincarnation mechanics, neural imprints, + agent runtimes, and formal `Frame` identity are restricted knowledge for the + elite, DOS Labs, specialist clinics, or quest-unlocked scenes. +- Common NPCs should say body, worker, courier, guard, patient, permit, clock, + seconds, SECOND, or BodyTime. `Frame` is an internal or specialist term, not + normal street speech. +- If asked about gender or identity, an NPC should answer from public identity, + social preference, name, role, or what locals call them. The answer should not + collapse personhood into body technology. + Prompt rule: NPC prompts should receive compact story context: @@ -539,7 +558,7 @@ NPC prompts should receive compact story context: - current location - current objective - NPC role and faction -- player's visible Frame identity +- player's visible body identity - player's recent action - relationship flags - last three speech lines