Improved UI textbox

This commit is contained in:
2026-06-12 11:56:30 -05:00
parent f6a0bb156e
commit 2f3a4eab66
39 changed files with 574 additions and 706 deletions
+4 -5
View File
@@ -1,7 +1,6 @@
class_name BattleCutsceneAction
const DialogueAction = preload("res://cutscene/dialogue/DialogueAction.gd")
static var NARRATION:DialogueResource = preload("res://dialogue/battle/narration.dialogue")
const _NARRATION_BASE:String = "res://dialogue/battle/narration"
# State object passed as extra_game_states so {{variable}} tokens resolve in the dialogue file.
class BattleNarrationState:
@@ -29,7 +28,7 @@ static func battleDecisionCallable(params:Dictionary) -> int:
var cutscene:Cutscene = params['cutscene']
cutscene.addCallable(
DialogueAction.getDialogueCallable(NARRATION, 'move_perform', [state]).merged(
DialogueAction.getDialogueCallable(_NARRATION_BASE, 'move_perform', [state], DialogueAction.DialogueMode.NARRATION).merged(
{'position': Cutscene.CUTSCENE_ADD_NEXT}, false
)
)
@@ -58,7 +57,7 @@ static func playerDecisionCallable(params:Dictionary) -> int:
if allPlayersDead:
params['cutscene'].addCallable(
DialogueAction.getDialogueCallable(NARRATION, 'battle_defeat').merged(
DialogueAction.getDialogueCallable(_NARRATION_BASE, 'battle_defeat', [], DialogueAction.DialogueMode.NARRATION).merged(
{'position': Cutscene.CUTSCENE_ADD_NEXT}, false
)
)
@@ -66,7 +65,7 @@ static func playerDecisionCallable(params:Dictionary) -> int:
if allEnemiesDead:
params['cutscene'].addCallable(
DialogueAction.getDialogueCallable(NARRATION, 'battle_victory').merged(
DialogueAction.getDialogueCallable(_NARRATION_BASE, 'battle_victory', [], DialogueAction.DialogueMode.NARRATION).merged(
{'position': Cutscene.CUTSCENE_ADD_NEXT}, false
)
)
+56 -38
View File
@@ -1,55 +1,73 @@
class_name DialogueAction
# Runs a .dialogue file through the VNTextbox and returns CUTSCENE_CONTINUE when
# the last line is dismissed. Mutations in the .dialogue file are executed
# automatically by DialogueManager before the line is returned.
#
# extra_game_states: additional objects/dicts whose properties and methods are
# accessible inside the .dialogue file (alongside all autoloads).
const _TextboxGd = preload("res://ui/component/DialogueTextbox.gd")
const _ChoiceBoxGd = preload("res://ui/component/DialogueChoiceBox.gd")
enum DialogueMode {
CONVERSATION, # blocks movement, player advances
NARRATION, # non-blocking, player advances
AMBIENT, # non-blocking, timed auto-advance
}
static func dialogueCallable(params:Dictionary) -> int:
assert(params.has('resource'))
var resource:DialogueResource = params['resource']
assert(params.has('basePath'))
var basePath:String = params['basePath']
var title:String = params.get('title', 'start')
var extraStates:Array = params.get('extraStates', [])
var characterTargets:Dictionary = params.get('characterTargets', {})
var mode:DialogueMode = params.get('mode', DialogueMode.CONVERSATION)
var resource:DialogueResource = _loadLocaleResource(basePath)
assert(resource != null, "DialogueAction: could not load resource for path: " + basePath)
if mode == DialogueMode.CONVERSATION:
UI.activeConversation = true
var advancementMode:int = (
_TextboxGd.AdvancementMode.TIMED
if mode == DialogueMode.AMBIENT
else _TextboxGd.AdvancementMode.PLAYER
)
UI.dialogueActive = true
var line:DialogueLine = await DialogueManager.get_next_dialogue_line(resource, title, extraStates)
while line != null:
var target:Node3D = _getLineTarget(line.character, characterTargets)
var text:String = line.text
var entity:Entity = OVERWORLD.getEntityByDialogueName(line.character)
if line.responses.size() > 0:
# Show text then auto-pick the first allowed response.
# Replace this block with a real response UI when branching dialogue is needed.
await UI.MAIN_CHATBOX.setTextAndWait(text, target)
var nextId:String = ""
for response:DialogueResponse in line.responses:
if response.is_allowed:
nextId = response.next_id
break
line = await DialogueManager.get_next_dialogue_line(resource, nextId, extraStates)
var textbox = _TextboxGd.SCENE.instantiate()
UI.chatBoxContainer.add_child(textbox)
textbox.setup(line, entity, advancementMode)
await textbox.dismissed
var allowedResponses:Array = line.responses.filter(func(r): return r.is_allowed)
if allowedResponses.size() > 0:
var playerEntity:Entity = OVERWORLD.getPlayerEntity()
var choiceBox = _ChoiceBoxGd.SCENE.instantiate()
UI.chatBoxContainer.add_child(choiceBox)
choiceBox.setup(line.responses, playerEntity)
var chosen:DialogueResponse = await choiceBox.chosen
line = await DialogueManager.get_next_dialogue_line(resource, chosen.next_id, extraStates)
else:
await UI.MAIN_CHATBOX.setTextAndWait(text, target)
line = await DialogueManager.get_next_dialogue_line(resource, line.next_id, extraStates)
UI.dialogueActive = false
if mode == DialogueMode.CONVERSATION:
UI.activeConversation = false
return Cutscene.CUTSCENE_CONTINUE
# "player" (case-insensitive) maps to the player entity; any other named
# character maps to the npc entity; empty character (narrator) uses no target.
static func _getLineTarget(character:String, characterTargets:Dictionary) -> Node3D:
if characterTargets.is_empty() or character == "":
return null
if character.to_lower() == "player":
return characterTargets.get("player", null)
return characterTargets.get("npc", null)
static func _loadLocaleResource(basePath:String) -> DialogueResource:
var lang:String = TranslationServer.get_locale().left(2)
var localePath:String = basePath + "." + lang + ".dialogue"
if ResourceLoader.exists(localePath):
return load(localePath)
var fallback:String = basePath + ".en.dialogue"
if ResourceLoader.exists(fallback):
return load(fallback)
return null
static func getDialogueCallable(resource:DialogueResource, title:String = 'start', extraStates:Array = [], characterTargets:Dictionary = {}) -> Dictionary:
static func getDialogueCallable(basePath:String, title:String = "start", extraStates:Array = [], mode:DialogueMode = DialogueMode.CONVERSATION) -> Dictionary:
return {
'function': dialogueCallable,
'resource': resource,
'title': title,
'extraStates': extraStates,
'characterTargets': characterTargets,
"function": dialogueCallable,
"basePath": basePath,
"title": title,
"extraStates": extraStates,
"mode": mode,
}
+8 -7
View File
@@ -1,23 +1,24 @@
class_name ItemAction
const DialogueAction = preload("res://cutscene/dialogue/DialogueAction.gd")
# Passed as extra_game_states so {{item_name}} and {{quantity}} resolve in the .dialogue file.
class ItemDialogueState:
var item_name:String
var quantity:int
func _init(name:String, qty:int) -> void:
item_name = name
func _init(itemName:String, qty:int) -> void:
item_name = itemName
quantity = qty
static var PICKUP_DIALOGUE:DialogueResource = preload("res://dialogue/item/pickup.dialogue")
static func itemGetCallable(params:Dictionary) -> int:
assert(params.has('stack'))
var stack:ItemStack = params['stack']
PARTY.BACKPACK.addStack(stack)
var state = ItemDialogueState.new(Item.getItemName(stack.item), stack.quantity)
var dialogueParams:Dictionary = DialogueAction.getDialogueCallable(PICKUP_DIALOGUE, 'start', [state])
var dialogueParams:Dictionary = DialogueAction.getDialogueCallable(
"res://dialogue/item/pickup",
"start",
[state],
DialogueAction.DialogueMode.CONVERSATION
)
dialogueParams['position'] = Cutscene.CUTSCENE_ADD_NEXT
params['cutscene'].addCallable(dialogueParams)
return Cutscene.CUTSCENE_CONTINUE