Add portrait battle unit list rows
This commit is contained in:
@@ -11,6 +11,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr
|
|||||||
- Move, attack, wait, end turn.
|
- Move, attack, wait, end turn.
|
||||||
- Enemy AI with movement, damage-aware physical attacks, multiple MP tactic choices, and scenario target priorities that events can retune mid-battle.
|
- Enemy AI with movement, damage-aware physical attacks, multiple MP tactic choices, and scenario target priorities that events can retune mid-battle.
|
||||||
- Hover tile and unit info with class, movement type, AGI, movement, range, and equipped gear.
|
- Hover tile and unit info with class, movement type, AGI, movement, range, and equipped gear.
|
||||||
|
- Active battle unit list opens from the top toolbar with ally/enemy tabs, portrait or sprite rows, compact HP/status/position badges, and hover combat details.
|
||||||
- Enemy threat range overlay with tile-level threat source, physical damage hints, and hostile tactic hints.
|
- Enemy threat range overlay with tile-level threat source, physical damage hints, and hostile tactic hints.
|
||||||
- Damage forecast for selected-unit attacks.
|
- Damage forecast for selected-unit attacks.
|
||||||
- Terrain defense and avoid affect physical attack damage and hit chance.
|
- Terrain defense and avoid affect physical attack damage and hit chance.
|
||||||
|
|||||||
@@ -134,7 +134,9 @@ const HUD_COMMAND_BUTTON_SIZE := Vector2(72, 42)
|
|||||||
const HUD_COMMAND_GRID_COLUMNS := 4
|
const HUD_COMMAND_GRID_COLUMNS := 4
|
||||||
const TOP_TOOL_BUTTON_SIZE := Vector2(42, 42)
|
const TOP_TOOL_BUTTON_SIZE := Vector2(42, 42)
|
||||||
const BATTLE_UNIT_LIST_PANEL_SIZE := Vector2(420, 360)
|
const BATTLE_UNIT_LIST_PANEL_SIZE := Vector2(420, 360)
|
||||||
const BATTLE_UNIT_LIST_ROW_SIZE := Vector2(372, 42)
|
const BATTLE_UNIT_LIST_ROW_SIZE := Vector2(372, 58)
|
||||||
|
const BATTLE_UNIT_LIST_ICON_SIZE := Vector2(46, 46)
|
||||||
|
const BATTLE_UNIT_LIST_BADGE_SIZE := Vector2(72, 22)
|
||||||
const SETTINGS_PANEL_SIZE := Vector2(420, 286)
|
const SETTINGS_PANEL_SIZE := Vector2(420, 286)
|
||||||
const LOCAL_COMMAND_BUTTON_SIZE := Vector2(92, 38)
|
const LOCAL_COMMAND_BUTTON_SIZE := Vector2(92, 38)
|
||||||
const LOCAL_COMMAND_CANCEL_BUTTON_SIZE := Vector2(196, 30)
|
const LOCAL_COMMAND_CANCEL_BUTTON_SIZE := Vector2(196, 30)
|
||||||
@@ -10600,7 +10602,7 @@ func _rebuild_battle_unit_list_rows() -> void:
|
|||||||
battle_unit_list_rows.add_child(empty_label)
|
battle_unit_list_rows.add_child(empty_label)
|
||||||
return
|
return
|
||||||
for unit in units:
|
for unit in units:
|
||||||
battle_unit_list_rows.add_child(_make_battle_unit_list_row_button(unit))
|
battle_unit_list_rows.add_child(_make_battle_unit_list_row(unit))
|
||||||
|
|
||||||
|
|
||||||
func _clear_battle_unit_list_rows() -> void:
|
func _clear_battle_unit_list_rows() -> void:
|
||||||
@@ -10634,32 +10636,116 @@ func _format_battle_unit_list_status(units: Array[Dictionary]) -> String:
|
|||||||
return "행동 %d · 완료 %d" % [ready_count, done_count]
|
return "행동 %d · 완료 %d" % [ready_count, done_count]
|
||||||
|
|
||||||
|
|
||||||
func _make_battle_unit_list_row_button(unit: Dictionary) -> Button:
|
func _make_battle_unit_list_row(unit: Dictionary) -> HBoxContainer:
|
||||||
var unit_id := str(unit.get("id", ""))
|
var unit_id := str(unit.get("id", ""))
|
||||||
|
var tooltip_text := _format_battle_unit_list_row_tooltip(unit)
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.custom_minimum_size = BATTLE_UNIT_LIST_ROW_SIZE
|
||||||
|
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
row.add_theme_constant_override("separation", 8)
|
||||||
|
row.tooltip_text = tooltip_text
|
||||||
|
|
||||||
|
row.add_child(_make_battle_unit_list_icon(unit, tooltip_text))
|
||||||
|
|
||||||
|
var column := VBoxContainer.new()
|
||||||
|
column.custom_minimum_size = Vector2(318, 0)
|
||||||
|
column.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
column.add_theme_constant_override("separation", 4)
|
||||||
|
column.tooltip_text = tooltip_text
|
||||||
|
row.add_child(column)
|
||||||
|
|
||||||
var row_button := Button.new()
|
var row_button := Button.new()
|
||||||
row_button.text = _format_battle_unit_list_row_text(unit)
|
row_button.text = _format_battle_unit_list_row_text(unit)
|
||||||
row_button.tooltip_text = _format_battle_unit_list_row_tooltip(unit)
|
row_button.tooltip_text = tooltip_text
|
||||||
row_button.custom_minimum_size = BATTLE_UNIT_LIST_ROW_SIZE
|
row_button.custom_minimum_size = Vector2(318, 28)
|
||||||
row_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
row_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
_apply_button_style(row_button)
|
_apply_button_style(row_button)
|
||||||
row_button.add_theme_font_size_override("font_size", 12)
|
row_button.add_theme_font_size_override("font_size", 12)
|
||||||
row_button.pressed.connect(_on_battle_unit_list_row_pressed.bind(unit_id))
|
row_button.pressed.connect(_on_battle_unit_list_row_pressed.bind(unit_id))
|
||||||
return row_button
|
column.add_child(row_button)
|
||||||
|
|
||||||
|
var badge_row := HBoxContainer.new()
|
||||||
|
badge_row.custom_minimum_size = Vector2(318, 22)
|
||||||
|
badge_row.add_theme_constant_override("separation", 5)
|
||||||
|
badge_row.tooltip_text = tooltip_text
|
||||||
|
column.add_child(badge_row)
|
||||||
|
|
||||||
|
for badge_text in _format_battle_unit_list_row_badges(unit):
|
||||||
|
var text := str(badge_text).strip_edges()
|
||||||
|
if text.is_empty():
|
||||||
|
continue
|
||||||
|
badge_row.add_child(_make_battle_unit_list_badge(text, tooltip_text))
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
func _make_battle_unit_list_icon(unit: Dictionary, tooltip_text: String) -> PanelContainer:
|
||||||
|
var panel := PanelContainer.new()
|
||||||
|
panel.custom_minimum_size = BATTLE_UNIT_LIST_ICON_SIZE
|
||||||
|
panel.tooltip_text = tooltip_text
|
||||||
|
_apply_panel_style(panel, "portrait")
|
||||||
|
|
||||||
|
var stack := Control.new()
|
||||||
|
stack.custom_minimum_size = BATTLE_UNIT_LIST_ICON_SIZE
|
||||||
|
stack.tooltip_text = tooltip_text
|
||||||
|
panel.add_child(stack)
|
||||||
|
|
||||||
|
var texture_rect := TextureRect.new()
|
||||||
|
texture_rect.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
texture_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||||
|
texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||||
|
texture_rect.tooltip_text = tooltip_text
|
||||||
|
var texture := _load_portrait_texture(str(unit.get("portrait", "")))
|
||||||
|
if texture == null:
|
||||||
|
texture = _load_art_texture(str(unit.get("sprite", "")))
|
||||||
|
texture_rect.texture = texture
|
||||||
|
texture_rect.visible = texture != null
|
||||||
|
stack.add_child(texture_rect)
|
||||||
|
|
||||||
|
var initials_label := Label.new()
|
||||||
|
initials_label.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
initials_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
initials_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
initials_label.text = _dialogue_portrait_initials(str(unit.get("name", "")))
|
||||||
|
initials_label.visible = texture == null
|
||||||
|
initials_label.tooltip_text = tooltip_text
|
||||||
|
_apply_label_style(initials_label, UI_PARCHMENT_TEXT, 14)
|
||||||
|
stack.add_child(initials_label)
|
||||||
|
return panel
|
||||||
|
|
||||||
|
|
||||||
func _format_battle_unit_list_row_text(unit: Dictionary) -> String:
|
func _format_battle_unit_list_row_text(unit: Dictionary) -> String:
|
||||||
|
return str(unit.get("name", "부대"))
|
||||||
|
|
||||||
|
|
||||||
|
func _format_battle_unit_list_row_badges(unit: Dictionary) -> Array:
|
||||||
var hp := int(unit.get("hp", 0))
|
var hp := int(unit.get("hp", 0))
|
||||||
var max_hp := maxi(1, int(unit.get("max_hp", 1)))
|
var max_hp := maxi(1, int(unit.get("max_hp", 1)))
|
||||||
var hp_percent := int(round(float(hp) * 100.0 / float(max_hp)))
|
var hp_percent := int(round(float(hp) * 100.0 / float(max_hp)))
|
||||||
return "%s %s 병%d%% %s %s" % [
|
return [
|
||||||
str(unit.get("name", "부대")),
|
|
||||||
_unit_class_display_name(unit),
|
_unit_class_display_name(unit),
|
||||||
hp_percent,
|
"병 %d%%" % hp_percent,
|
||||||
_battle_unit_action_state_text(unit),
|
_battle_unit_action_state_text(unit),
|
||||||
_format_cell_label(unit.get("pos", Vector2i(-1, -1)))
|
"좌 %s" % _format_cell_label(unit.get("pos", Vector2i(-1, -1)))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
func _make_battle_unit_list_badge(text: String, tooltip_text: String) -> PanelContainer:
|
||||||
|
var panel := PanelContainer.new()
|
||||||
|
panel.custom_minimum_size = BATTLE_UNIT_LIST_BADGE_SIZE
|
||||||
|
panel.tooltip_text = tooltip_text
|
||||||
|
_apply_panel_style(panel, "caption")
|
||||||
|
|
||||||
|
var label := Label.new()
|
||||||
|
label.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
label.text = text
|
||||||
|
label.tooltip_text = tooltip_text
|
||||||
|
_apply_label_style(label, UI_PARCHMENT_TEXT, 10)
|
||||||
|
panel.add_child(label)
|
||||||
|
return panel
|
||||||
|
|
||||||
|
|
||||||
func _format_battle_unit_list_row_tooltip(unit: Dictionary) -> String:
|
func _format_battle_unit_list_row_tooltip(unit: Dictionary) -> String:
|
||||||
var hint := "클릭하면 위치로 이동합니다."
|
var hint := "클릭하면 위치로 이동합니다."
|
||||||
if str(unit.get("team", "")) == BattleState.TEAM_PLAYER and _can_select(unit) and _can_select_from_battle_unit_list():
|
if str(unit.get("team", "")) == BattleState.TEAM_PLAYER and _can_select(unit) and _can_select_from_battle_unit_list():
|
||||||
|
|||||||
@@ -3744,6 +3744,16 @@ func _check_battle_unit_list_panel(failures: Array[String]) -> void:
|
|||||||
failures.append("ally battle unit list should include deployed officers")
|
failures.append("ally battle unit list should include deployed officers")
|
||||||
elif not _row_texts_contain(scene.battle_unit_list_rows, "조조") or not _row_texts_contain(scene.battle_unit_list_rows, "하후돈"):
|
elif not _row_texts_contain(scene.battle_unit_list_rows, "조조") or not _row_texts_contain(scene.battle_unit_list_rows, "하후돈"):
|
||||||
failures.append("ally battle unit list should show Cao Cao and Xiahou Dun")
|
failures.append("ally battle unit list should show Cao Cao and Xiahou Dun")
|
||||||
|
var ally_visible_text := _collect_child_text(scene.battle_unit_list_rows)
|
||||||
|
var ally_tooltips := _collect_child_tooltips(scene.battle_unit_list_rows)
|
||||||
|
if not ally_visible_text.contains("지휘관") or not ally_visible_text.contains("병 ") or not ally_visible_text.contains("좌 "):
|
||||||
|
failures.append("ally battle unit list should show compact class, HP, and position badges: %s" % ally_visible_text)
|
||||||
|
if ally_visible_text.contains("군령:") or ally_visible_text.contains("행군 4") or ally_visible_text.contains("타격 1-1"):
|
||||||
|
failures.append("ally battle unit list rows should keep dense combat detail in hover text: %s" % ally_visible_text)
|
||||||
|
if not ally_tooltips.contains("군세") or not ally_tooltips.contains("클릭하면 이 장수를 선택"):
|
||||||
|
failures.append("ally battle unit list tooltips should retain selection detail: %s" % ally_tooltips)
|
||||||
|
if not _has_descendant_texture(scene.battle_unit_list_rows):
|
||||||
|
failures.append("ally battle unit list should show officer portrait artwork")
|
||||||
|
|
||||||
scene._on_battle_unit_list_row_pressed("cao_cao")
|
scene._on_battle_unit_list_row_pressed("cao_cao")
|
||||||
var selected: Dictionary = scene.state.get_selected_unit()
|
var selected: Dictionary = scene.state.get_selected_unit()
|
||||||
@@ -3759,6 +3769,16 @@ func _check_battle_unit_list_panel(failures: Array[String]) -> void:
|
|||||||
failures.append("enemy battle unit list should show living enemies")
|
failures.append("enemy battle unit list should show living enemies")
|
||||||
elif not _row_texts_contain(scene.battle_unit_list_rows, "장만성"):
|
elif not _row_texts_contain(scene.battle_unit_list_rows, "장만성"):
|
||||||
failures.append("enemy battle unit list should include the enemy commander")
|
failures.append("enemy battle unit list should include the enemy commander")
|
||||||
|
var enemy_visible_text := _collect_child_text(scene.battle_unit_list_rows)
|
||||||
|
var enemy_tooltips := _collect_child_tooltips(scene.battle_unit_list_rows)
|
||||||
|
if not enemy_visible_text.contains("적") or not enemy_visible_text.contains("병 ") or not enemy_visible_text.contains("좌 "):
|
||||||
|
failures.append("enemy battle unit list should show compact status badges: %s" % enemy_visible_text)
|
||||||
|
if enemy_visible_text.contains("군령:") or enemy_visible_text.contains("직접 명령"):
|
||||||
|
failures.append("enemy battle unit list rows should keep dense enemy detail in hover text: %s" % enemy_visible_text)
|
||||||
|
if not enemy_tooltips.contains("군령: 적군 군기") or not enemy_tooltips.contains("클릭하면 위치로 이동"):
|
||||||
|
failures.append("enemy battle unit list tooltips should retain enemy detail: %s" % enemy_tooltips)
|
||||||
|
if not _has_descendant_texture(scene.battle_unit_list_rows):
|
||||||
|
failures.append("enemy battle unit list should show unit sprite artwork")
|
||||||
scene._on_battle_unit_list_row_pressed("yellow_turban_1")
|
scene._on_battle_unit_list_row_pressed("yellow_turban_1")
|
||||||
selected = scene.state.get_selected_unit()
|
selected = scene.state.get_selected_unit()
|
||||||
if selected.is_empty() or str(selected.get("id", "")) != "cao_cao":
|
if selected.is_empty() or str(selected.get("id", "")) != "cao_cao":
|
||||||
@@ -3775,12 +3795,7 @@ func _check_battle_unit_list_panel(failures: Array[String]) -> void:
|
|||||||
func _row_texts_contain(container: VBoxContainer, needle: String) -> bool:
|
func _row_texts_contain(container: VBoxContainer, needle: String) -> bool:
|
||||||
if container == null:
|
if container == null:
|
||||||
return false
|
return false
|
||||||
for child in container.get_children():
|
return _collect_child_text(container).contains(needle)
|
||||||
if child is Button and str((child as Button).text).contains(needle):
|
|
||||||
return true
|
|
||||||
if child is Label and str((child as Label).text).contains(needle):
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
|
|
||||||
|
|
||||||
func _check_action_button_disabled_reasons(failures: Array[String]) -> void:
|
func _check_action_button_disabled_reasons(failures: Array[String]) -> void:
|
||||||
|
|||||||
Reference in New Issue
Block a user