From f89781d78bb5e3ca4973668ecaca87084eee64c3 Mon Sep 17 00:00:00 2001 From: grabowskil Date: Sat, 15 Aug 2026 19:02:01 +0200 Subject: [PATCH] implemented NPC system and working bartender conversation --- .../Assets/Dialogue/Characters/Bartender.yarn | 93 ++++ ...{Bouncer.yarn.meta => Bartender.yarn.meta} | 2 +- .../Assets/Dialogue/Characters/Bouncer.yarn | 65 --- NightclubArcadia/Assets/Dialogue/Common.yarn | 22 +- .../Assets/Dialogue/Scenes/Entrance.yarn | 27 - .../Assets/Dialogue/Scenes/Entrance.yarn.meta | 10 - .../Assets/Scenes/DialogueTest.unity | 475 +++++++++++++++++- .../Scripts/Dialogue/AliasNameGenerator.cs | 61 +++ .../Dialogue/AliasNameGenerator.cs.meta | 2 + .../Scripts/Dialogue/DialogueInteractable.cs | 19 +- .../Dialogue/DialogueTransitionController.cs | 98 +++- .../Scripts/Dialogue/DialogueUIVisibility.cs | 67 +++ .../Dialogue/DialogueUIVisibility.cs.meta | 2 + NightclubArcadia/Assets/Scripts/NPCs.meta | 8 + .../Assets/Scripts/NPCs/InputDeviceTracker.cs | 48 ++ .../Scripts/NPCs/InputDeviceTracker.cs.meta | 2 + .../Assets/Scripts/NPCs/NPCStandIn.cs | 199 ++++++++ .../Assets/Scripts/NPCs/NPCStandIn.cs.meta | 2 + 18 files changed, 1042 insertions(+), 160 deletions(-) create mode 100644 NightclubArcadia/Assets/Dialogue/Characters/Bartender.yarn rename NightclubArcadia/Assets/Dialogue/Characters/{Bouncer.yarn.meta => Bartender.yarn.meta} (86%) delete mode 100644 NightclubArcadia/Assets/Dialogue/Characters/Bouncer.yarn delete mode 100644 NightclubArcadia/Assets/Dialogue/Scenes/Entrance.yarn delete mode 100644 NightclubArcadia/Assets/Dialogue/Scenes/Entrance.yarn.meta create mode 100644 NightclubArcadia/Assets/Scripts/Dialogue/AliasNameGenerator.cs create mode 100644 NightclubArcadia/Assets/Scripts/Dialogue/AliasNameGenerator.cs.meta create mode 100644 NightclubArcadia/Assets/Scripts/Dialogue/DialogueUIVisibility.cs create mode 100644 NightclubArcadia/Assets/Scripts/Dialogue/DialogueUIVisibility.cs.meta create mode 100644 NightclubArcadia/Assets/Scripts/NPCs.meta create mode 100644 NightclubArcadia/Assets/Scripts/NPCs/InputDeviceTracker.cs create mode 100644 NightclubArcadia/Assets/Scripts/NPCs/InputDeviceTracker.cs.meta create mode 100644 NightclubArcadia/Assets/Scripts/NPCs/NPCStandIn.cs create mode 100644 NightclubArcadia/Assets/Scripts/NPCs/NPCStandIn.cs.meta diff --git a/NightclubArcadia/Assets/Dialogue/Characters/Bartender.yarn b/NightclubArcadia/Assets/Dialogue/Characters/Bartender.yarn new file mode 100644 index 0000000..cc51138 --- /dev/null +++ b/NightclubArcadia/Assets/Dialogue/Characters/Bartender.yarn @@ -0,0 +1,93 @@ +// Everything the bartender ever says. +// +// These nodes share a title, which makes them a node group. Callers just +// `<>` and Yarn runs the most specific variation whose +// `when:` conditions pass. Adding a new variation later means adding a node, +// not editing a growing <> chain. +// +// Each variation sets $went_inside so the calling scene knows the verdict. + +title: Bartender_Talk +--- +<> + <> + Bartender: Ah, still hazy? A drink? + <> + Bartender: Ah, {$player_alias}. A drink? + <> + -> Yes, please. + <> + -> No, I have questions. + <> +<> + <> + Bartender: Ah, {$player_alias}, back again? + Narrator: What does he mean? "Back again?" + -> Back? I have never been here. + <> + 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? + <> + Narrator: Mr. Doss? + PROVENANCE: Mr. Doss. We need to find him, appraise him, document our findings, file them, cross-reference them ... + <> + CONFLUENCE: There he is again: Mr. Doss. + <> + <> + Bartender: What can I get ya? + -> What can you recommend? + <> + -> 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? + <> + <> + -> I am, and I have a few questions. + <> +<> +<> +=== + +title: SC101_Hook_Drink_Bartender_Talk +--- +Bartender: One Estate coming right up. +<> + Narrator: He puts another amber drink before you. +<> + <> + 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. +<> +AMNESTY: Is that the taste of our favorite drink? I cannot remember ... +-> Yes, it is! + <> + <> + 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. + <> + HOUSE POUR: Long and dry, it recedes again. + <> +-> I don't think so. + <> + <> + HOUSE POUR: Bitter, at the end. Like it's waiting to be poured again. + <> + HOUSE POUR: Still bitter. + <> +=== + +title: SC101_Hook_Questions_Bartender_Talk +--- +Bartender: Sure, what do you want to know? +-> Question 1 + Bartender: Answer 1 + <> +-> Question 2 + Bartender: Answer 2 + <> +-> Nothing. + Bartender: Alright. +=== diff --git a/NightclubArcadia/Assets/Dialogue/Characters/Bouncer.yarn.meta b/NightclubArcadia/Assets/Dialogue/Characters/Bartender.yarn.meta similarity index 86% rename from NightclubArcadia/Assets/Dialogue/Characters/Bouncer.yarn.meta rename to NightclubArcadia/Assets/Dialogue/Characters/Bartender.yarn.meta index 001560a..6283b21 100644 --- a/NightclubArcadia/Assets/Dialogue/Characters/Bouncer.yarn.meta +++ b/NightclubArcadia/Assets/Dialogue/Characters/Bartender.yarn.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 582cd3b2ab9274b8ca0737660a0d6b1a +guid: 6b8ac538a619b45b8a197ad048a94505 ScriptedImporter: internalIDToNameTable: [] externalObjects: {} diff --git a/NightclubArcadia/Assets/Dialogue/Characters/Bouncer.yarn b/NightclubArcadia/Assets/Dialogue/Characters/Bouncer.yarn deleted file mode 100644 index 6b75374..0000000 --- a/NightclubArcadia/Assets/Dialogue/Characters/Bouncer.yarn +++ /dev/null @@ -1,65 +0,0 @@ -// Everything the bouncer ever says. -// -// These nodes share a title, which makes them a node group. Callers just -// `<>` and Yarn runs the most specific variation whose -// `when:` conditions pass. Adding a new variation later means adding a node, -// not editing a growing <> 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. - <> - <> - <> - Bouncer: ...you're not on the list. - Bouncer: But it's slow tonight. Go on. - <> - <> - Bouncer: Nobody's on the list tonight. Especially not you. - <> - <> --> Let me through. <= 2>> - <> - <> - Bouncer: ...yeah. Yeah, okay. - <> - <> - <> - Bouncer: Fine. Don't make it a thing. - <> - <> - Bouncer: Try that again and you're talking to the pavement. - <> - <> - <> --> Actually, I'll head home. - Bouncer: Smart choice. - <> -=== - -title: Bouncer_Talk -when: $is_vip ---- -Bouncer: Didn't even see you there. Go on in. -<> -=== - -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. -<> -=== - -title: Bouncer_Talk -when: always ---- -Bouncer: Back again. -<> -=== diff --git a/NightclubArcadia/Assets/Dialogue/Common.yarn b/NightclubArcadia/Assets/Dialogue/Common.yarn index 0bffedf..04f92d9 100644 --- a/NightclubArcadia/Assets/Dialogue/Common.yarn +++ b/NightclubArcadia/Assets/Dialogue/Common.yarn @@ -18,16 +18,6 @@ title: Declarations // How much the door staff likes you. <> -// Set when the player has tried the "I'm on the list" line at least once. -<> - -// The bouncer's verdict, read by the scene that called him. -<> - -// Smart variable: derived, not stored. Can never drift out of sync with -// $reputation, so there's nothing to remember to update. -< 10>> - // Written by <> (YarnSkillCommands). Never <> these by hand. <> <> @@ -56,12 +46,16 @@ title: Declarations <> <> <> + <> <> <> <> + <> <> +<> +<> <> <> @@ -79,6 +73,8 @@ title: Declarations <> <> <> +<> +<> // 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. <> <> + +// 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 <> by hand. Same real name always +// yields the same alias; the alias itself bears no resemblance to the input. +<> === diff --git a/NightclubArcadia/Assets/Dialogue/Scenes/Entrance.yarn b/NightclubArcadia/Assets/Dialogue/Scenes/Entrance.yarn deleted file mode 100644 index 7e71d6d..0000000 --- a/NightclubArcadia/Assets/Dialogue/Scenes/Entrance.yarn +++ /dev/null @@ -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 <> -// 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. -<> -<> - <> -<> - <> -<> -=== - -title: Entrance_Inside ---- -Narrator: The bass hits you like a wall. -=== - -title: Entrance_Outside ---- -Narrator: The night swallows you back up. -=== diff --git a/NightclubArcadia/Assets/Dialogue/Scenes/Entrance.yarn.meta b/NightclubArcadia/Assets/Dialogue/Scenes/Entrance.yarn.meta deleted file mode 100644 index b27b2c4..0000000 --- a/NightclubArcadia/Assets/Dialogue/Scenes/Entrance.yarn.meta +++ /dev/null @@ -1,10 +0,0 @@ -fileFormatVersion: 2 -guid: cb0faaa47fe2440d78e49de30f921218 -ScriptedImporter: - internalIDToNameTable: [] - externalObjects: {} - serializedVersion: 2 - userData: - assetBundleName: - assetBundleVariant: - script: {fileID: 11500000, guid: 94073015eacc34c1d8fc6786e43d60ca, type: 3} diff --git a/NightclubArcadia/Assets/Scenes/DialogueTest.unity b/NightclubArcadia/Assets/Scenes/DialogueTest.unity index 9238699..b732e9b 100644 --- a/NightclubArcadia/Assets/Scenes/DialogueTest.unity +++ b/NightclubArcadia/Assets/Scenes/DialogueTest.unity @@ -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: diff --git a/NightclubArcadia/Assets/Scripts/Dialogue/AliasNameGenerator.cs b/NightclubArcadia/Assets/Scripts/Dialogue/AliasNameGenerator.cs new file mode 100644 index 0000000..c75b672 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Dialogue/AliasNameGenerator.cs @@ -0,0 +1,61 @@ +using Yarn.Unity; + +namespace NightclubArcadia.Dialogue +{ + /// + /// 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)} + /// + 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; + } + } +} diff --git a/NightclubArcadia/Assets/Scripts/Dialogue/AliasNameGenerator.cs.meta b/NightclubArcadia/Assets/Scripts/Dialogue/AliasNameGenerator.cs.meta new file mode 100644 index 0000000..ae140fe --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Dialogue/AliasNameGenerator.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ee985664120c84b41921806fadeccb34 \ No newline at end of file diff --git a/NightclubArcadia/Assets/Scripts/Dialogue/DialogueInteractable.cs b/NightclubArcadia/Assets/Scripts/Dialogue/DialogueInteractable.cs index 17378eb..d9ac31f 100644 --- a/NightclubArcadia/Assets/Scripts/Dialogue/DialogueInteractable.cs +++ b/NightclubArcadia/Assets/Scripts/Dialogue/DialogueInteractable.cs @@ -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(); - 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); } } } diff --git a/NightclubArcadia/Assets/Scripts/Dialogue/DialogueTransitionController.cs b/NightclubArcadia/Assets/Scripts/Dialogue/DialogueTransitionController.cs index 74c57de..50f543d 100644 --- a/NightclubArcadia/Assets/Scripts/Dialogue/DialogueTransitionController.cs +++ b/NightclubArcadia/Assets/Scripts/Dialogue/DialogueTransitionController.cs @@ -5,8 +5,14 @@ using Yarn.Unity; namespace NightclubArcadia.Dialogue { /// - /// Registers the <> 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 <> 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. /// 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(); + playerInputs = playerArmature.GetComponent(); + playerAnimator = playerArmature.GetComponent(); + } + + // 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(); - - // Dialogue is in progress as soon as this controller is active (including - // auto-started nodes), so the player stays locked out until <>. - // 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 <>. 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 <> 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; } } } diff --git a/NightclubArcadia/Assets/Scripts/Dialogue/DialogueUIVisibility.cs b/NightclubArcadia/Assets/Scripts/Dialogue/DialogueUIVisibility.cs new file mode 100644 index 0000000..a7c66af --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Dialogue/DialogueUIVisibility.cs @@ -0,0 +1,67 @@ +using UnityEngine; + +namespace NightclubArcadia.Dialogue +{ + /// + /// 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. + /// + public static class DialogueUIVisibility + { + /// + /// 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. + /// + public static bool Show(GameObject dialogueUI) => SetVisible(dialogueUI, true); + + /// + /// Hides the dialogue UI, leaving the hierarchy active. See class docs. + /// + 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(); + if (group == null) + { + group = dialogueUI.AddComponent(); + } + + group.alpha = visible ? 1f : 0f; + group.interactable = visible; + group.blocksRaycasts = visible; + + return wasReactivated; + } + } +} diff --git a/NightclubArcadia/Assets/Scripts/Dialogue/DialogueUIVisibility.cs.meta b/NightclubArcadia/Assets/Scripts/Dialogue/DialogueUIVisibility.cs.meta new file mode 100644 index 0000000..5b882b3 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Dialogue/DialogueUIVisibility.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ad5dcf93105bd49b287dbd19bc42cef7 \ No newline at end of file diff --git a/NightclubArcadia/Assets/Scripts/NPCs.meta b/NightclubArcadia/Assets/Scripts/NPCs.meta new file mode 100644 index 0000000..043b7d4 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/NPCs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2216657f3a0224501bcfa09add14f689 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/NPCs/InputDeviceTracker.cs b/NightclubArcadia/Assets/Scripts/NPCs/InputDeviceTracker.cs new file mode 100644 index 0000000..ddfc2cd --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/NPCs/InputDeviceTracker.cs @@ -0,0 +1,48 @@ +using UnityEngine; +using UnityEngine.InputSystem; +using UnityEngine.InputSystem.LowLevel; + +/// +/// 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". +/// +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() && !eventPtr.IsA()) 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; + } + } +} diff --git a/NightclubArcadia/Assets/Scripts/NPCs/InputDeviceTracker.cs.meta b/NightclubArcadia/Assets/Scripts/NPCs/InputDeviceTracker.cs.meta new file mode 100644 index 0000000..e89ef00 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/NPCs/InputDeviceTracker.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 941f7ff9039984f729ceddebd57d4159 \ No newline at end of file diff --git a/NightclubArcadia/Assets/Scripts/NPCs/NPCStandIn.cs b/NightclubArcadia/Assets/Scripts/NPCs/NPCStandIn.cs new file mode 100644 index 0000000..7410e4a --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/NPCs/NPCStandIn.cs @@ -0,0 +1,199 @@ +using UnityEngine; +using UnityEngine.InputSystem; +using TMPro; +using Yarn.Unity; +using NightclubArcadia.Dialogue; + +/// +/// 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). +/// +[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(); + 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("/f"); + interactAction.AddBinding("/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(); + 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(); + 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 + // <>. 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); + } + + /// + /// 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. + /// + GameObject ResolveDialogueUI() + { + if (dialogueUI != null) + { + return dialogueUI; + } + + var canvas = dialogueRunner.GetComponentInChildren(true); + if (canvas == null) + { + Debug.LogWarning($"[NPCStandIn] No Canvas found under {dialogueRunner.name} for {npcName}"); + return null; + } + + dialogueUI = canvas.gameObject; + return dialogueUI; + } +} diff --git a/NightclubArcadia/Assets/Scripts/NPCs/NPCStandIn.cs.meta b/NightclubArcadia/Assets/Scripts/NPCs/NPCStandIn.cs.meta new file mode 100644 index 0000000..66d604d --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/NPCs/NPCStandIn.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 23d0ac85377c640a1900cfd13e89c389 \ No newline at end of file