Show unit portraits in battle HUD

This commit is contained in:
2026-06-18 13:07:08 +09:00
parent 68342c1b0b
commit 7ad34697f9
9 changed files with 111 additions and 14 deletions

View File

@@ -48,6 +48,8 @@ const DIALOGUE_PANEL_SIZE := Vector2(1040, 210)
const DIALOGUE_PORTRAIT_SIZE := Vector2(164, 186)
const DIALOGUE_COLUMN_SIZE := Vector2(820, 186)
const DIALOGUE_TEXT_SIZE := Vector2(800, 104)
const HUD_UNIT_PORTRAIT_SIZE := Vector2(92, 104)
const HUD_UNIT_PORTRAIT_STACK_SIZE := Vector2(88, 100)
var state: BattleState = BattleStateScript.new()
var campaign_state: CampaignState = CampaignStateScript.new()
@@ -70,6 +72,9 @@ var campaign_complete_screen := false
var status_label: Label
var objective_label: Label
var campaign_status_label: Label
var hud_unit_portrait_panel: PanelContainer
var hud_unit_portrait_texture: TextureRect
var hud_unit_portrait_label: Label
var selected_label: Label
var cell_info_label: Label
var forecast_label: Label
@@ -144,6 +149,7 @@ var dialogue_queue: Array = []
var active_dialogue_lines: Array = []
var active_dialogue_index := 0
var portrait_texture_cache: Dictionary = {}
var missing_portrait_paths := {}
var bgm_player: AudioStreamPlayer
var current_bgm_key := ""
var audio_stream_cache := {}
@@ -252,10 +258,36 @@ func _create_hud() -> void:
side_column.add_theme_constant_override("separation", 10)
side_panel.add_child(side_column)
var selected_row := HBoxContainer.new()
selected_row.add_theme_constant_override("separation", 8)
side_column.add_child(selected_row)
hud_unit_portrait_panel = PanelContainer.new()
hud_unit_portrait_panel.custom_minimum_size = HUD_UNIT_PORTRAIT_SIZE
selected_row.add_child(hud_unit_portrait_panel)
var hud_unit_portrait_stack := Control.new()
hud_unit_portrait_stack.custom_minimum_size = HUD_UNIT_PORTRAIT_STACK_SIZE
hud_unit_portrait_panel.add_child(hud_unit_portrait_stack)
hud_unit_portrait_texture = TextureRect.new()
hud_unit_portrait_texture.set_anchors_preset(Control.PRESET_FULL_RECT)
hud_unit_portrait_texture.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
hud_unit_portrait_texture.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
hud_unit_portrait_stack.add_child(hud_unit_portrait_texture)
hud_unit_portrait_label = Label.new()
hud_unit_portrait_label.set_anchors_preset(Control.PRESET_FULL_RECT)
hud_unit_portrait_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
hud_unit_portrait_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
hud_unit_portrait_label.add_theme_font_size_override("font_size", 24)
hud_unit_portrait_stack.add_child(hud_unit_portrait_label)
selected_label = Label.new()
selected_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
selected_label.custom_minimum_size = Vector2(420, 92)
side_column.add_child(selected_label)
selected_label.custom_minimum_size = Vector2(320, 104)
selected_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
selected_row.add_child(selected_label)
cell_info_label = Label.new()
cell_info_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
@@ -1435,7 +1467,21 @@ func _update_hud() -> void:
var selected := state.get_selected_unit()
if selected.is_empty():
selected_label.text = "No unit selected."
var hovered_unit := _hovered_unit_for_hud_portrait()
_update_hud_unit_portrait(hovered_unit)
if hovered_unit.is_empty():
selected_label.text = "No unit selected."
else:
selected_label.text = "Hover: %s\n%s Lv.%d %s\nHP %d/%d MP %d/%d" % [
str(hovered_unit.get("name", "Unit")),
str(hovered_unit.get("team", "")).capitalize(),
int(hovered_unit.get("level", 1)),
str(hovered_unit.get("class", "Unit")),
int(hovered_unit.get("hp", 0)),
int(hovered_unit.get("max_hp", 1)),
int(hovered_unit.get("mp", 0)),
int(hovered_unit.get("max_mp", 0))
]
wait_button.disabled = true
_update_tactic_button({})
_hide_tactic_menu()
@@ -1444,6 +1490,7 @@ func _update_hud() -> void:
_update_equip_button({})
_hide_equip_menu()
else:
_update_hud_unit_portrait(selected)
var roster_mark := " *" if selected.get("loaded_from_roster", false) else ""
selected_label.text = "%s%s Lv.%d %s EXP %d\nHP %d/%d MP %d/%d ATK %d DEF %d INT %d\nMOV %d RNG %d-%d" % [
selected["name"],
@@ -1483,6 +1530,31 @@ func _update_hud() -> void:
threat_button.disabled = not battle_started or state.battle_status != BattleState.STATUS_ACTIVE or campaign_complete_screen
func _hovered_unit_for_hud_portrait() -> Dictionary:
if not state.is_inside(hover_cell):
return {}
return state.get_unit_at(hover_cell)
func _update_hud_unit_portrait(unit: Dictionary) -> void:
if hud_unit_portrait_panel == null:
return
if unit.is_empty():
hud_unit_portrait_panel.visible = false
hud_unit_portrait_texture.texture = null
hud_unit_portrait_texture.visible = false
hud_unit_portrait_label.text = ""
hud_unit_portrait_label.visible = false
return
hud_unit_portrait_panel.visible = true
var texture := _load_portrait_texture(str(unit.get("portrait", "")))
var has_portrait := texture != null
hud_unit_portrait_texture.texture = texture
hud_unit_portrait_texture.visible = has_portrait
hud_unit_portrait_label.text = _dialogue_portrait_initials(str(unit.get("name", "")))
hud_unit_portrait_label.visible = not has_portrait
func _update_cell_info() -> void:
if not state.is_inside(hover_cell):
cell_info_label.text = "Tile: -"
@@ -2032,7 +2104,7 @@ func _apply_dialogue_side(side: String) -> void:
func _update_dialogue_portrait(speaker: String, portrait_path: String) -> void:
if dialogue_portrait_panel == null:
return
var texture := _load_dialogue_portrait(portrait_path)
var texture := _load_portrait_texture(portrait_path)
var has_portrait := texture != null
var has_placeholder := not speaker.is_empty()
dialogue_portrait_panel.visible = has_portrait or has_placeholder
@@ -2042,12 +2114,14 @@ func _update_dialogue_portrait(speaker: String, portrait_path: String) -> void:
dialogue_portrait_label.visible = not has_portrait and has_placeholder
func _load_dialogue_portrait(portrait_path: String) -> Texture2D:
func _load_portrait_texture(portrait_path: String) -> Texture2D:
var normalized_path := portrait_path.strip_edges()
if normalized_path.is_empty():
return null
if not normalized_path.begins_with("res://") and not normalized_path.begins_with("user://"):
normalized_path = "res://%s" % normalized_path
if missing_portrait_paths.has(normalized_path):
return null
if portrait_texture_cache.has(normalized_path):
return portrait_texture_cache[normalized_path] as Texture2D
if ResourceLoader.exists(normalized_path, "Texture2D"):
@@ -2056,9 +2130,11 @@ func _load_dialogue_portrait(portrait_path: String) -> Texture2D:
portrait_texture_cache[normalized_path] = imported_texture
return imported_texture
if not FileAccess.file_exists(normalized_path):
missing_portrait_paths[normalized_path] = true
return null
var image := Image.new()
if image.load(normalized_path) != OK:
missing_portrait_paths[normalized_path] = true
return null
var raw_texture := ImageTexture.create_from_image(image)
portrait_texture_cache[normalized_path] = raw_texture