implemented NPC system and working bartender conversation

This commit is contained in:
2026-08-15 19:02:01 +02:00
parent be63c21e15
commit f89781d78b
18 changed files with 1042 additions and 160 deletions
@@ -0,0 +1,93 @@
// Everything the bartender ever says.
//
// These nodes share a title, which makes them a node group. Callers just
// `<<detour Bartender_Talk>>` and Yarn runs the most specific variation whose
// `when:` conditions pass. Adding a new variation later means adding a node,
// not editing a growing <<if>> chain.
//
// Each variation sets $went_inside so the calling scene knows the verdict.
title: Bartender_Talk
---
<<if $spoke_to_bartender>>
<<if $revealed_fugue_to_bartender>>
Bartender: Ah, still hazy? A drink?
<<else>>
Bartender: Ah, {$player_alias}. A drink?
<<endif>>
-> Yes, please.
<<detour SC101_Hook_Drink_Bartender_Talk>>
-> No, I have questions.
<<detour SC101_Hook_Questions_Bartender_Talk>>
<<else>>
<<set $spoke_to_bartender = true>>
Bartender: Ah, {$player_alias}, back again?
Narrator: What does he mean? "Back again?"
-> Back? I have never been here.
<<set $revealed_fugue_to_bartender = true>>
Narrator: The bartender squints with a critical look.
FACEWORK: He will tell on you. This is the face of a man who is not only here to tend the bar, but to watch, to observe. And to report back to his masters.
Bartender: I could have sworn you are {$player_alias}, weren't you with Mr. Doss a few days ago?
<<if $looking_for_doss neq true>>
Narrator: Mr. Doss?
PROVENANCE: Mr. Doss. We need to find him, appraise him, document our findings, file them, cross-reference them ...
<<else>>
CONFLUENCE: There he is again: Mr. Doss.
<<endif>>
<<set $self_lean_made_asset += 1>>
Bartender: What can I get ya?
-> What can you recommend?
<<detour SC101_Hook_Drink_Bartender_Talk>>
-> Yes. I'll take the usual.
Narrator: The bartender doesn't even flinch, while taking ingredients from the bar.
PROVENANCE: We cannot remember these substances.
AMNESTY: Is this really our favorite drink?
<<set $self_lean_inheritor += 1>>
<<detour SC101_Hook_Drink_Bartender_Talk>>
-> I am, and I have a few questions.
<<detour SC101_Hook_Questions_Bartender_Talk>>
<<endif>>
<<enter_environment>>
===
title: SC101_Hook_Drink_Bartender_Talk
---
Bartender: One Estate coming right up.
<<if $had_the_estate_drink>>
Narrator: He puts another amber drink before you.
<<else>>
<<set $had_the_estate_drink = true>>
Narrator: He puts a deep amber drink before you.
HOUSE POUR: It smells like old wood, oak, which went sour with age, like a library shelf that's been rained on once and dried wrong.
Narrator: You take a sip.
HOUSE POUR: Deceptively soft. Brown sugar, a little burnt. Vanilla that's actually oak-tannin doing an impression of vanilla.
<<endif>>
AMNESTY: Is that the taste of our favorite drink? I cannot remember ...
-> Yes, it is!
<<if $had_the_estate_drink neq true>>
<<set $self_lean_inheritor += 1>>
HOUSE POUR: Long, dry, faintly bitter - gentian root or scorched citrus pith - and it doesn't fade so much as recede, like it's waiting to be poured again.
<<else>>
HOUSE POUR: Long and dry, it recedes again.
<<endif>>
-> I don't think so.
<<if $had_the_estate_drink neq true>>
<<set $self_lean_inheritor -= 1>>
HOUSE POUR: Bitter, at the end. Like it's waiting to be poured again.
<<else>>
HOUSE POUR: Still bitter.
<<endif>>
===
title: SC101_Hook_Questions_Bartender_Talk
---
Bartender: Sure, what do you want to know?
-> Question 1
Bartender: Answer 1
<<detour SC101_Hook_Questions_Bartender_Talk>>
-> Question 2
Bartender: Answer 2
<<detour SC101_Hook_Questions_Bartender_Talk>>
-> Nothing.
Bartender: Alright.
===
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 582cd3b2ab9274b8ca0737660a0d6b1a
guid: 6b8ac538a619b45b8a197ad048a94505
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
@@ -1,65 +0,0 @@
// Everything the bouncer ever says.
//
// These nodes share a title, which makes them a node group. Callers just
// `<<detour Bouncer_Talk>>` and Yarn runs the most specific variation whose
// `when:` conditions pass. Adding a new variation later means adding a node,
// not editing a growing <<if>> chain.
//
// Each variation sets $went_inside so the calling scene knows the verdict.
title: Bouncer_Talk
when: once
---
Bouncer: Evening. Name on the list?
You: Depends who's asking.
-> I'm on the list.
<<set $claimed_to_be_on_list = true>>
<<check placement routine>>
<<if $check_result>>
Bouncer: ...you're not on the list.
Bouncer: But it's slow tonight. Go on.
<<set $went_inside = true>>
<<else>>
Bouncer: Nobody's on the list tonight. Especially not you.
<<set $went_inside = false>>
<<endif>>
-> Let me through. <<if skill_rank("undisclosed") >= 2>>
<<check undisclosed hard -1>>
<<if $check_degree == "critical_success">>
Bouncer: ...yeah. Yeah, okay.
<<set $reputation += 2>>
<<set $went_inside = true>>
<<elseif $check_result>>
Bouncer: Fine. Don't make it a thing.
<<set $went_inside = true>>
<<else>>
Bouncer: Try that again and you're talking to the pavement.
<<set $reputation -= 1>>
<<set $went_inside = false>>
<<endif>>
-> Actually, I'll head home.
Bouncer: Smart choice.
<<set $went_inside = false>>
===
title: Bouncer_Talk
when: $is_vip
---
Bouncer: Didn't even see you there. Go on in.
<<set $went_inside = true>>
===
title: Bouncer_Talk
when: $claimed_to_be_on_list
---
Bouncer: Still not on the list, I assume.
Bouncer: Go on, before I change my mind.
<<set $went_inside = true>>
===
title: Bouncer_Talk
when: always
---
Bouncer: Back again.
<<set $went_inside = true>>
===
+12 -10
View File
@@ -18,16 +18,6 @@ title: Declarations
// How much the door staff likes you.
<<declare $reputation = 0 as number>>
// Set when the player has tried the "I'm on the list" line at least once.
<<declare $claimed_to_be_on_list = false as bool>>
// The bouncer's verdict, read by the scene that called him.
<<declare $went_inside = false as bool>>
// Smart variable: derived, not stored. Can never drift out of sync with
// $reputation, so there's nothing to remember to update.
<<declare $is_vip = $reputation > 10>>
// Written by <<check>> (YarnSkillCommands). Never <<set>> these by hand.
<<declare $check_result = false as bool>>
<<declare $check_roll = 0 as number>>
@@ -56,12 +46,16 @@ title: Declarations
<<declare $found_made_asset_testimony = false as bool>>
<<declare $found_made_asset_document = false as bool>>
<<declare $found_made_asset_intuition = false as bool>>
<<declare $found_journalist_object = false as bool>>
<<declare $found_journalist_testimony = false as bool>>
<<declare $found_journalist_document = false as bool>>
<<declare $found_journalist_intuition = false as bool>>
<<declare $found_inheritor_object = false as bool>>
<<declare $found_inheritor_testimony = false as bool>>
<<declare $looking_for_doss = false as bool>>
<<declare $had_the_estate_drink = false as bool>>
<<declare $found_inheritor_document = false as bool>>
<<declare $found_inheritor_intuition = false as bool>>
@@ -79,6 +73,8 @@ title: Declarations
<<declare $errand_door_list = false as bool>>
<<declare $errand_find_contact = false as bool>>
<<declare $errand_ask_about_name = false as bool>>
<<declare $spoke_to_bartender = false as bool>>
<<declare $revealed_fugue_to_bartender = false as bool>>
// Player name substitution. Uppercase [PC] in the Skill Bible means the full,
// formal form (STANDING ORDER, PROVENANCE); lowercase [pc] means the familiar
@@ -87,4 +83,10 @@ title: Declarations
// Defaults are placeholders; naming the PC is design's call.
<<declare $player_name = "Vesper" as string>>
<<declare $player_name_full = "Vesper Adaire" as string>>
// Club alias. Smart variable: deterministically derived from $player_name via
// AliasNameGenerator.alias_name (hash-seeded pick from two word pools), so it
// is never in sync-drift and never <<set>> by hand. Same real name always
// yields the same alias; the alias itself bears no resemblance to the input.
<<declare $player_alias = alias_name($player_name) as string>>
===
@@ -1,27 +0,0 @@
// The door. Scene files stay thin: they set the stage, detour into character
// nodes for the actual talking, then branch on the outcome.
//
// No longer reached from SC-101: that exit now calls <<enter_environment>>
// and steps out of dialogue instead of jumping here. The game's
// DialogueRunner entry is `Start` in Scenes/SC101.yarn.
title: Entrance_Door
---
Narrator: Neon bleeds across wet asphalt. The queue hasn't moved in ten minutes.
<<detour Bouncer_Talk>>
<<if $went_inside>>
<<jump Entrance_Inside>>
<<else>>
<<jump Entrance_Outside>>
<<endif>>
===
title: Entrance_Inside
---
Narrator: The bass hits you like a wall.
===
title: Entrance_Outside
---
Narrator: The night swallows you back up.
===
@@ -1,10 +0,0 @@
fileFormatVersion: 2
guid: cb0faaa47fe2440d78e49de30f921218
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 11500000, guid: 94073015eacc34c1d8fc6786e43d60ca, type: 3}
+448 -27
View File
@@ -148,12 +148,10 @@ Transform:
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1338917760}
- {fileID: 1765398450}
- {fileID: 2027639678}
- {fileID: 545208167}
- {fileID: 617631794}
- {fileID: 499771567}
- {fileID: 848933010}
- {fileID: 1138462137}
- {fileID: 798705542}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1001 &227288519
@@ -174,7 +172,7 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 3893294197879345259, guid: 64dce48905ffd9b4293e595fa6941544, type: 3}
propertyPath: m_LocalPosition.z
value: 0
value: -5.5
objectReference: {fileID: 0}
- target: {fileID: 3893294197879345259, guid: 64dce48905ffd9b4293e595fa6941544, type: 3}
propertyPath: m_LocalRotation.w
@@ -288,7 +286,7 @@ Transform:
m_GameObject: {fileID: 538020797}
serializedVersion: 2
m_LocalRotation: {x: 0.38268343, y: 0, z: 0, w: 0.92387956}
m_LocalPosition: {x: 85.86633, y: 8.671058, z: -70.820435}
m_LocalPosition: {x: 85.86633, y: 8.671058, z: -76.32044}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -416,12 +414,12 @@ Transform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 545208166}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: -5, y: 1.5, z: 0}
m_LocalScale: {x: 0.2, y: 3, z: 10}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: -7.5, y: 1.5, z: 0}
m_LocalScale: {x: 0.2, y: 3, z: 15}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 91390000}
m_Father: {fileID: 798705542}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!65 &545208168
BoxCollider:
@@ -528,12 +526,12 @@ Transform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 617631793}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 5, y: 1.5, z: 0}
m_LocalScale: {x: 0.2, y: 3, z: 10}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 7.5, y: 1.5, z: 0}
m_LocalScale: {x: 0.2, y: 3, z: 15}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 91390000}
m_Father: {fileID: 798705542}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!65 &617631795
BoxCollider:
@@ -613,6 +611,180 @@ MeshFilter:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 617631793}
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
--- !u!1 &798705541
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 798705542}
m_Layer: 0
m_Name: Geometry
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &798705542
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 798705541}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1338917760}
- {fileID: 1765398450}
- {fileID: 2027639678}
- {fileID: 545208167}
- {fileID: 617631794}
- {fileID: 1988869524}
m_Father: {fileID: 91390000}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &848933009
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 848933010}
- component: {fileID: 848933013}
- component: {fileID: 848933012}
- component: {fileID: 848933011}
- component: {fileID: 848933014}
m_Layer: 0
m_Name: Bartender
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &848933010
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 848933009}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: -5, y: 2, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 91390000}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!135 &848933011
SphereCollider:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 848933009}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 3
m_Radius: 0.5
m_Center: {x: 0, y: 0, z: 0}
--- !u!23 &848933012
MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 848933009}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_MaskInteraction: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!33 &848933013
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 848933009}
m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0}
--- !u!114 &848933014
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 848933009}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 23d0ac85377c640a1900cfd13e89c389, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::NPCStandIn
npcName: Bartender
npcColor: {r: 1, g: 0.92156863, b: 0.015686275, a: 1}
dialogueRunner: {fileID: 910000019}
yarnStartNode: Bartender_Talk
dialogueUI: {fileID: 263597886}
labelHeight: 1.2
labelFontSize: 4
labelColor: {r: 0.2, g: 0.2, b: 0.2, a: 1}
player: {fileID: 499771567}
interactionRange: 3
promptHeight: 0.7
promptFontSize: 3
--- !u!1 &910000001
GameObject:
m_ObjectHideFlags: 0
@@ -1055,6 +1227,143 @@ CanvasRenderer:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 910000016}
m_CullTransparentMesh: 1
--- !u!1 &1138462136
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1138462137}
- component: {fileID: 1138462141}
- component: {fileID: 1138462140}
- component: {fileID: 1138462139}
- component: {fileID: 1138462138}
m_Layer: 0
m_Name: Bartender (1)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1138462137
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1138462136}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 2.5, y: 2, z: 2}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 91390000}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1138462138
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1138462136}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 23d0ac85377c640a1900cfd13e89c389, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::NPCStandIn
npcName: Chair
npcColor: {r: 0, g: 0, b: 1, a: 1}
dialogueRunner: {fileID: 910000019}
yarnStartNode: Bartender_Talk
dialogueUI: {fileID: 263597886}
labelHeight: 1.2
labelFontSize: 4
labelColor: {r: 0.2, g: 0.2, b: 0.2, a: 1}
player: {fileID: 499771567}
interactionRange: 3
promptHeight: 0.7
promptFontSize: 3
--- !u!135 &1138462139
SphereCollider:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1138462136}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 3
m_Radius: 0.5
m_Center: {x: 0, y: 0, z: 0}
--- !u!23 &1138462140
MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1138462136}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_MaskInteraction: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!33 &1138462141
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1138462136}
m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0}
--- !u!1 &1338917759
GameObject:
m_ObjectHideFlags: 0
@@ -1082,12 +1391,12 @@ Transform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1338917759}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 1.5, z: 0}
m_LocalScale: {x: 10, y: 0.2, z: 10}
m_LocalScale: {x: 15, y: 0.2, z: 15}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 91390000}
m_Father: {fileID: 798705542}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!65 &1338917761
BoxCollider:
@@ -1268,7 +1577,7 @@ Transform:
m_GameObject: {fileID: 1470509442}
serializedVersion: 2
m_LocalRotation: {x: 0.38268343, y: 0, z: 0, w: 0.92387956}
m_LocalPosition: {x: 85.86633, y: 8.671058, z: -70.820435}
m_LocalPosition: {x: 85.86633, y: 8.671058, z: -76.32044}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -1687,12 +1996,12 @@ Transform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1765398449}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 1.5, z: -5}
m_LocalScale: {x: 10, y: 3, z: 0.2}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 1.5, z: -7.500004}
m_LocalScale: {x: 15, y: 3, z: 0.2}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 91390000}
m_Father: {fileID: 798705542}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!65 &1765398451
BoxCollider:
@@ -1772,6 +2081,118 @@ MeshFilter:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1765398449}
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
--- !u!1 &1988869523
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1988869524}
- component: {fileID: 1988869527}
- component: {fileID: 1988869526}
- component: {fileID: 1988869525}
m_Layer: 0
m_Name: Table
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1988869524
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1988869523}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 4, y: 2, z: 0}
m_LocalScale: {x: 2, y: 1, z: 10}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 798705542}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!65 &1988869525
BoxCollider:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1988869523}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 3
m_Size: {x: 1, y: 1, z: 1}
m_Center: {x: 0, y: 0, z: 0}
--- !u!23 &1988869526
MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1988869523}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_MaskInteraction: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!33 &1988869527
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1988869523}
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
--- !u!1 &2027639677
GameObject:
m_ObjectHideFlags: 0
@@ -1799,12 +2220,12 @@ Transform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2027639677}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 1.5, z: 5}
m_LocalScale: {x: 10, y: 3, z: 0.2}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 1.5, z: 7.5}
m_LocalScale: {x: 15, y: 3, z: 0.2}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 91390000}
m_Father: {fileID: 798705542}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!65 &2027639679
BoxCollider:
@@ -0,0 +1,61 @@
using Yarn.Unity;
namespace NightclubArcadia.Dialogue
{
/// <summary>
/// Deterministically turns the player's real name into an unrelated club
/// alias. Same input name always yields the same alias (hash-seeded pick
/// from two word pools), but the alias carries no resemblance to the
/// input — it's a stage name, not a nickname derived from it.
///
/// Used from Yarn as a smart variable (see $player_alias in Common.yarn)
/// or directly: {alias_name($player_name)}
/// </summary>
public static class AliasNameGenerator
{
static readonly string[] FirstWords =
{
"Midnight", "Velvet", "Neon", "Crimson", "Static", "Amber",
"Obsidian", "Electric", "Hollow", "Silver", "Violet", "Iron",
"Phantom", "Copper", "Glass", "Ashen",
};
static readonly string[] SecondWords =
{
"Vex", "Wilde", "Sable", "Rook", "Marlowe", "Cross",
"Vane", "Thorn", "Rye", "Blaze", "Faye", "Quinn",
"Storm", "Reyes", "Lark", "Doyle",
};
[YarnFunction("alias_name")]
public static string AliasName(string realName)
{
if (string.IsNullOrEmpty(realName))
{
realName = "unknown";
}
var hash = Fnv1aHash(realName);
var first = FirstWords[hash % (uint)FirstWords.Length];
var second = SecondWords[(hash / (uint)FirstWords.Length) % (uint)SecondWords.Length];
return $"{first} {second}";
}
// FNV-1a: simple, stable across platforms and .NET versions (unlike
// string.GetHashCode, which is not guaranteed stable run-to-run).
static uint Fnv1aHash(string value)
{
const uint offsetBasis = 2166136261;
const uint prime = 16777619;
var hash = offsetBasis;
foreach (var c in value)
{
hash ^= c;
hash *= prime;
}
return hash;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ee985664120c84b41921806fadeccb34
@@ -1,4 +1,3 @@
using StarterAssets;
using UnityEngine;
using Yarn.Unity;
@@ -13,7 +12,6 @@ namespace NightclubArcadia.Dialogue
{
[SerializeField] private DialogueRunner dialogueRunner;
[SerializeField] private GameObject dialogueUI;
[SerializeField] private GameObject playerArmature;
[SerializeField] private string yarnNodeName = "Bartender_Chat";
private bool playerInRange = false;
@@ -38,14 +36,19 @@ namespace NightclubArcadia.Dialogue
}
}
void StartConversation()
async void StartConversation()
{
var playerController = playerArmature.GetComponent<ThirdPersonController>();
if (playerController != null)
playerController.enabled = false;
// Player controls are locked by DialogueTransitionController off the
// runner's onDialogueStart event — nothing to do for that here.
if (DialogueUIVisibility.Show(dialogueUI))
{
// Only reached if the UI was found deactivated; give TMP a
// frame to re-cache its Canvas in OnEnable before any text is
// generated.
await Awaitable.NextFrameAsync();
}
dialogueUI.SetActive(true);
dialogueRunner.StartDialogue(yarnNodeName);
await dialogueRunner.StartDialogue(yarnNodeName);
}
}
}
@@ -5,8 +5,14 @@ using Yarn.Unity;
namespace NightclubArcadia.Dialogue
{
/// <summary>
/// Registers the <<enter_environment>> Yarn command, which steps the player
/// out of dialogue and hands control to the 3D environment.
/// Owns the hand-off between dialogue and the 3D environment: locks the
/// player's controls for as long as a conversation is running, and registers
/// the <<enter_environment>> Yarn command that hands control back.
///
/// The lock is driven by the DialogueRunner's own lifecycle events rather
/// than by each interactable, so every entry point — auto-started nodes,
/// NPCStandIn, DialogueInteractable — gets the same behaviour without
/// having to remember to do it.
/// </summary>
public class DialogueTransitionController : MonoBehaviour
{
@@ -15,25 +21,93 @@ namespace NightclubArcadia.Dialogue
[SerializeField] private GameObject playerArmature;
private ThirdPersonController playerController;
private StarterAssetsInputs playerInputs;
private Animator playerAnimator;
static readonly int AnimIDSpeed = Animator.StringToHash("Speed");
static readonly int AnimIDMotionSpeed = Animator.StringToHash("MotionSpeed");
void Awake()
{
if (playerArmature != null)
{
playerController = playerArmature.GetComponent<ThirdPersonController>();
playerInputs = playerArmature.GetComponent<StarterAssetsInputs>();
playerAnimator = playerArmature.GetComponent<Animator>();
}
// Subscribed in Awake rather than Start: the runner auto-starts its
// first node from its own Start(), and script execution order
// between the two is not guaranteed.
dialogueRunner.onDialogueStart?.AddListener(OnDialogueStart);
dialogueRunner.onDialogueComplete?.AddListener(OnDialogueComplete);
}
void Start()
{
playerController = playerArmature.GetComponent<ThirdPersonController>();
// Dialogue is in progress as soon as this controller is active (including
// auto-started nodes), so the player stays locked out until <<enter_environment>>.
// The armature itself stays active/visible — only its controls are disabled.
if (playerController != null)
playerController.enabled = false;
// Dialogue is in progress as soon as this controller is active
// (including auto-started nodes), so the player stays locked out
// until <<enter_environment>>. The armature itself stays
// active/visible — only its controls are disabled.
SetPlayerControlEnabled(false);
dialogueRunner.AddCommandHandler("enter_environment", EnterEnvironment);
}
void OnDestroy()
{
dialogueRunner.onDialogueStart?.RemoveListener(OnDialogueStart);
dialogueRunner.onDialogueComplete?.RemoveListener(OnDialogueComplete);
}
void OnDialogueStart()
{
DialogueUIVisibility.Show(dialogueUI);
SetPlayerControlEnabled(false);
}
// Safety net: a node that ends without <<enter_environment>> must still
// give the player their controls back.
void OnDialogueComplete() => EnterEnvironment();
void EnterEnvironment()
{
dialogueUI.SetActive(false);
if (playerController != null)
playerController.enabled = true;
// Hide, don't deactivate: deactivating the Canvas breaks TMP's
// cached Canvas reference and makes the next conversation throw.
// See DialogueUIVisibility.
DialogueUIVisibility.Hide(dialogueUI);
SetPlayerControlEnabled(true);
}
void SetPlayerControlEnabled(bool value)
{
if (playerController == null)
{
return;
}
if (!value)
{
// Clear pending input and the locomotion blend before freezing
// the controller. Disabling it mid-stride otherwise leaves
// Speed at its last value, and the character keeps running on
// the spot for the whole conversation.
if (playerInputs != null)
{
playerInputs.MoveInput(Vector2.zero);
playerInputs.LookInput(Vector2.zero);
playerInputs.SprintInput(false);
playerInputs.JumpInput(false);
}
if (playerAnimator != null)
{
playerAnimator.SetFloat(AnimIDSpeed, 0f);
playerAnimator.SetFloat(AnimIDMotionSpeed, 0f);
}
}
playerController.enabled = value;
}
}
}
@@ -0,0 +1,67 @@
using UnityEngine;
namespace NightclubArcadia.Dialogue
{
/// <summary>
/// Shows and hides the dialogue UI *without* deactivating its GameObject.
///
/// SetActive(false) is not a safe way to hide this UI. TextMeshProUGUI
/// resolves its Canvas through Graphic.CacheCanvas, which calls
/// GetComponentsInParent(includeInactive: false) and then requires
/// canvas.isActiveAndEnabled. Once the Canvas GameObject has been
/// deactivated, that lookup yields null, and TMP.GetTextInfo does:
///
/// if (m_canvas == null) m_canvas = this.canvas; // still null
/// GenerateTextMesh(); // m_canvas.worldCamera -> NRE
///
/// GenerateTextMesh dereferences m_canvas unguarded, so the very next line
/// Yarn tries to type throws NullReferenceException. Keeping the hierarchy
/// active and driving a CanvasGroup instead means the cached Canvas
/// reference stays valid across any number of enter/exit dialogue cycles.
/// </summary>
public static class DialogueUIVisibility
{
/// <summary>
/// Makes the dialogue UI visible and interactive. Returns true if the
/// GameObject had to be re-activated (legacy scene state), in which
/// case the caller must wait one frame before rendering text so that
/// TMP's OnEnable can re-cache its Canvas.
/// </summary>
public static bool Show(GameObject dialogueUI) => SetVisible(dialogueUI, true);
/// <summary>
/// Hides the dialogue UI, leaving the hierarchy active. See class docs.
/// </summary>
public static void Hide(GameObject dialogueUI) => SetVisible(dialogueUI, false);
static bool SetVisible(GameObject dialogueUI, bool visible)
{
if (dialogueUI == null)
{
return false;
}
// The UI may still be deactivated by an older saved scene state or
// by code that predates this helper. Recover from that, and tell
// the caller a frame of grace is needed.
var wasReactivated = false;
if (!dialogueUI.activeSelf)
{
dialogueUI.SetActive(true);
wasReactivated = true;
}
var group = dialogueUI.GetComponent<CanvasGroup>();
if (group == null)
{
group = dialogueUI.AddComponent<CanvasGroup>();
}
group.alpha = visible ? 1f : 0f;
group.interactable = visible;
group.blocksRaycasts = visible;
return wasReactivated;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ad5dcf93105bd49b287dbd19bc42cef7
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2216657f3a0224501bcfa09add14f689
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,48 @@
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.LowLevel;
/// <summary>
/// Tracks which input device was most recently active, globally, across the whole game.
/// NPCStandIn (and any other UI that shows button prompts) reads InputDeviceTracker.Current
/// to decide whether to show "[F]", "(X)", "(✕)", etc.
/// Requires the Input System package (Window > Package Manager > Input System),
/// and Project Settings > Player > Active Input Handling set to
/// "Input System Package (New)" or "Both".
/// </summary>
public static class InputDeviceTracker
{
public enum DeviceType { Keyboard, Xbox, PlayStation, Generic }
public static DeviceType Current { get; private set; } = DeviceType.Keyboard;
[RuntimeInitializeOnLoadMethod]
static void Init()
{
InputSystem.onEvent += OnEvent;
}
static void OnEvent(InputEventPtr eventPtr, InputDevice device)
{
// Only care about actual state changes (button/stick/key presses), not noise.
if (!eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>()) return;
switch (device)
{
case Keyboard:
case Mouse:
Current = DeviceType.Keyboard;
break;
case Gamepad gp:
string n = gp.displayName.ToLowerInvariant();
if (n.Contains("dualshock") || n.Contains("dualsense") || n.Contains("playstation"))
Current = DeviceType.PlayStation;
else if (n.Contains("xbox") || n.Contains("xinput"))
Current = DeviceType.Xbox;
else
Current = DeviceType.Generic;
break;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 941f7ff9039984f729ceddebd57d4159
@@ -0,0 +1,199 @@
using UnityEngine;
using UnityEngine.InputSystem;
using TMPro;
using Yarn.Unity;
using NightclubArcadia.Dialogue;
/// <summary>
/// Drop this on a sphere primitive to use it as a placeholder NPC:
/// - Sets sphere color per-instance
/// - Spawns a floating TextMeshPro name label that billboards to face the camera
/// - Shows a proximity interaction prompt ("[F]", "(X)", "(✕)") that swaps based on
/// the last input device used, via InputDeviceTracker
/// - Press the interact button while in range to start a Yarn Spinner dialogue node
/// Requires: TextMeshPro Essentials imported, Input System package installed,
/// InputDeviceTracker.cs in the project, and a "Player" tag on your player object
/// (or assign the Player field manually in the Inspector).
/// </summary>
[RequireComponent(typeof(SphereCollider))]
public class NPCStandIn : MonoBehaviour
{
[Header("Identity")]
public string npcName = "Unnamed NPC";
public Color npcColor = Color.white;
[Header("Dialogue")]
public DialogueRunner dialogueRunner;
public string yarnStartNode = "Start";
[Tooltip("Optional. Left empty, the Canvas under the DialogueRunner is used.")]
public GameObject dialogueUI;
[Header("Label")]
public float labelHeight = 1.2f;
public float labelFontSize = 4f;
public Color labelColor = Color.white;
[Header("Interaction Prompt")]
public Transform player; // auto-found by "Player" tag if left empty
public float interactionRange = 3f;
public float promptHeight = 0.7f;
public float promptFontSize = 3f;
private TextMeshPro labelText;
private TextMeshPro promptText;
private Renderer rend;
private Camera mainCam;
private InputAction interactAction;
private bool playerInRange;
void Awake()
{
rend = GetComponent<Renderer>();
rend.material.color = npcColor;
mainCam = Camera.main;
if (player == null)
{
GameObject found = GameObject.FindGameObjectWithTag("Player");
if (found != null) player = found.transform;
}
CreateLabel();
CreatePrompt();
interactAction = new InputAction("Interact");
interactAction.AddBinding("<Keyboard>/f");
interactAction.AddBinding("<Gamepad>/buttonSouth");
interactAction.performed += ctx =>
{
if (playerInRange) Interact();
};
}
void OnEnable() => interactAction?.Enable();
void OnDisable() => interactAction?.Disable();
void CreateLabel()
{
GameObject labelObj = new GameObject($"{npcName}_Label");
labelObj.transform.SetParent(transform);
labelObj.transform.localPosition = Vector3.up * labelHeight;
labelText = labelObj.AddComponent<TextMeshPro>();
labelText.text = npcName;
labelText.fontSize = labelFontSize;
labelText.color = labelColor;
labelText.alignment = TextAlignmentOptions.Center;
labelText.rectTransform.sizeDelta = new Vector2(2f, 0.5f);
}
void CreatePrompt()
{
GameObject promptObj = new GameObject($"{npcName}_Prompt");
promptObj.transform.SetParent(transform);
promptObj.transform.localPosition = Vector3.up * promptHeight;
promptText = promptObj.AddComponent<TextMeshPro>();
promptText.fontSize = promptFontSize;
promptText.color = Color.yellow;
promptText.alignment = TextAlignmentOptions.Center;
promptText.rectTransform.sizeDelta = new Vector2(1.5f, 0.4f);
promptObj.SetActive(false); // hidden until player is in range
}
void Update()
{
if (player == null) return;
float dist = Vector3.Distance(transform.position, player.position);
bool inRangeNow = dist <= interactionRange;
if (inRangeNow != playerInRange)
{
playerInRange = inRangeNow;
promptText.gameObject.SetActive(playerInRange);
}
if (playerInRange)
{
promptText.text = GlyphFor(InputDeviceTracker.Current);
}
}
void LateUpdate()
{
if (mainCam == null) return;
if (labelText != null)
labelText.transform.rotation = Quaternion.LookRotation(
labelText.transform.position - mainCam.transform.position);
if (promptText != null)
promptText.transform.rotation = Quaternion.LookRotation(
promptText.transform.position - mainCam.transform.position);
}
static string GlyphFor(InputDeviceTracker.DeviceType type)
{
switch (type)
{
case InputDeviceTracker.DeviceType.Xbox: return "(X)";
case InputDeviceTracker.DeviceType.PlayStation: return "(\u2715)"; // Cross button
default: return "[F]";
}
}
public void Interact()
{
if (dialogueRunner == null)
{
Debug.LogWarning($"[NPCStandIn] No DialogueRunner assigned on {npcName}");
return;
}
if (!dialogueRunner.IsDialogueRunning)
{
StartDialogueAsync();
}
}
async void StartDialogueAsync()
{
// The dialogue UI is hidden when the player leaves a conversation via
// <<enter_environment>>. Make it visible again before the runner
// renders its first line.
if (DialogueUIVisibility.Show(ResolveDialogueUI()))
{
// Only reached if the UI was found deactivated; give TMP a frame
// to re-cache its Canvas in OnEnable before any text is generated.
await Awaitable.NextFrameAsync();
}
await dialogueRunner.StartDialogue(yarnStartNode);
}
/// <summary>
/// The Canvas under the DialogueRunner, unless one was assigned explicitly.
/// Resolved at call time (including inactive children) so this works
/// without any per-NPC Inspector wiring.
/// </summary>
GameObject ResolveDialogueUI()
{
if (dialogueUI != null)
{
return dialogueUI;
}
var canvas = dialogueRunner.GetComponentInChildren<Canvas>(true);
if (canvas == null)
{
Debug.LogWarning($"[NPCStandIn] No Canvas found under {dialogueRunner.name} for {npcName}");
return null;
}
dialogueUI = canvas.gameObject;
return dialogueUI;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 23d0ac85377c640a1900cfd13e89c389