Dialog System
Multi-turn conversations, current_line advance, period lookup, typed arrays, and dialog bugs we hit.
On this page
- What we built
- Core UI state machine
- Why advance then show
- Preventing empty dialog crashes
- Per-line speaker
- Where data lives
- Bug: typed Array assignment
- Symptom
- Cause
- Fix
- Lesson
- Bug: first line feels like it repeats / second line missing
- Symptom
- Cause (two we hit / guarded against)
- Fix
- Lesson
- Bug: empty array index
- Symptom
- Fix
- Extending later
E advances current_line; Q quits. Portrait swaps for player vs guest via is_player.What we built
Dialog is not a 2D dialog[station][hour] table anymore. It is an ordered list of turns (player / guest), looked up by time-of-day period (morning / afternoon / evening / night) plus a one-shot checkin conversation.
Data flow:
- Guest JSON →
GuestLoader→GuestData.dialog(GuestDialog) Guest.get_dialog()picks conversation viaGuestDialog.get_conversation(period, day, possessed)- Turns render to
Array[Dictionary]withspeaker,text,is_player dialog_box.start_dialog(lines, portrait)shows one line at a time
Core UI state machine
From scripts/dialog_box.gd:
var lines: Array[Dictionary] = []
var current_line := 0
var is_open := false
func start_dialog(dialog_lines: Array, portrait: Texture2D = null) -> void:
if dialog_lines.is_empty():
return
lines.assign(dialog_lines) # typed Array copy — see bug below
current_line = 0
is_open = true
visible = true
show_current_line()
func _unhandled_input(event: InputEvent) -> void:
if not is_open:
return
if event.is_action_pressed("dialog_quit"):
close_dialog()
elif event.is_action_pressed("interact"):
current_line += 1
if current_line >= lines.size():
close_dialog()
else:
show_current_line()
get_viewport().set_input_as_handled()
Why advance then show
Index starts at 0. First open calls show_current_line() immediately. Each E increments before showing the next line. End condition is current_line >= lines.size().
Wrong pattern (off-by-one / stuck on first line):
# BAD: increment after show, or show then check size incorrectly
show_current_line()
current_line += 1
If you show before incrementing and also show on the same press that opened dialog without consuming input, the first line can appear to “repeat”.
Preventing empty dialog crashes
Always guard:
if dialog_lines.is_empty():
return
Guest.get_dialog() falls back to fallback_lines so the UI never indexes an empty array.
Per-line speaker
Earlier design passed one fixed speaker string into start_dialog(speaker, lines: Array[String]). Now each line owns its speaker:
lines.append({"speaker": speaker, "text": turn.text, "is_player": is_player})
show_current_line() updates both labels and swaps portrait (clerk vs guest).
Where data lives
| Layer | Location |
|---|---|
| Authored text | resources/guests/*.json |
| Runtime resources | DialogConversation / DialogTurn |
| Selection | GuestDialog.get_conversation |
| Presentation | dialog_box.gd |
Lookup prefers matching is_corrupted == possessed, else falls back to the non-corrupted conversation for that period.
Bug: typed Array assignment
Symptom
Passing a plain Array / Array[Dictionary] into a typed Array[Dictionary] field with = can error or silently fail depending on Godot version / inference.
Cause
GDScript typed arrays are strict. lines = dialog_lines is not always a valid typed assignment.
Fix
lines.assign(dialog_lines)
Lesson
Prefer .assign() when filling Array[T] from a loosely typed parameter.
Bug: first line feels like it repeats / second line missing
Symptom
lines.size() prints correctly (e.g. 4) but pressing E shows line 1 again or skips oddly.
Cause (two we hit / guarded against)
- Same frame double-handle: Guest opens dialog on
interact, dialog box also advances oninteractin the same frame → immediatecurrent_line += 1or double show. - Index shown before reset: Reopening without setting
current_line = 0.
Fix
- Guest calls
set_input_as_handled()after opening. - Dialog box also marks handled while open.
start_dialogalways resetscurrent_line = 0.- Guest only opens when
not dialog_box.is_open.
Lesson
One action, one consumer per frame. Use is_open as a mutex.
Bug: empty array index
Symptom
Crash or blank when lookup returns nothing.
Fix
Empty guard in start_dialog; fallback lines on the guest.
Extending later
Recommended next time (not fully built):
- Branching choices (arrays of replies)
- Typewriter / RichText BBCode effects
- Voice beep per character
- Logging
heard_factswhen a turn has anid(resource field exists onDialogTurn)
heard_facts exists on GameState but is not deeply wired through every turn yet — treat as partial.