feat(ui): entity-anchored dialogue log with keyboard selection
Log entries store entity IDs and resolve display names at render time from _entity_display lookup — enables retroactive name update when player learns an NPC's real name. Color index from server replaces name-hash coloring for stable NPC colors. Dialogue options: switched RichTextLabel to Label (fixes stacking bug), added 1/2/3 number key selection, numbered option labels. Interaction list: added background panel, mouse hover highlighting, click-to-interact, pointing hand cursor. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,10 +19,12 @@ const LABEL_GAP := 2
|
||||
const INSERT_FG := Constants.IMPLANT_TEXT_COLOR
|
||||
const INSERT_DIM := Constants.IMPLANT_TEXT_DIM
|
||||
const INSERT_BG := Color(0.05, 0.05, 0.08, 0.7)
|
||||
const ENTITY_OFFSET := Vector2(0, -12) # nudge above entity sprite center
|
||||
|
||||
var _showing: bool = false
|
||||
var _insert_active: bool = true
|
||||
var _current_target_id: int = -1
|
||||
var _entity_world_pos: Vector2 = Vector2.ZERO # cached world tile position of target
|
||||
var _verb_items: Array = [] # sorted [{kind, label, priority, available}]
|
||||
var _selected_index: int = 0
|
||||
var _active_tween: Tween = null
|
||||
@@ -37,6 +39,21 @@ func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _showing:
|
||||
_update_screen_position()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if not _showing or _verb_labels.is_empty():
|
||||
return
|
||||
var pad := 6.0
|
||||
var bg_rect := Rect2(-pad, -pad, size.x + pad * 2, size.y + pad * 2)
|
||||
draw_rect(bg_rect, INSERT_BG)
|
||||
draw_rect(bg_rect, Constants.IMPLANT_TEXT_DIM * Color(1, 1, 1, 0.3), false, 1.0)
|
||||
|
||||
|
||||
func update_from_state() -> void:
|
||||
# D-055: Sprint stance suppresses interaction list
|
||||
if GameState.player_stance == "Sprint":
|
||||
@@ -70,6 +87,7 @@ func update_from_state() -> void:
|
||||
_current_target_id = entity_id
|
||||
_verb_items = sorted
|
||||
_selected_index = 0
|
||||
_cache_entity_position()
|
||||
_rebuild_labels()
|
||||
_show()
|
||||
|
||||
@@ -84,15 +102,46 @@ func _rebuild_labels() -> void:
|
||||
for i in range(_verb_items.size()):
|
||||
var verb: Dictionary = _verb_items[i]
|
||||
var lbl := Label.new()
|
||||
lbl.text = verb.get("label", "")
|
||||
lbl.text = " %s " % verb.get("label", "")
|
||||
lbl.add_theme_font_size_override("font_size", 14)
|
||||
lbl.add_theme_color_override("font_color", INSERT_FG if i == _selected_index else INSERT_DIM)
|
||||
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
lbl.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
lbl.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
var idx := i
|
||||
lbl.gui_input.connect(func(event: InputEvent):
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
_select_and_interact(idx)
|
||||
)
|
||||
lbl.mouse_entered.connect(func(): _hover_index(idx))
|
||||
lbl.mouse_exited.connect(func(): _unhover_index(idx))
|
||||
_vbox.add_child(lbl)
|
||||
_verb_labels.append(lbl)
|
||||
|
||||
|
||||
## Cache the target entity's world tile position from GameState.visible_entities.
|
||||
func _cache_entity_position() -> void:
|
||||
for entity in GameState.visible_entities:
|
||||
if entity.get("entity_id") == _current_target_id:
|
||||
_entity_world_pos = Vector2(entity.x, entity.y)
|
||||
return
|
||||
|
||||
|
||||
## Convert entity world position to screen coords and reposition this Control.
|
||||
## Runs every frame while showing so the list tracks the entity as the camera moves.
|
||||
func _update_screen_position() -> void:
|
||||
var camera := get_viewport().get_camera_2d()
|
||||
if camera == null:
|
||||
return
|
||||
var viewport_size := get_viewport_rect().size
|
||||
var cam_center := camera.get_screen_center_position()
|
||||
var zoom: Vector2 = camera.zoom if camera.zoom.length_squared() > 0.01 else Constants.CAMERA_DEFAULT_ZOOM
|
||||
var world_px := _entity_world_pos * Constants.TILE_SIZE
|
||||
var screen_pos := (world_px - cam_center) * zoom + viewport_size / 2.0
|
||||
# Anchor above the entity, centered horizontally
|
||||
position = screen_pos + ENTITY_OFFSET * zoom - Vector2(size.x / 2.0, size.y)
|
||||
|
||||
|
||||
func _show() -> void:
|
||||
if _showing:
|
||||
return
|
||||
@@ -161,6 +210,29 @@ func get_verb_labels() -> Array:
|
||||
return labels
|
||||
|
||||
|
||||
func _hover_index(idx: int) -> void:
|
||||
_selected_index = idx
|
||||
_update_label_colors()
|
||||
|
||||
|
||||
func _unhover_index(_idx: int) -> void:
|
||||
pass # keep last hover highlighted
|
||||
|
||||
|
||||
func _select_and_interact(idx: int) -> void:
|
||||
if idx < 0 or idx >= _verb_items.size():
|
||||
return
|
||||
_selected_index = idx
|
||||
verb_selected.emit(_verb_items[idx].get("kind", ""), _current_target_id)
|
||||
|
||||
|
||||
func _update_label_colors() -> void:
|
||||
for i in range(_verb_labels.size()):
|
||||
if is_instance_valid(_verb_labels[i]):
|
||||
_verb_labels[i].add_theme_color_override(
|
||||
"font_color", INSERT_FG if i == _selected_index else INSERT_DIM)
|
||||
|
||||
|
||||
func set_insert_active(active: bool) -> void:
|
||||
_insert_active = active
|
||||
if not active and _showing:
|
||||
|
||||
Reference in New Issue
Block a user