Example Scripts

Complete scripts taken from the V6.9 test set. Each example includes the scene setup it expects, the engine features it teaches, and source you can copy or download.

1.

FPS Display

FPSDisplay.moducpp

Updates a UI text object every tick and optionally caps the game at 120 FPS.

This shipped sample still derives from ModuBehaviour, which remains compatible. New V6.9 scripts should generally derive from ModuNode.

FPSDisplay.moducpp
ModuCPP
1add ModuCPP;
2add ModuEngine;
3public class FPSDisplay : ModuBehaviour {
4 public bool clampTo120 = false;
5 void TickUpdate() {SetFPSCap(clampTo120, 120.0f); obj.UILabel = "FPS: " + IntR(ModuEngine.FPS);}
6}
2.

Top-Down 2D Movement

TopDownMovement2D.moducpp

Moves a 2D character with normalized WASD input, directional animation clips, sprinting, acceleration, drag, and footsteps.

TopDownMovement2D.moducpp
ModuCPP
1add ModuCPP;
2add ModuInput;
3add ModuEngine;
4
5public enum FacingDirection {Down, Up, Right, Left}
6public class TopDownMovement2D : ModuNode
7{
8 [Header("Top Down Movement 2D")]
9 [Slider(0.0f, 50.0f)] public float walkSpeed = 4.0f;
10 [Slider(0.0f, 80.0f)] public float runSpeed = 7.0f;
11 [Slider(0.0f, 200.0f)] public float acceleration = 18.0f;
12 [Slider(0.0f, 200.0f)] public float drag = 8.0f;
13 [Slider(1.0f, 30.0f)] public float animationFps = 10.0f;
14 [Slider(1.0f, 60.0f)] public float runAnimationFps = 14.0f;
15 [Slider(0.01f, 10.0f)] public float movementThreshold = 0.15f;
16 [ClipGridPair]
17 private int[4] idleClips;
18 private int[4][4] walkClips;
19 [Separator]
20 [SoundSet("Footsteps")]
21 private string[6] stepSounds;
22 private FacingDirection facing = FacingDirection.Down;
23 private float animationTime = 0.0f;
24 private int lastWalkFrame = -1;
25 private int nextFootstepIndex = 0;
26 void Begin() to ResetAnimationState();
27 void TickUpdate()
28 {
29 if (dt <= 0.0f) return;
30 Vector2 move = input.WASDNormalized(); bool running = input.sprint();
31 float speed = running ? runSpeed : walkSpeed;
32 Vector2 targetVelocity = move * speed; Vector2 actualVelocity = targetVelocity;
33 TryMove(targetVelocity, dt, ref actualVelocity);
34 Vector2 motion = move.Dot(move) > 0.000001f ? targetVelocity : actualVelocity;
35 TickAnimation(motion, dt, running);
36 }
37 void TryMove(Vector2 targetVelocity, float dt, ref Vector2 actualVelocity)
38 {
39 actualVelocity = targetVelocity;
40 TryMoveRigidbody2D(ctx, targetVelocity, acceleration, drag, dt, ref actualVelocity);
41 }
42 void TickAnimation(Vector2 motion, float dt, bool running)
43 {
44 if (motion.Dot(motion) > movementThreshold * movementThreshold) {ApplyWalkingAnimation(motion, dt, running); return;}
45 ApplyIdleAnimation();
46 }
47 void ApplyWalkingAnimation(Vector2 motion, float dt, bool running)
48 {
49 facing = ResolveFacing(motion, facing); animationTime += dt;
50 float fps = Math.Max(1.0f, running ? runAnimationFps : animationFps);
51 int frame = (int)(animationTime * fps) % 4;
52 int clip = SelectWalkClip(frame); if (clip >= 0) sprite.SetClip(clip);
53 PlayFootstepsBetweenFrames(lastWalkFrame, frame); lastWalkFrame = frame;
54 }
55 void ApplyIdleAnimation()
56 {
57 animationTime = 0.0f; lastWalkFrame = -1;
58 int clip = idleClips[FacingIndex(facing)]; if (IsValidClip(clip)) sprite.SetClip(clip);
59 }
60 void PlayFootstepsBetweenFrames(int previousFrame, int currentFrame)
61 {
62 int probe = previousFrame >= 0 ? previousFrame : 3;
63 while (probe != currentFrame) {probe = (probe + 1) % 4; if (probe == 0 || probe == 2) PlayRandomFootstep();}
64 }
65 void PlayRandomFootstep()
66 {
67 int[6] validIndices; int count = 0;
68 for (int i = 0; i < 6; i++) if (!stepSounds[i].IsEmpty()) validIndices[count++] = i;
69 if (count == 0) return;
70 int selected = validIndices[nextFootstepIndex % count];
71 nextFootstepIndex = (nextFootstepIndex + 1) % count;
72 audio.PlayOneShot(stepSounds[selected]);
73 }
74 int SelectWalkClip(int frame)
75 {
76 int dir = FacingIndex(facing);
77 int[4] clips = walkClips[dir];
78 int candidate = clips[Math.Clamp(frame, 0, 3)];
79 if (IsValidClip(candidate)) return candidate;
80 for (int i = 0; i < 4; i++) if (IsValidClip(clips[i])) return clips[i];
81 candidate = idleClips[dir];
82 return IsValidClip(candidate) ? candidate : -1;
83 }
84 void ResetAnimationState() {facing = FacingDirection.Down; animationTime = 0.0f; lastWalkFrame = -1; nextFootstepIndex = 0;}
85 int FacingIndex(FacingDirection direction) to (int)direction;
86 bool IsValidClip(int clip) to clip >= 0 && clip < ctx.GetSpriteClipCount();
87 FacingDirection ResolveFacing(Vector2 motion, FacingDirection fallback)
88 {
89 if (motion.Dot(motion) <= 0.000001f) return fallback;
90 if (Math.Abs(motion.x) > Math.Abs(motion.y)) return motion.x >= 0.0f ? FacingDirection.Right : FacingDirection.Left;
91 return motion.y >= 0.0f ? FacingDirection.Up : FacingDirection.Down;
92 }
93}
3.

Standalone 3D Movement

StandaloneMovementController.moducpp

A compact camera-relative 3D controller with acceleration, sprinting, ground checks, facing, and jumping.

StandaloneMovementController.moducpp
ModuCPP
1add ModuCPP;
2add ModuEngine;
3add ModuInput;
4public class PlayerMovement : ModuNode {
5 public SceneObj camera;
6 public float walkSpeed = 4.0f;
7 public float sprintSpeed = 7.0f;
8 public float acceleration = 20.0f;
9 public float deceleration = 25.0f;
10 [Header("Ground Check & Jump")]
11 public float groundCheckDistance = 0.1f;
12 public float jumpForce = 6.5f;
13 private float verticalVelocity = 0.0f;
14 void Begin() { Ensure.obj; Ensure.Rigidbody3D(obj); }
15 void TickUpdate() {
16 Vector2 move = Input.WASDMovement();
17 Vector3 direction = Movement.Direction(move, camera);
18 float speed = Input.ButtonHeld("Sprint") ? sprintSpeed : walkSpeed;
19 bool grounded = obj.Physics.IsGrounded(groundCheckDistance);
20 obj.Rigidbody3D.Accelerate(direction, speed, acceleration, deceleration);
21 obj.Transform.Face(direction);
22 if (grounded) { verticalVelocity = 0.0f; if (Input.ButtonDown("Jump")) then verticalVelocity = jumpForce; }
23 else then verticalVelocity = obj.Rigidbody3D.VelocityY;
24 obj.Rigidbody3D.AddVelocityY(verticalVelocity);
25 }
26}
5.

Dialogue System

DialogueSystem.moducpp

A localized typewriter dialogue runtime with per-line audio, text effects, mouth states, object toggles, and open/close animation hooks.

DialogueSystem.moducpp
ModuCPP
1add ModuCPP;
2add ModuCPP.Experimental;
3#include "DialoguePortShared.h"
4using namespace DialoguePort;
5
6public enum MouthState {TalkingOpen, TalkingClosed, NotTalking}
7public class DialogueSystem : ModuNode
8{
9 [Header("Bindings")]
10 [ObjectRef] public string characterNameTextRef;
11 [ObjectRef] public string dialogueTextRef;
12 [ObjectRef] public string playerRef;
13 public Language currentLanguage = Language.English;
14
15 [Header("Timing")]
16 [Slider(0.0f, 2.0f)] public float pitchVariation = 0.1f;
17 [Slider(0.0f, 10.0f)] public float openAnimationDelay = 0.3f;
18 [Slider(0.0f, 10.0f)] public float closeAnimationDelay = 0.3f;
19 [Slider(0.0f, 2.0f)] public float spacingFactor = 0.1f;
20 [Slider(0.1f, 10.0f)] public float sizeMultiplier = 2.0f;
21
22 [Header("Audio")]
23 public string characterSoundClip;
24 public string triggerSoundClip;
25 public string enterSoundClip;
26 public string exitSoundClip;
27 public string skipSoundClip;
28
29 [Header("Flags")]
30 public bool rigidSimulated = true;
31 public bool disableSelfOnEnd = true;
32 public bool autoOpenOnBegin = false;
33 [ObjectList] public string[] itemsToEnable;
34 [ObjectList] public string[] itemsToDisable;
35
36 [Header("Dialogue")]
37 public DialoguePort.DialogueLine[] dialogueLines;
38
39 private bool running = false;
40 private bool isTyping = false;
41 private bool isTextFullyDisplayed = false;
42 private bool autoOpened = false;
43 private int index = 0;
44 private size_t revealedCharacters = 0;
45 private float typeAccumulator = 0.0f;
46 private float holdDelay = 0.0f;
47 private float effectTimer = 0.0f;
48 private float soundCooldown = 0.0f;
49 private float openDelayRemaining = 0.0f;
50 private float closeDelayRemaining = 0.0f;
51 private bool pendingClose = false;
52 private int lastInteractionRequestSerial = 0;
53 private bool prevSubmitDown = false;
54 private string currentCleanSentence;
55 private string currentPlayerRef;
56 private DialoguePort.DialogueLine[] activeLines;
57 private string[] endItemsToEnable;
58 private string[] endItemsToDisable;
59
60 inspector
61 {
62 Tabs {
63 Tab("Bindings") {
64 AutoFields(characterNameTextRef, dialogueTextRef, playerRef);
65 Run(DrawLanguageCombo("Language", currentLanguage));
66 }
67
68 Tab("Timing") {
69 AutoFields(pitchVariation, openAnimationDelay, closeAnimationDelay, spacingFactor, sizeMultiplier);
70 }
71
72 Tab("Audio") {
73 AutoFields(characterSoundClip, triggerSoundClip, enterSoundClip, exitSoundClip, skipSoundClip);
74 }
75
76 Tab("Flags") {
77 AutoFields(rigidSimulated, disableSelfOnEnd, autoOpenOnBegin, itemsToEnable, itemsToDisable);
78 }
79
80 Tab("Dialogue") {
81 AutoFields(dialogueLines);
82 }
83
84 Tab("Runtime") {
85 Run(ImGui::TextDisabled("Running: %s", running ? "Yes" : "No"));
86 Run(ImGui::TextDisabled("Typing: %s", isTyping ? "Yes" : "No"));
87 Run(ImGui::TextDisabled("Line: %d", index + 1));
88 Run(ImGui::TextDisabled("Chars: %zu / %zu", revealedCharacters, currentCleanSentence.size()));
89 }
90 }
91 }
92
93 void Begin()
94 {
95 if (!ctx.object) return;
96 ResetRuntimeState();
97 TryAutoOpenOnBegin();
98 }
99
100 void TickUpdate()
101 {
102 if (!ctx.object) return;
103
104 const float step = Math.Max(0.0f, dt);
105 ConsumeInteractionRequest();
106 TryAutoOpenOnBegin();
107 if (!running) return;
108
109 if (TickPendingClose(step)) return;
110
111 const bool submitPressed = ConsumeSubmitPressed();
112 if (TickOpenDelay(step)) return;
113
114 tickTyping(step);
115 if (submitPressed) {
116 HandleSubmitPressed();
117 }
118 }
119
120 void ResetRuntimeState()
121 {
122 running = false;
123 isTyping = false;
124 isTextFullyDisplayed = false;
125 autoOpened = false;
126 index = 0;
127 revealedCharacters = 0;
128 typeAccumulator = 0.0f;
129 holdDelay = 0.0f;
130 effectTimer = 0.0f;
131 soundCooldown = 0.0f;
132 openDelayRemaining = 0.0f;
133 closeDelayRemaining = 0.0f;
134 pendingClose = false;
135 prevSubmitDown = false;
136 const int interactionSerial = ParseInt(ctx.GetSetting(kInteractionRequestSerial, "0"), 0);
137 const bool interactionPending = ParseBool(ctx.GetSetting(kInteractionRequestPending, "0"), false);
138 lastInteractionRequestSerial = interactionPending ? (interactionSerial - 1) : interactionSerial;
139 currentCleanSentence.clear();
140 currentPlayerRef = playerRef;
141 activeLines.clear();
142 endItemsToEnable.clear();
143 endItemsToDisable.clear();
144 resetTextWidgets();
145 }
146
147 void TryAutoOpenOnBegin()
148 {
149 if (running || autoOpened) return;
150 if (!autoOpenOnBegin || dialogueLines.empty()) return;
151
152 openDialogue(dialogueLines, itemsToEnable, itemsToDisable, playerRef);
153 autoOpened = true;
154 }
155
156 void ConsumeInteractionRequest()
157 {
158 const int interactionSerial = ParseInt(ctx.GetSetting(kInteractionRequestSerial, "0"), 0);
159 const bool interactionPending = ParseBool(ctx.GetSetting(kInteractionRequestPending, "0"), false);
160 if (interactionSerial == lastInteractionRequestSerial && !interactionPending) return;
161
162 lastInteractionRequestSerial = interactionSerial;
163 if (interactionPending) {
164 ctx.SetSetting(kInteractionRequestPending, "0");
165 }
166
167 DialoguePort.DialogueLine[] overrideLines = DeserializeDialogueLines(ctx.GetSetting(kInteractionOverrideLines, ""));
168 string[] overrideEnable = DeserializeObjectRefs(ctx.GetSetting(kInteractionEndEnable, ""));
169 string[] overrideDisable = DeserializeObjectRefs(ctx.GetSetting(kInteractionEndDisable, ""));
170 std::string overridePlayerRef = ctx.GetSetting(kInteractionPlayerRef, "");
171
172 if (overrideLines.empty()) overrideLines = dialogueLines;
173 if (overrideEnable.empty()) overrideEnable = itemsToEnable;
174 if (overrideDisable.empty()) overrideDisable = itemsToDisable;
175 if (overridePlayerRef.empty()) overridePlayerRef = playerRef;
176
177 openDialogue(overrideLines, overrideEnable, overrideDisable, overridePlayerRef);
178 }
179
180 bool TickPendingClose(float step)
181 {
182 if (!pendingClose) return false;
183
184 closeDelayRemaining -= step;
185 if (closeDelayRemaining <= 0.0f) {
186 finalizeDialogueEnd();
187 }
188 return true;
189 }
190
191 bool ConsumeSubmitPressed()
192 {
193 const bool submitDown = IsSubmitDown();
194 const bool submitPressed = submitDown && !prevSubmitDown;
195 prevSubmitDown = submitDown;
196 return submitPressed;
197 }
198
199 bool TickOpenDelay(float step)
200 {
201 if (openDelayRemaining <= 0.0f) return false;
202
203 openDelayRemaining -= step;
204 if (openDelayRemaining <= 0.0f) {
205 if (!enterSoundClip.empty()) {
206 PlaySoundClip(enterSoundClip);
207 }
208 startLine();
209 }
210 return true;
211 }
212
213 void HandleSubmitPressed()
214 {
215 if (isTyping) {
216 completeTyping();
217 if (!skipSoundClip.empty()) {
218 PlaySoundClip(skipSoundClip);
219 }
220 return;
221 }
222
223 if (!isTextFullyDisplayed) return;
224
225 if (index < static_cast<int>(activeLines.size()) - 1) {
226 ++index;
227 if (!enterSoundClip.empty()) {
228 PlaySoundClip(enterSoundClip);
229 }
230 startLine();
231 return;
232 }
233
234 if (!exitSoundClip.empty()) {
235 PlaySoundClip(exitSoundClip);
236 }
237 beginDialogueClose();
238 }
239
240 void resetTextWidgets()
241 {
242 SetUITextLabel(ctx, characterNameTextRef, "");
243 SetUITextLabel(ctx, dialogueTextRef, "");
244 SetUITextEffects(ctx, dialogueTextRef, TextEffectType.None, 1.0f, 0.0f);
245 }
246
247 void revealDialogueText()
248 {
249 const size_t shown = std::min(revealedCharacters, currentCleanSentence.size());
250 SetUITextLabel(ctx, dialogueTextRef, currentCleanSentence.substr(0, shown));
251 }
252
253 void startLine()
254 {
255 if (index < 0 || index >= static_cast<int>(activeLines.size())) {
256 running = false;
257 isTyping = false;
258 isTextFullyDisplayed = false;
259 return;
260 }
261
262 const DialogueLine& line = activeLines[static_cast<size_t>(index)];
263 SetObjectsEnabledState(ctx, line.itemsToDisable, false);
264 SetObjectsEnabledState(ctx, line.itemsToEnable, true);
265
266 SetUITextLabel(ctx, characterNameTextRef, line.characterName);
267
268 currentCleanSentence = ParseSentenceForDisplay(GetSentenceForLanguage(line, currentLanguage));
269 revealedCharacters = 0;
270 typeAccumulator = 0.0f;
271 holdDelay = 0.0f;
272 soundCooldown = 0.0f;
273 isTyping = true;
274 isTextFullyDisplayed = false;
275
276 revealDialogueText();
277 SetUITextEffects(ctx, dialogueTextRef, line.textEffect, line.animationSpeed, line.effectIntensity);
278 applyMouthState(line, MouthState.TalkingClosed);
279 }
280
281 void completeTyping()
282 {
283 if (index < 0 || index >= static_cast<int>(activeLines.size())) return;
284
285 const DialogueLine& line = activeLines[static_cast<size_t>(index)];
286 revealedCharacters = currentCleanSentence.size();
287 isTyping = false;
288 isTextFullyDisplayed = true;
289 holdDelay = 0.0f;
290 typeAccumulator = 0.0f;
291
292 revealDialogueText();
293 applyMouthState(line, MouthState.NotTalking);
294 }
295
296 void finalizeDialogueEnd()
297 {
298 SetObjectsEnabledState(ctx, endItemsToDisable, false);
299 SetObjectsEnabledState(ctx, endItemsToEnable, true);
300 SetRigidbody2DSimulated(ctx, currentPlayerRef, rigidSimulated);
301
302 resetTextWidgets();
303
304 running = false;
305 pendingClose = false;
306 closeDelayRemaining = 0.0f;
307 isTyping = false;
308 isTextFullyDisplayed = false;
309 revealedCharacters = 0;
310 currentCleanSentence.clear();
311 if (disableSelfOnEnd) {
312 autoOpened = false;
313 ctx.SetObjectEnabled(false);
314 }
315 }
316
317 void beginDialogueClose()
318 {
319 if (pendingClose) return;
320 pendingClose = true;
321 closeDelayRemaining = std::max(0.0f, closeAnimationDelay);
322 TryPlayAnimationClipNamed(ctx, "DialogueStateClose");
323
324 if (closeDelayRemaining <= 0.0f) {
325 finalizeDialogueEnd();
326 }
327 }
328
329 void openDialogue(ref DialoguePort.DialogueLine[] lines,
330 ref string[] itemsToEnableOnEnd,
331 ref string[] itemsToDisableOnEnd,
332 const std::string& playerRefOverride)
333 {
334 if (lines.empty()) {
335 ctx.AddConsoleMessage("DialogueSystem: no dialogue lines configured.", ConsoleMessageType.Warning);
336 return;
337 }
338
339 activeLines = lines;
340 endItemsToEnable = itemsToEnableOnEnd;
341 endItemsToDisable = itemsToDisableOnEnd;
342 currentPlayerRef = playerRefOverride.empty() ? playerRef : playerRefOverride;
343
344 running = true;
345 index = 0;
346 revealedCharacters = 0;
347 typeAccumulator = 0.0f;
348 holdDelay = 0.0f;
349 soundCooldown = 0.0f;
350 effectTimer = 0.0f;
351 isTyping = false;
352 isTextFullyDisplayed = false;
353 pendingClose = false;
354 closeDelayRemaining = 0.0f;
355 currentCleanSentence.clear();
356 openDelayRemaining = openAnimationDelay;
357
358 resetTextWidgets();
359 SetRigidbody2DSimulated(ctx, currentPlayerRef, false);
360 TryPlayAnimationClipNamed(ctx, "DialogueStateOpen");
361
362 if (!triggerSoundClip.empty()) {
363 PlaySoundClip(triggerSoundClip);
364 }
365
366 if (openDelayRemaining <= 0.0f) {
367 startLine();
368 }
369 }
370
371 void tickTyping(float step)
372 {
373 if (!isTyping) return;
374 if (index < 0 || index >= static_cast<int>(activeLines.size())) return;
375
376 const DialogueLine& line = activeLines[static_cast<size_t>(index)];
377
378 if (currentCleanSentence.empty()) {
379 completeTyping();
380 return;
381 }
382
383 const float typingSpeed = std::max(0.001f, line.typingSpeed);
384 effectTimer += step;
385 soundCooldown = std::max(0.0f, soundCooldown - step);
386
387 float remaining = step;
388 while (remaining > 0.0f && isTyping) {
389 if (holdDelay > 0.0f) {
390 const float consume = std::min(remaining, holdDelay);
391 holdDelay -= consume;
392 remaining -= consume;
393 continue;
394 }
395
396 typeAccumulator += remaining;
397 remaining = 0.0f;
398
399 bool revealedAny = false;
400 while (typeAccumulator >= typingSpeed && revealedCharacters < currentCleanSentence.size()) {
401 typeAccumulator -= typingSpeed;
402 ++revealedCharacters;
403 revealedAny = true;
404
405 const size_t charIndex = revealedCharacters - 1;
406 const char character = currentCleanSentence[charIndex];
407
408 if (std::isalnum(static_cast<unsigned char>(character)) != 0) {
409 const std::string& clip = line.characterSoundClip.empty() ? characterSoundClip : line.characterSoundClip;
410 if (!clip.empty() && soundCooldown <= 0.0f) {
411 PlaySoundClip(clip);
412 soundCooldown = std::max(0.01f, typingSpeed * 0.6f);
413 }
414 }
415
416 if ((charIndex % 2U) == 0U) {
417 applyMouthState(line, MouthState.TalkingOpen);
418 } else {
419 applyMouthState(line, MouthState.TalkingClosed);
420 }
421
422 if (character == ',' || character == ';') {
423 holdDelay += typingSpeed * 3.0f;
424 } else if (character == '.' || character == '!' || character == '?') {
425 if (character == '.' && charIndex + 2 < currentCleanSentence.size() &&
426 currentCleanSentence[charIndex + 1] == '.' &&
427 currentCleanSentence[charIndex + 2] == '.') {
428 holdDelay += typingSpeed * 5.0f;
429 } else {
430 holdDelay += typingSpeed * 5.0f;
431 }
432 }
433 }
434
435 if (revealedAny) {
436 revealDialogueText();
437 }
438
439 if (revealedCharacters >= currentCleanSentence.size()) {
440 completeTyping();
441 break;
442 }
443
444 if (typeAccumulator < typingSpeed) {
445 break;
446 }
447 }
448 }
449
450 void applyMouthState(const DialogueLine& line, MouthState mouthState)
451 {
452 const bool isOpen = (mouthState == MouthState.TalkingOpen);
453 const bool isClosed = (mouthState == MouthState.TalkingClosed);
454 const bool isIdle = (mouthState == MouthState.NotTalking);
455
456 auto openObj = ResolveObject(line.openMouthObjectRef);
457 auto closedObj = ResolveObject(line.closedMouthObjectRef);
458 auto idleObj = ResolveObject(line.notTalkingObjectRef);
459
460 SetObjectEnabledState(ctx, openObj, isOpen);
461 SetObjectEnabledState(ctx, closedObj, isClosed);
462 SetObjectEnabledState(ctx, idleObj, isIdle);
463 }
464}
6.

Interactable Object

InteractableObject.moducpp

Handles E-key interactions, optional player range checks, selection visuals, one-time use, object toggles, and dialogue handoff.

InteractableObject.moducpp
ModuCPP
1add ModuCPP;
2add ModuEngine;
3add ModuInput;
4add ModuCPP.Experimental;
5#include "DialoguePortShared.h"
6using namespace DialoguePort;
7
8public enum InteractableType {Dialogue, ToggleObjects}
9
10SubScript InteractionOption
11{
12 public string optionName = "New Option";
13 public int interactionType = 0;
14 public string dialogueSystemRef;
15 public DialoguePort.DialogueLine[] dialogueLines;
16 public string[] dialogueItemsToEnableOnEnd;
17 public string[] dialogueItemsToDisableOnEnd;
18 public string[] itemsToEnable;
19 public string[] itemsToDisable;
20}
21
22public class InteractableObject : ModuNode
23{
24 [Header("Basics")]
25 public bool canInteract = true;
26 public bool oneTimeUse = false;
27 public bool interactOnKeyPress = true;
28 public bool requirePlayerInRange = false;
29 [Slider(0.0f, 100.0f)] public float interactDistance = 2.5f;
30 public bool debugRange = false;
31 [ObjectRef] public string playerRef;
32 public string selectionNameOverride;
33
34 [Header("Selection")]
35 public bool isSelected = false;
36 [ObjectList] public string[] selectedStateEnable;
37 [ObjectList] public string[] selectedStateDisable;
38
39 [Header("Interaction")]
40 public int selectedOptionIndex = 0;
41 public InteractionOption[] options;
42
43 private bool prevInteractDown = false;
44 private bool hasSelectionState = false;
45 private bool lastSelectionState = false;
46 private float lastRangeDistance = -1.0f;
47 private bool lastRangeInRange = false;
48 private bool lastRangeHasPlayer = false;
49 private vec3 lastPlayerWorldPos = vec3(0.0f);
50 private vec3 lastSelfWorldPos = vec3(0.0f);
51
52 inspector
53 {
54 Tabs {
55 Tab("Basics") {
56 AutoFields(canInteract, oneTimeUse, interactOnKeyPress, requirePlayerInRange,
57 interactDistance, debugRange, playerRef, selectionNameOverride);
58 Run(DrawInteractNowButton());
59 }
60
61 Tab("Selection") {
62 AutoFields(isSelected, selectedStateEnable, selectedStateDisable);
63 Run(DrawSelectionNameInfo());
64 }
65
66 Tab("Options") {
67 AutoFields(selectedOptionIndex, options);
68 }
69
70 Tab("Runtime") {
71 Run(DrawRuntimeStatus());
72 }
73 }
74 }
75
76 void Begin()
77 {
78 if (!ctx.object) return;
79
80 ResetRuntimeState();
81 MigrateLegacyOptionsIfNeeded();
82 applySelectedState();
83 }
84
85 void TickUpdate()
86 {
87 if (!ctx.object) return;
88
89 SyncSelectedState();
90 isPlayerInRange();
91
92 if (!interactOnKeyPress || !ConsumeInteractPressed()) {
93 return;
94 }
95
96 interact();
97 }
98
99 void ResetRuntimeState()
100 {
101 prevInteractDown = false;
102 hasSelectionState = true;
103 lastSelectionState = isSelected;
104 lastRangeDistance = -1.0f;
105 lastRangeInRange = false;
106 lastRangeHasPlayer = false;
107 lastPlayerWorldPos = vec3(0.0f);
108 lastSelfWorldPos = ctx.object ? GetObjectReferencePosition(ctx, *ctx.object) : vec3(0.0f);
109 }
110
111 void MigrateLegacyOptionsIfNeeded()
112 {
113 if (!options.empty()) return;
114
115 string legacyBlob = ctx.GetSetting("optionsBlob", "");
116 if (legacyBlob.empty()) return;
117
118 options = DeserializeSubScriptArray<InteractionOption>(legacyBlob);
119 if (options.empty()) return;
120
121 ctx.SetSetting("options", SerializeSubScriptArray(options));
122 ctx.SetSetting("optionsBlob", "");
123 // ctx.SetSetting already marks dirty; no manual MarkDirty needed here.
124 }
125
126 void SyncSelectedState()
127 {
128 if (hasSelectionState && lastSelectionState == isSelected) {
129 return;
130 }
131 applySelectedState();
132 hasSelectionState = true;
133 lastSelectionState = isSelected;
134 }
135
136 bool ConsumeInteractPressed()
137 {
138 const bool interactDown = isInteractDown();
139 const bool interactPressed = interactDown && !prevInteractDown;
140 prevInteractDown = interactDown;
141 return interactPressed;
142 }
143
144 bool interact()
145 {
146 if (!canInteract || options.empty()) {
147 return false;
148 }
149
150 if (!isPlayerInRange()) {
151 if (requirePlayerInRange && debugRange) {
152 if (lastRangeHasPlayer) {
153 char msg[256];
154 std::snprintf(msg, sizeof(msg),
155 "InteractableObject: out of range (distance %.2f > %.2f).",
156 lastRangeDistance,
157 Math.Max(0.0f, interactDistance));
158 ctx.AddConsoleMessage(msg, ConsoleMessageType::Info);
159 } else {
160 ctx.AddConsoleMessage("InteractableObject: player reference is missing or unresolved for range check.",
161 ConsoleMessageType::Warning);
162 }
163 }
164 return false;
165 }
166
167 const int optionIndex = Math.Clamp(selectedOptionIndex, 0, (int)options.size() - 1);
168 selectedOptionIndex = optionIndex;
169 const InteractionOption option = options[optionIndex];
170
171 const bool executed = executeOption(option);
172 if (executed && oneTimeUse) {
173 canInteract = false;
174 ctx.SetSetting("canInteract", "0");
175 }
176
177 return executed;
178 }
179
180 bool executeOption(InteractionOption option)
181 {
182 if (option.interactionType == 0) {
183 return executeDialogueOption(option);
184 }
185
186 SetObjectsEnabledState(ctx, option.itemsToEnable, true);
187 SetObjectsEnabledState(ctx, option.itemsToDisable, false);
188 return true;
189 }
190
191 bool executeDialogueOption(InteractionOption option)
192 {
193 auto dialogueObject = ResolveObject(option.dialogueSystemRef);
194 if (!dialogueObject) {
195 ctx.AddConsoleMessage("InteractableObject: dialogue option is missing a valid DialogueSystem object reference.",
196 ConsoleMessageType::Warning);
197 return false;
198 }
199
200 const DialogueScriptTarget dialogueTarget = FindDialogueScriptTarget(ctx, dialogueObject);
201 if (!dialogueTarget.script || !dialogueTarget.object) {
202 ctx.AddConsoleMessage("InteractableObject: target DialogueSystem object has no DialogueSystem script in its hierarchy.",
203 ConsoleMessageType::Warning);
204 return false;
205 }
206
207 bool changed = false;
208 changed |= WriteScriptSetting(dialogueTarget.script, kInteractionRequestPending, "1");
209 changed |= WriteScriptSetting(dialogueTarget.script, kInteractionOverrideLines,
210 SerializeDialogueLines(option.dialogueLines));
211 changed |= WriteScriptSetting(dialogueTarget.script, kInteractionEndEnable,
212 SerializeObjectRefs(option.dialogueItemsToEnableOnEnd));
213 changed |= WriteScriptSetting(dialogueTarget.script, kInteractionEndDisable,
214 SerializeObjectRefs(option.dialogueItemsToDisableOnEnd));
215 changed |= WriteScriptSetting(dialogueTarget.script, kInteractionPlayerRef, playerRef);
216
217 const int currentSerial = ParseInt(GetScriptSetting(dialogueTarget.script, kInteractionRequestSerial, "0"), 0);
218 changed |= WriteScriptSetting(dialogueTarget.script, kInteractionRequestSerial, std::to_string(currentSerial + 1));
219 changed |= SetObjectEnabledState(ctx, dialogueTarget.object, true);
220
221 // WriteScriptSetting auto-dirties on change; SetObjectEnabledState dirties internally.
222 (void)changed;
223
224 return true;
225 }
226
227 void applySelectedState()
228 {
229 SetObjectsEnabledState(ctx, selectedStateEnable, isSelected);
230 SetObjectsEnabledState(ctx, selectedStateDisable, !isSelected);
231 }
232
233 bool isInteractDown()
234 {
235 return IsRuntimeKeyDown(GLFW_KEY_E, ImGuiKey_E);
236 }
237
238 bool isPlayerInRange()
239 {
240 if (!ctx.object) return false;
241
242 auto player = ResolveObject(playerRef);
243 if (!player) {
244 lastRangeHasPlayer = false;
245 lastRangeInRange = false;
246 lastRangeDistance = -1.0f;
247 lastSelfWorldPos = GetObjectReferencePosition(ctx, *ctx.object);
248 lastPlayerWorldPos = vec3(0.0f);
249 return !requirePlayerInRange;
250 }
251
252 const vec3 selfPos = GetObjectReferencePosition(ctx, *ctx.object);
253 const vec3 playerPos = GetObjectReferencePosition(ctx, *player);
254 const vec3 delta = playerPos - selfPos;
255 const float range = Math.Max(0.0f, interactDistance);
256 const float distanceSq = glm::dot(delta, delta);
257 const bool inRange = distanceSq <= (range * range);
258
259 lastRangeHasPlayer = true;
260 lastRangeInRange = inRange;
261 lastRangeDistance = std::sqrt(Math.Max(0.0f, distanceSq));
262 lastSelfWorldPos = selfPos;
263 lastPlayerWorldPos = playerPos;
264
265 return !requirePlayerInRange || inRange;
266 }
267
268 void DrawSelectionNameInfo()
269 {
270 ImGui::TextDisabled("Selection Name: %s", GetCurrentObjectName(ctx, "Interactable").c_str());
271 }
272
273 bool DrawInteractNowButton()
274 {
275 return ImGui::Button("Interact Now") && interact();
276 }
277
278 void DrawRuntimeStatus()
279 {
280 ImGui::Separator();
281 ImGui::TextUnformatted("Runtime");
282 ImGui::TextDisabled("Can Interact: %s", canInteract ? "Yes" : "No");
283 ImGui::TextDisabled("Options: %zu", options.size());
284 ImGui::TextDisabled("Selected Option: %d", selectedOptionIndex);
285 if (!requirePlayerInRange && !debugRange) return;
286 if (!lastRangeHasPlayer) {
287 ImGui::TextDisabled("Range: player not found");
288 return;
289 }
290
291 ImGui::TextDisabled("Range Distance: %.2f / %.2f", lastRangeDistance, Math.Max(0.0f, interactDistance));
292 ImGui::TextDisabled("In Range: %s", lastRangeInRange ? "Yes" : "No");
293 if (!debugRange) return;
294 ImGui::TextDisabled("Self Pos: (%.2f, %.2f, %.2f)", lastSelfWorldPos.x, lastSelfWorldPos.y, lastSelfWorldPos.z);
295 ImGui::TextDisabled("Player Pos: (%.2f, %.2f, %.2f)", lastPlayerWorldPos.x, lastPlayerWorldPos.y, lastPlayerWorldPos.z);
296 }
297}
Shared

Shared Dialogue Header

DialoguePortShared.h

Sample infrastructure shared by DialogueSystem and InteractableObject. Keep it in the same script directory as both — it is not attached to a scene object.

DialoguePortShared.h
C++
1#pragma once
2
3#include "ModuCPPScriptApi.h"
4#include "ModuInputScriptApi.h"
5#include "ModuEngineScriptApi.h"
6#include "ModuCPPExperimentalScriptApi.h"
7
8#include <algorithm>
9#include <array>
10#include <cmath>
11#include <cctype>
12#include <regex>
13#include <string>
14#include <unordered_map>
15#include <vector>
16
17#include <GLFW/glfw3.h>
18
19namespace DialoguePort {
20
21enum class Language {
22 English = 0,
23 German = 1,
24 JapaneseKana = 2
25};
26
27enum class TextEffectType : int {
28 None = 0,
29 Wave = 1 << 0,
30 Shake = 1 << 1,
31 Bounce = 1 << 2,
32 Rotate = 1 << 3,
33 Fade = 1 << 4
34};
35
36inline constexpr const char* kInteractionRequestPending = "interaction.requestPending";
37inline constexpr const char* kInteractionOverrideLines = "interaction.overrideDialogueLines";
38inline constexpr const char* kInteractionEndEnable = "interaction.itemsToEnableOnEnd";
39inline constexpr const char* kInteractionEndDisable = "interaction.itemsToDisableOnEnd";
40inline constexpr const char* kInteractionPlayerRef = "interaction.playerRef";
41inline constexpr const char* kInteractionRequestSerial = "interaction.requestSerial";
42
43struct DialogueLine {
44 std::string characterName;
45 std::string sentence;
46 std::string sentenceGerman;
47 std::string sentenceJapaneseKana;
48 std::string characterSoundClip;
49 float typingSpeed = 0.03f;
50 TextEffectType textEffect = TextEffectType::None;
51 float animationSpeed = 1.0f;
52 float effectIntensity = 1.0f;
53 std::string notTalkingObjectRef;
54 std::string openMouthObjectRef;
55 std::string closedMouthObjectRef;
56 std::vector<std::string> itemsToEnable;
57 std::vector<std::string> itemsToDisable;
58};
59
60using ::ModuCPP::Trim;
61using ::ModuCPP::ParseInt;
62using ::ModuCPP::ParseFloat;
63using ::ModuCPP::ParseBool;
64using ::ModuCPP::GetScriptSetting;
65using ::ModuCPP::SetScriptSetting;
66using ::ModuCPP::EscapeField;
67using ::ModuCPP::UnescapeField;
68using ::ModuCPP::SplitEscaped;
69using ::ModuCPP::JoinEscaped;
70using ::ModuCPP::DeserializeObjectRefs;
71using ::ModuCPP::SerializeObjectRefs;
72
73inline std::string SerializeDialogueLines(const std::vector<DialogueLine>& lines) {
74 std::vector<std::string> encodedLines;
75 encodedLines.reserve(lines.size());
76 for (const DialogueLine& line : lines) {
77 std::vector<std::string> fields;
78 fields.reserve(14);
79 fields.push_back(line.characterName);
80 fields.push_back(line.sentence);
81 fields.push_back(line.sentenceGerman);
82 fields.push_back(line.sentenceJapaneseKana);
83 fields.push_back(line.characterSoundClip);
84 fields.push_back(std::to_string(line.typingSpeed));
85 fields.push_back(std::to_string(static_cast<int>(line.textEffect)));
86 fields.push_back(std::to_string(line.animationSpeed));
87 fields.push_back(std::to_string(line.effectIntensity));
88 fields.push_back(line.notTalkingObjectRef);
89 fields.push_back(line.openMouthObjectRef);
90 fields.push_back(line.closedMouthObjectRef);
91 fields.push_back(SerializeObjectRefs(line.itemsToEnable));
92 fields.push_back(SerializeObjectRefs(line.itemsToDisable));
93 encodedLines.push_back(JoinEscaped(fields, '|'));
94 }
95 // Use tab as the outer delimiter so values stay single-line in scene files.
96 return JoinEscaped(encodedLines, '\t');
97}
98
99inline std::vector<DialogueLine> DeserializeDialogueLines(const std::string& encoded) {
100 std::vector<DialogueLine> lines;
101 if (encoded.empty()) return lines;
102
103 std::vector<std::string> rawLines = SplitEscaped(encoded, '\t');
104 // Backward compatibility for older saved data that used newline delimiters.
105 if (rawLines.size() <= 1 && encoded.find('\t') == std::string::npos &&
106 encoded.find('\n') != std::string::npos) {
107 rawLines = SplitEscaped(encoded, '\n');
108 }
109 lines.reserve(rawLines.size());
110
111 for (const std::string& rawLine : rawLines) {
112 if (Trim(rawLine).empty()) continue;
113 std::vector<std::string> fields = SplitEscaped(rawLine, '|');
114 if (fields.empty()) continue;
115
116 DialogueLine line;
117 if (fields.size() > 0) line.characterName = fields[0];
118 if (fields.size() > 1) line.sentence = fields[1];
119 if (fields.size() > 2) line.sentenceGerman = fields[2];
120 if (fields.size() > 3) line.sentenceJapaneseKana = fields[3];
121 if (fields.size() > 4) line.characterSoundClip = fields[4];
122 if (fields.size() > 5) line.typingSpeed = ParseFloat(fields[5], line.typingSpeed);
123 if (fields.size() > 6) line.textEffect = static_cast<TextEffectType>(ParseInt(fields[6], 0));
124 if (fields.size() > 7) line.animationSpeed = ParseFloat(fields[7], line.animationSpeed);
125 if (fields.size() > 8) line.effectIntensity = ParseFloat(fields[8], line.effectIntensity);
126 if (fields.size() > 9) line.notTalkingObjectRef = fields[9];
127 if (fields.size() > 10) line.openMouthObjectRef = fields[10];
128 if (fields.size() > 11) line.closedMouthObjectRef = fields[11];
129 if (fields.size() > 12) line.itemsToEnable = DeserializeObjectRefs(fields[12]);
130 if (fields.size() > 13) line.itemsToDisable = DeserializeObjectRefs(fields[13]);
131
132 lines.push_back(std::move(line));
133 }
134
135 return lines;
136}
137
138inline std::string ToLowerAscii(std::string value) {
139 std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
140 return static_cast<char>(std::tolower(c));
141 });
142 return value;
143}
144
145inline std::string GetSentenceForLanguage(const DialogueLine& line, Language language) {
146 switch (language) {
147 case Language::English:
148 return line.sentence;
149 case Language::German:
150 return line.sentenceGerman.empty() ? line.sentence : line.sentenceGerman;
151 case Language::JapaneseKana:
152 return line.sentenceJapaneseKana.empty() ? line.sentence : line.sentenceJapaneseKana;
153 default:
154 return line.sentence;
155 }
156}
157
158inline std::string ParseSentenceForDisplay(const std::string& sentence) {
159 static const std::regex taggedWordPattern(R"(\(\[(\w+),\s*([\d.]+),\s*([\d.]+),\](.*?)\))");
160
161 std::string clean;
162 clean.reserve(sentence.size());
163
164 size_t lastIndex = 0;
165 for (std::sregex_iterator it(sentence.begin(), sentence.end(), taggedWordPattern), end; it != end; ++it) {
166 const std::smatch& match = *it;
167 const size_t matchPos = static_cast<size_t>(match.position());
168 const size_t matchLen = static_cast<size_t>(match.length());
169
170 if (matchPos > lastIndex) {
171 clean += sentence.substr(lastIndex, matchPos - lastIndex);
172 }
173
174 if (match.size() >= 5) {
175 clean += match[4].str();
176 }
177
178 lastIndex = matchPos + matchLen;
179 }
180
181 if (lastIndex < sentence.size()) {
182 clean += sentence.substr(lastIndex);
183 }
184
185 return clean;
186}
187
188using ::ModuCPP::MakeObjectRef;
189using ::ModuCPP::ResolveSceneObjectRef;
190
191inline bool IsDialogueSystemScript(const ScriptComponent& script) {
192 const std::string pathLower = ToLowerAscii(script.path);
193 const std::string managedTypeLower = ToLowerAscii(script.managedType);
194 return pathLower.find("dialoguesystem") != std::string::npos ||
195 managedTypeLower.find("dialoguesystem") != std::string::npos;
196}
197
198struct DialogueScriptTarget {
199 SceneObject* object = nullptr;
200 ScriptComponent* script = nullptr;
201};
202
203inline ScriptComponent* FindDialogueSystemScriptOnObject(SceneObject* object) {
204 if (!object) return nullptr;
205
206 for (ScriptComponent& script : object->scripts) {
207 if (IsDialogueSystemScript(script)) {
208 return &script;
209 }
210 }
211
212 return nullptr;
213}
214
215inline DialogueScriptTarget FindDialogueScriptTarget(ScriptContext& ctx, SceneObject* object) {
216 if (!object) return {};
217
218 if (ScriptComponent* direct = FindDialogueSystemScriptOnObject(object)) {
219 return DialogueScriptTarget{object, direct};
220 }
221
222 std::vector<int> pendingChildIds = object->childIds;
223 int childGuard = 0;
224 while (!pendingChildIds.empty() && childGuard < 512) {
225 const int childId = pendingChildIds.back();
226 pendingChildIds.pop_back();
227 ++childGuard;
228 SceneObject* child = ctx.FindObjectById(childId);
229 if (!child) continue;
230 if (ScriptComponent* childScript = FindDialogueSystemScriptOnObject(child)) {
231 return DialogueScriptTarget{child, childScript};
232 }
233 pendingChildIds.insert(pendingChildIds.end(), child->childIds.begin(), child->childIds.end());
234 }
235
236 int parentId = object->parentId;
237 int parentGuard = 0;
238 while (parentId >= 0 && parentGuard < 256) {
239 ++parentGuard;
240 SceneObject* parent = ctx.FindObjectById(parentId);
241 if (!parent) break;
242 if (ScriptComponent* parentScript = FindDialogueSystemScriptOnObject(parent)) {
243 return DialogueScriptTarget{parent, parentScript};
244 }
245 parentId = parent->parentId;
246 }
247
248 return {};
249}
250
251using ::ModuCPP::SetObjectEnabledState;
252using ::ModuCPP::SetObjectsEnabledState;
253using ::ModuCPP::GetObjectReferencePosition;
254using ::ModuCPP::GetCurrentObjectName;
255using ::ModuCPP::TryPlayAnimationClipNamed;
256using ::ModuCPP::SetUITextLabel;
257
258inline bool SetUITextEffects(ScriptContext& ctx,
259 const std::string& objectRef,
260 TextEffectType effect,
261 float animationSpeed,
262 float effectIntensity) {
263 return ModuCPP::SetUITextEffects(ctx, objectRef, static_cast<int>(effect), animationSpeed, effectIntensity);
264}
265
266using ::ModuCPP::SetRigidbody2DSimulated;
267using ::ModuCPP::DrawStdStringInput;
268using ::ModuCPP::DrawObjectRefInput;
269using ::ModuCPP::DrawAudioClipInput;
270using ::ModuCPP::DrawObjectRefListEditor;
271using ::ModuCPP::IsRuntimeKeyDown;
272using ::ModuCPP::IsSubmitDown;
273
274inline const char* LanguageLabel(Language language) {
275 switch (language) {
276 case Language::English: return "English";
277 case Language::German: return "German";
278 case Language::JapaneseKana: return "Japanese Kana";
279 default: return "English";
280 }
281}
282
283inline bool DrawLanguageCombo(const char* label, Language& language) {
284 bool changed = false;
285 if (ImGui::BeginCombo(label, LanguageLabel(language))) {
286 std::array<Language, 3> values = {
287 Language::English,
288 Language::German,
289 Language::JapaneseKana
290 };
291 for (Language value : values) {
292 const bool selected = (language == value);
293 if (ImGui::Selectable(LanguageLabel(value), selected)) {
294 language = value;
295 changed = true;
296 }
297 if (selected) ImGui::SetItemDefaultFocus();
298 }
299 ImGui::EndCombo();
300 }
301 return changed;
302}
303
304inline void DrawTextEffectFlagsEditor(TextEffectType& effect) {
305 int bits = static_cast<int>(effect);
306 auto drawToggle = [&](const char* label, TextEffectType flag, bool sameLineAfter = false) {
307 bool enabled = (bits & static_cast<int>(flag)) != 0;
308 if (ImGui::Checkbox(label, &enabled)) {
309 if (enabled) bits |= static_cast<int>(flag);
310 else bits &= ~static_cast<int>(flag);
311 }
312 if (sameLineAfter) ImGui::SameLine();
313 };
314
315 drawToggle("Wave", TextEffectType::Wave, true);
316 drawToggle("Shake", TextEffectType::Shake, true);
317 drawToggle("Bounce", TextEffectType::Bounce);
318 drawToggle("Rotate", TextEffectType::Rotate, true);
319 drawToggle("Fade", TextEffectType::Fade);
320 effect = static_cast<TextEffectType>(bits);
321}
322
323inline bool DrawDialogueLineToolbar(std::vector<DialogueLine>& lines,
324 int& selectedIndex,
325 const char* addLabel,
326 const char* removeLabel) {
327 bool changed = false;
328 if (ImGui::Button(addLabel)) {
329 lines.push_back(lines.empty() ? DialogueLine{} : lines.back());
330 selectedIndex = static_cast<int>(lines.size()) - 1;
331 changed = true;
332 }
333 ImGui::SameLine();
334 if (ImGui::Button(removeLabel) &&
335 selectedIndex >= 0 &&
336 selectedIndex < static_cast<int>(lines.size())) {
337 lines.erase(lines.begin() + static_cast<std::ptrdiff_t>(selectedIndex));
338 selectedIndex = lines.empty()
339 ? -1
340 : std::clamp(selectedIndex, 0, static_cast<int>(lines.size()) - 1);
341 changed = true;
342 }
343 return changed;
344}
345
346inline void DrawDialogueLineList(const std::vector<DialogueLine>& lines,
347 int& selectedIndex,
348 const char* childId) {
349 ImGui::BeginChild(childId, ImVec2(230.0f, 220.0f), true);
350 for (size_t i = 0; i < lines.size(); ++i) {
351 std::string label = std::to_string(i + 1) + ". " +
352 (lines[i].characterName.empty() ? std::string("<Unnamed>") : lines[i].characterName);
353 if (ImGui::Selectable(label.c_str(), selectedIndex == static_cast<int>(i))) {
354 selectedIndex = static_cast<int>(i);
355 }
356 }
357 ImGui::EndChild();
358}
359
360inline bool DrawDialogueLineEditor(ScriptContext& ctx,
361 std::vector<DialogueLine>& lines,
362 int& selectedIndex,
363 const char* addLabel = "Add Line",
364 const char* removeLabel = "Remove Line",
365 const char* emptyText = "No dialogue lines configured.",
366 const char* childId = "DialogueLinesList") {
367 bool changed = DrawDialogueLineToolbar(lines, selectedIndex, addLabel, removeLabel);
368 if (lines.empty()) {
369 ImGui::TextDisabled("%s", emptyText);
370 return changed;
371 }
372
373 selectedIndex = std::clamp(selectedIndex, 0, static_cast<int>(lines.size()) - 1);
374 DrawDialogueLineList(lines, selectedIndex, childId);
375
376 ImGui::SameLine();
377 ImGui::BeginGroup();
378 DialogueLine& line = lines[static_cast<size_t>(selectedIndex)];
379
380 changed |= DrawStdStringInput("Character Name", line.characterName, 256);
381 changed |= DrawStdStringInput("Sentence (EN)", line.sentence, 2048, 0, true, 60.0f);
382 changed |= DrawStdStringInput("Sentence (DE)", line.sentenceGerman, 2048, 0, true, 60.0f);
383 changed |= DrawStdStringInput("Sentence (JP Kana)", line.sentenceJapaneseKana, 2048, 0, true, 60.0f);
384
385 changed |= DrawAudioClipInput("Character Voice Clip", line.characterSoundClip, 512);
386 changed |= ImGui::DragFloat("Typing Speed", &line.typingSpeed, 0.001f, 0.001f, 1.0f, "%.3f");
387 changed |= ImGui::DragFloat("Animation Speed", &line.animationSpeed, 0.01f, 0.01f, 10.0f, "%.2f");
388 changed |= ImGui::DragFloat("Effect Intensity", &line.effectIntensity, 0.01f, 0.0f, 10.0f, "%.2f");
389
390 ImGui::TextUnformatted("Text Effects");
391 const TextEffectType before = line.textEffect;
392 DrawTextEffectFlagsEditor(line.textEffect);
393 changed |= (before != line.textEffect);
394
395 changed |= DrawObjectRefInput(ctx, "Not Talking Object", line.notTalkingObjectRef);
396 changed |= DrawObjectRefInput(ctx, "Open Mouth Object", line.openMouthObjectRef);
397 changed |= DrawObjectRefInput(ctx, "Closed Mouth Object", line.closedMouthObjectRef);
398 changed |= DrawObjectRefListEditor(ctx, "Line Items To Enable", line.itemsToEnable);
399 changed |= DrawObjectRefListEditor(ctx, "Line Items To Disable", line.itemsToDisable);
400
401 ImGui::EndGroup();
402 return changed;
403}
404
405template <typename RuntimeStateT>
406inline void DrawDialogueRuntimeStatus(const RuntimeStateT* state) {
407 if (!state) {
408 ImGui::TextDisabled("Runtime: idle");
409 return;
410 }
411 ImGui::Separator();
412 ImGui::TextUnformatted("Runtime");
413 ImGui::TextDisabled("Running: %s", state->running ? "Yes" : "No");
414 ImGui::TextDisabled("Typing: %s", state->isTyping ? "Yes" : "No");
415 ImGui::TextDisabled("Line: %d", state->index + 1);
416 ImGui::TextDisabled("Chars: %zu / %zu",
417 state->revealedCharacters,
418 state->currentCleanSentence.size());
419}
420
421} // namespace DialoguePort