D-061 compliant UI skeleton: NPC speech top, max 3 response options below, left-aligned. Insert-styled colors (white-blue, amber hover, green pressed). WASD walk-away fades over 300ms, no close button. InsertOverlay (CanvasLayer 10, z-layer 6). Mock data in next commit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
128 lines
3.9 KiB
GDScript
128 lines
3.9 KiB
GDScript
extends Control
|
|
|
|
# Dialogue box — D-061: bottom screen, max 20% height, no portraits.
|
|
# InsertOverlay (CanvasLayer 10, z-layer 6) — diegetic, insert-styled.
|
|
# NPC speech top, player response options below, left-aligned.
|
|
# Max 3 visible options. No close button — walk-away (WASD) or option select only.
|
|
# Sprint 7: UI skeleton with mock data. Server wiring deferred to #305.
|
|
|
|
signal option_selected(index: int, text: String)
|
|
signal dialogue_dismissed # Walk-away or conversation end
|
|
|
|
@onready var panel: PanelContainer = $PanelContainer
|
|
@onready var npc_speech: RichTextLabel = $PanelContainer/MarginContainer/VBoxContainer/NpcSpeech
|
|
@onready var options_container: VBoxContainer = $PanelContainer/MarginContainer/VBoxContainer/OptionsContainer
|
|
|
|
var _is_showing: bool = false
|
|
var _active_tween: Tween = null
|
|
var _option_buttons: Array[Button] = []
|
|
var _npc_name: String = ""
|
|
|
|
const FADE_IN: float = 0.2
|
|
const FADE_OUT: float = 0.3 # D-064: 300ms fade on walk-away
|
|
const MAX_OPTIONS: int = 3 # D-061: max 3 response options visible
|
|
|
|
|
|
func _ready() -> void:
|
|
panel.modulate.a = 0.0
|
|
visible = false
|
|
_is_showing = false
|
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
if not _is_showing:
|
|
return
|
|
|
|
# D-064: WASD during dialogue → walk-away, 300ms fade
|
|
if event is InputEventKey and event.pressed:
|
|
if (event.is_action_pressed("move_north")
|
|
or event.is_action_pressed("move_south")
|
|
or event.is_action_pressed("move_east")
|
|
or event.is_action_pressed("move_west")
|
|
or event.is_action_pressed("move_northeast")
|
|
or event.is_action_pressed("move_southeast")
|
|
or event.is_action_pressed("move_southwest")
|
|
or event.is_action_pressed("move_northwest")):
|
|
hide_dialogue()
|
|
dialogue_dismissed.emit()
|
|
|
|
|
|
# Show dialogue with NPC speech and response options.
|
|
# npc_name: who is speaking (displayed as prefix)
|
|
# speech: the NPC's dialogue text
|
|
# options: Array of Strings — player response choices (max 3 shown)
|
|
func show_dialogue(npc_name: String, speech: String, options: Array = []) -> void:
|
|
_npc_name = npc_name
|
|
|
|
# NPC speech — name prefix in bold
|
|
if npc_name.is_empty():
|
|
npc_speech.text = speech
|
|
else:
|
|
npc_speech.text = "[b]%s:[/b] %s" % [npc_name, speech]
|
|
|
|
# Clear old option buttons
|
|
_clear_options()
|
|
|
|
# Build response option buttons (max 3)
|
|
var count := mini(options.size(), MAX_OPTIONS)
|
|
for i in range(count):
|
|
var btn := Button.new()
|
|
btn.text = options[i]
|
|
btn.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
|
btn.flat = true
|
|
btn.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
|
# Insert-styled: geometric, minimal
|
|
btn.add_theme_color_override("font_color", Color("#c8d0e0"))
|
|
btn.add_theme_color_override("font_hover_color", Color("#e8c547"))
|
|
btn.add_theme_color_override("font_pressed_color", Color("#6bc9a6"))
|
|
var idx := i
|
|
btn.pressed.connect(func(): _on_option_pressed(idx))
|
|
options_container.add_child(btn)
|
|
_option_buttons.append(btn)
|
|
|
|
# Show with fade
|
|
visible = true
|
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
|
_is_showing = true
|
|
|
|
if _active_tween and _active_tween.is_valid():
|
|
_active_tween.kill()
|
|
_active_tween = create_tween()
|
|
_active_tween.tween_property(panel, "modulate:a", 1.0, FADE_IN)
|
|
|
|
|
|
# Hide dialogue with fade (D-064: 300ms)
|
|
func hide_dialogue() -> void:
|
|
if not _is_showing:
|
|
return
|
|
|
|
_is_showing = false
|
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
|
|
if _active_tween and _active_tween.is_valid():
|
|
_active_tween.kill()
|
|
_active_tween = create_tween()
|
|
_active_tween.tween_property(panel, "modulate:a", 0.0, FADE_OUT)
|
|
_active_tween.tween_callback(func():
|
|
visible = false
|
|
_clear_options()
|
|
)
|
|
|
|
|
|
func is_dialogue_active() -> bool:
|
|
return _is_showing
|
|
|
|
|
|
func _on_option_pressed(index: int) -> void:
|
|
if index < _option_buttons.size():
|
|
option_selected.emit(index, _option_buttons[index].text)
|
|
hide_dialogue()
|
|
|
|
|
|
func _clear_options() -> void:
|
|
for btn in _option_buttons:
|
|
if is_instance_valid(btn):
|
|
btn.queue_free()
|
|
_option_buttons.clear()
|