Clarify combat result reports
This commit is contained in:
@@ -7,6 +7,7 @@ signal changed
|
|||||||
signal log_added(message: String)
|
signal log_added(message: String)
|
||||||
signal dialogue_requested(lines: Array)
|
signal dialogue_requested(lines: Array)
|
||||||
signal combat_feedback_requested(unit_id: String, text: String, kind: String)
|
signal combat_feedback_requested(unit_id: String, text: String, kind: String)
|
||||||
|
signal combat_result_reported(report: Dictionary)
|
||||||
signal unit_motion_requested(unit_id: String, from_cell: Vector2i, to_cell: Vector2i)
|
signal unit_motion_requested(unit_id: String, from_cell: Vector2i, to_cell: Vector2i)
|
||||||
signal unit_action_motion_requested(unit_id: String, from_cell: Vector2i, to_cell: Vector2i, action_kind: String)
|
signal unit_action_motion_requested(unit_id: String, from_cell: Vector2i, to_cell: Vector2i, action_kind: String)
|
||||||
signal objective_updated(victory: String, defeat: String)
|
signal objective_updated(victory: String, defeat: String)
|
||||||
@@ -3752,19 +3753,40 @@ func _opposing_team(team: String) -> String:
|
|||||||
|
|
||||||
|
|
||||||
func _resolve_combat(attacker: Dictionary, target: Dictionary) -> void:
|
func _resolve_combat(attacker: Dictionary, target: Dictionary) -> void:
|
||||||
|
var report := _make_combat_result_report(attacker, target)
|
||||||
var attack_result := _resolve_attack(attacker, target, false)
|
var attack_result := _resolve_attack(attacker, target, false)
|
||||||
|
(report["entries"] as Array).append(attack_result)
|
||||||
var attack_exp := EXP_ATTACK if bool(attack_result.get("hit", false)) else EXP_MISS
|
var attack_exp := EXP_ATTACK if bool(attack_result.get("hit", false)) else EXP_MISS
|
||||||
if bool(attack_result.get("defeated", false)) or battle_status != STATUS_ACTIVE:
|
if bool(attack_result.get("defeated", false)) or battle_status != STATUS_ACTIVE:
|
||||||
_grant_experience(attacker, attack_exp + (EXP_DEFEAT_BONUS if bool(attack_result.get("defeated", false)) else 0))
|
var final_attack_exp := attack_exp + (EXP_DEFEAT_BONUS if bool(attack_result.get("defeated", false)) else 0)
|
||||||
|
_grant_experience(attacker, final_attack_exp)
|
||||||
|
report["attacker_merit"] = final_attack_exp
|
||||||
|
combat_result_reported.emit(report)
|
||||||
return
|
return
|
||||||
|
|
||||||
var counter_exp := 0
|
var counter_exp := 0
|
||||||
if bool(target.get("controllable", true)) and _is_in_attack_range(target, attacker["pos"]):
|
if bool(target.get("controllable", true)) and _is_in_attack_range(target, attacker["pos"]):
|
||||||
var counter_result := _resolve_attack(target, attacker, true)
|
var counter_result := _resolve_attack(target, attacker, true)
|
||||||
|
(report["entries"] as Array).append(counter_result)
|
||||||
counter_exp = (EXP_COUNTER if bool(counter_result.get("hit", false)) else EXP_MISS) + (EXP_DEFEAT_BONUS if bool(counter_result.get("defeated", false)) else 0)
|
counter_exp = (EXP_COUNTER if bool(counter_result.get("hit", false)) else EXP_MISS) + (EXP_DEFEAT_BONUS if bool(counter_result.get("defeated", false)) else 0)
|
||||||
|
|
||||||
_grant_experience(attacker, attack_exp)
|
_grant_experience(attacker, attack_exp)
|
||||||
|
report["attacker_merit"] = attack_exp
|
||||||
_grant_experience(target, counter_exp)
|
_grant_experience(target, counter_exp)
|
||||||
|
report["counter_merit"] = counter_exp
|
||||||
|
combat_result_reported.emit(report)
|
||||||
|
|
||||||
|
|
||||||
|
func _make_combat_result_report(attacker: Dictionary, target: Dictionary) -> Dictionary:
|
||||||
|
return {
|
||||||
|
"attacker_id": str(attacker.get("id", "")),
|
||||||
|
"attacker_name": str(attacker.get("name", "")),
|
||||||
|
"target_id": str(target.get("id", "")),
|
||||||
|
"target_name": str(target.get("name", "")),
|
||||||
|
"attacker_merit": 0,
|
||||||
|
"counter_merit": 0,
|
||||||
|
"entries": []
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func _resolve_attack(attacker: Dictionary, target: Dictionary, is_counter := false) -> Dictionary:
|
func _resolve_attack(attacker: Dictionary, target: Dictionary, is_counter := false) -> Dictionary:
|
||||||
@@ -3780,7 +3802,22 @@ func _resolve_attack(attacker: Dictionary, target: Dictionary, is_counter := fal
|
|||||||
if rng.randi_range(1, 100) > hit_chance:
|
if rng.randi_range(1, 100) > hit_chance:
|
||||||
_emit_log("%s %s %s, 헛쳤다. 명중 %d%%." % [attacker["name"], verb, target["name"], hit_chance])
|
_emit_log("%s %s %s, 헛쳤다. 명중 %d%%." % [attacker["name"], verb, target["name"], hit_chance])
|
||||||
_emit_combat_feedback(target, "헛침", "miss")
|
_emit_combat_feedback(target, "헛침", "miss")
|
||||||
return {"hit": false, "defeated": false}
|
return {
|
||||||
|
"attacker_id": str(attacker.get("id", "")),
|
||||||
|
"attacker_name": str(attacker.get("name", "")),
|
||||||
|
"target_id": str(target.get("id", "")),
|
||||||
|
"target_name": str(target.get("name", "")),
|
||||||
|
"is_counter": is_counter,
|
||||||
|
"hit": false,
|
||||||
|
"defeated": false,
|
||||||
|
"damage": 0,
|
||||||
|
"hit_chance": hit_chance,
|
||||||
|
"target_hp": int(target.get("hp", 0)),
|
||||||
|
"target_max_hp": int(target.get("max_hp", 0)),
|
||||||
|
"directional_label": "",
|
||||||
|
"directional_bonus": 0,
|
||||||
|
"effective_bonus": 0
|
||||||
|
}
|
||||||
var directional_info := get_directional_attack_info(attacker, target)
|
var directional_info := get_directional_attack_info(attacker, target)
|
||||||
var damage := calculate_damage(attacker, target)
|
var damage := calculate_damage(attacker, target)
|
||||||
target["hp"] = max(0, int(target["hp"]) - damage)
|
target["hp"] = max(0, int(target["hp"]) - damage)
|
||||||
@@ -3799,8 +3836,38 @@ func _resolve_attack(attacker: Dictionary, target: Dictionary, is_counter := fal
|
|||||||
_emit_log("%s 퇴각." % target["name"])
|
_emit_log("%s 퇴각." % target["name"])
|
||||||
_emit_combat_feedback(target, "퇴각", "defeat")
|
_emit_combat_feedback(target, "퇴각", "defeat")
|
||||||
_handle_unit_defeated(target)
|
_handle_unit_defeated(target)
|
||||||
return {"hit": true, "defeated": true}
|
return {
|
||||||
return {"hit": true, "defeated": false}
|
"attacker_id": str(attacker.get("id", "")),
|
||||||
|
"attacker_name": str(attacker.get("name", "")),
|
||||||
|
"target_id": str(target.get("id", "")),
|
||||||
|
"target_name": str(target.get("name", "")),
|
||||||
|
"is_counter": is_counter,
|
||||||
|
"hit": true,
|
||||||
|
"defeated": true,
|
||||||
|
"damage": damage,
|
||||||
|
"hit_chance": hit_chance,
|
||||||
|
"target_hp": int(target.get("hp", 0)),
|
||||||
|
"target_max_hp": int(target.get("max_hp", 0)),
|
||||||
|
"directional_label": str(directional_info.get("label", "")),
|
||||||
|
"directional_bonus": directional_bonus,
|
||||||
|
"effective_bonus": effective_bonus
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"attacker_id": str(attacker.get("id", "")),
|
||||||
|
"attacker_name": str(attacker.get("name", "")),
|
||||||
|
"target_id": str(target.get("id", "")),
|
||||||
|
"target_name": str(target.get("name", "")),
|
||||||
|
"is_counter": is_counter,
|
||||||
|
"hit": true,
|
||||||
|
"defeated": false,
|
||||||
|
"damage": damage,
|
||||||
|
"hit_chance": hit_chance,
|
||||||
|
"target_hp": int(target.get("hp", 0)),
|
||||||
|
"target_max_hp": int(target.get("max_hp", 0)),
|
||||||
|
"directional_label": str(directional_info.get("label", "")),
|
||||||
|
"directional_bonus": directional_bonus,
|
||||||
|
"effective_bonus": effective_bonus
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func _resolve_skill_damage(caster: Dictionary, target: Dictionary, skill: Dictionary) -> bool:
|
func _resolve_skill_damage(caster: Dictionary, target: Dictionary, skill: Dictionary) -> bool:
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ const AUDIO_PREVIEW_LABEL := "확인음 · 발소리 · 타격음 · 퇴각음
|
|||||||
const FLOATING_TEXT_LIFETIME := 1.0
|
const FLOATING_TEXT_LIFETIME := 1.0
|
||||||
const FLOATING_TEXT_RISE := 42.0
|
const FLOATING_TEXT_RISE := 42.0
|
||||||
const OBJECTIVE_NOTICE_DURATION := 2.6
|
const OBJECTIVE_NOTICE_DURATION := 2.6
|
||||||
|
const COMBAT_RESULT_NOTICE_DURATION := 2.8
|
||||||
const EDGE_SCROLL_MARGIN := 48.0
|
const EDGE_SCROLL_MARGIN := 48.0
|
||||||
const EDGE_SCROLL_OUTER_MARGIN := 28.0
|
const EDGE_SCROLL_OUTER_MARGIN := 28.0
|
||||||
const EDGE_SCROLL_MIN_SPEED := 80.0
|
const EDGE_SCROLL_MIN_SPEED := 80.0
|
||||||
@@ -194,6 +195,8 @@ const OPENING_STORY_PROGRESS_LEDGER_SIZE := Vector2(326, 48)
|
|||||||
const OPENING_STORY_PROGRESS_SEAL_SIZE := Vector2(34, 34)
|
const OPENING_STORY_PROGRESS_SEAL_SIZE := Vector2(34, 34)
|
||||||
const COMMAND_NOTICE_ICON_SIZE := Vector2(32, 32)
|
const COMMAND_NOTICE_ICON_SIZE := Vector2(32, 32)
|
||||||
const COMMAND_NOTICE_MAX_ICONS := 5
|
const COMMAND_NOTICE_MAX_ICONS := 5
|
||||||
|
const COMBAT_RESULT_PANEL_POSITION := Vector2(838, 430)
|
||||||
|
const COMBAT_RESULT_PANEL_SIZE := Vector2(396, 128)
|
||||||
const CHAPTER_SEAL_BADGE_SIZE := Vector2(54, 54)
|
const CHAPTER_SEAL_BADGE_SIZE := Vector2(54, 54)
|
||||||
const CHAPTER_SCENARIO_THUMBNAIL_SIZE := Vector2(72, 50)
|
const CHAPTER_SCENARIO_THUMBNAIL_SIZE := Vector2(72, 50)
|
||||||
const CHAPTER_INFO_BADGE_SIZE := Vector2(86, 22)
|
const CHAPTER_INFO_BADGE_SIZE := Vector2(86, 22)
|
||||||
@@ -479,6 +482,10 @@ var mission_detail_label: Label
|
|||||||
var objective_notice_panel: PanelContainer
|
var objective_notice_panel: PanelContainer
|
||||||
var objective_notice_icon_strip: HBoxContainer
|
var objective_notice_icon_strip: HBoxContainer
|
||||||
var objective_notice_label: Label
|
var objective_notice_label: Label
|
||||||
|
var combat_result_panel: PanelContainer
|
||||||
|
var combat_result_title_label: Label
|
||||||
|
var combat_result_primary_label: Label
|
||||||
|
var combat_result_detail_label: Label
|
||||||
var ui_root_control: Control
|
var ui_root_control: Control
|
||||||
var screen_backdrop: ColorRect
|
var screen_backdrop: ColorRect
|
||||||
var hud_unit_portrait_panel: PanelContainer
|
var hud_unit_portrait_panel: PanelContainer
|
||||||
@@ -703,7 +710,9 @@ var audio_stream_cache := {}
|
|||||||
var recent_sfx_keys: Array[String] = []
|
var recent_sfx_keys: Array[String] = []
|
||||||
var last_announced_battle_status := ""
|
var last_announced_battle_status := ""
|
||||||
var floating_texts: Array[Dictionary] = []
|
var floating_texts: Array[Dictionary] = []
|
||||||
|
var pending_combat_result_report: Dictionary = {}
|
||||||
var objective_notice_timer := 0.0
|
var objective_notice_timer := 0.0
|
||||||
|
var combat_result_notice_timer := 0.0
|
||||||
var battle_start_notice_pending := false
|
var battle_start_notice_pending := false
|
||||||
var unit_motion_by_unit: Dictionary = {}
|
var unit_motion_by_unit: Dictionary = {}
|
||||||
var unit_action_motion_by_unit: Dictionary = {}
|
var unit_action_motion_by_unit: Dictionary = {}
|
||||||
@@ -730,6 +739,7 @@ func _ready() -> void:
|
|||||||
state.log_added.connect(_on_log_added)
|
state.log_added.connect(_on_log_added)
|
||||||
state.dialogue_requested.connect(_on_dialogue_requested)
|
state.dialogue_requested.connect(_on_dialogue_requested)
|
||||||
state.combat_feedback_requested.connect(_on_combat_feedback_requested)
|
state.combat_feedback_requested.connect(_on_combat_feedback_requested)
|
||||||
|
state.combat_result_reported.connect(_on_combat_result_reported)
|
||||||
state.unit_motion_requested.connect(_on_unit_motion_requested)
|
state.unit_motion_requested.connect(_on_unit_motion_requested)
|
||||||
state.unit_action_motion_requested.connect(_on_unit_action_motion_requested)
|
state.unit_action_motion_requested.connect(_on_unit_action_motion_requested)
|
||||||
state.objective_updated.connect(_on_objective_updated)
|
state.objective_updated.connect(_on_objective_updated)
|
||||||
@@ -3693,6 +3703,44 @@ func _create_hud() -> void:
|
|||||||
objective_notice_column.add_child(objective_notice_label)
|
objective_notice_column.add_child(objective_notice_label)
|
||||||
objective_notice_column.add_child(_make_scroll_rod(548, 3))
|
objective_notice_column.add_child(_make_scroll_rod(548, 3))
|
||||||
|
|
||||||
|
combat_result_panel = PanelContainer.new()
|
||||||
|
combat_result_panel.name = "CombatResultNotice"
|
||||||
|
combat_result_panel.visible = false
|
||||||
|
combat_result_panel.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||||
|
combat_result_panel.position = COMBAT_RESULT_PANEL_POSITION
|
||||||
|
combat_result_panel.size = COMBAT_RESULT_PANEL_SIZE
|
||||||
|
combat_result_panel.custom_minimum_size = COMBAT_RESULT_PANEL_SIZE
|
||||||
|
combat_result_panel.z_as_relative = false
|
||||||
|
combat_result_panel.z_index = 73
|
||||||
|
_apply_panel_style(combat_result_panel, "hud_readable")
|
||||||
|
root.add_child(combat_result_panel)
|
||||||
|
|
||||||
|
var combat_result_column := VBoxContainer.new()
|
||||||
|
combat_result_column.add_theme_constant_override("separation", 5)
|
||||||
|
combat_result_panel.add_child(combat_result_column)
|
||||||
|
|
||||||
|
combat_result_title_label = Label.new()
|
||||||
|
combat_result_title_label.text = "전공 보고"
|
||||||
|
combat_result_title_label.custom_minimum_size = Vector2(360, 24)
|
||||||
|
combat_result_title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
combat_result_title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||||
|
_apply_label_style(combat_result_title_label, UI_PARCHMENT_TEXT, 15)
|
||||||
|
combat_result_column.add_child(combat_result_title_label)
|
||||||
|
|
||||||
|
combat_result_primary_label = Label.new()
|
||||||
|
combat_result_primary_label.custom_minimum_size = Vector2(360, 30)
|
||||||
|
combat_result_primary_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
combat_result_primary_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
_apply_label_style(combat_result_primary_label, UI_RICE_PAPER_LIGHT, 15)
|
||||||
|
combat_result_column.add_child(combat_result_primary_label)
|
||||||
|
|
||||||
|
combat_result_detail_label = Label.new()
|
||||||
|
combat_result_detail_label.custom_minimum_size = Vector2(360, 44)
|
||||||
|
combat_result_detail_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
|
||||||
|
combat_result_detail_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
_apply_label_style(combat_result_detail_label, UI_PARCHMENT_TEXT, 13)
|
||||||
|
combat_result_column.add_child(combat_result_detail_label)
|
||||||
|
|
||||||
auto_end_turn_prompt_panel = PanelContainer.new()
|
auto_end_turn_prompt_panel = PanelContainer.new()
|
||||||
auto_end_turn_prompt_panel.name = "AutoEndTurnPrompt"
|
auto_end_turn_prompt_panel.name = "AutoEndTurnPrompt"
|
||||||
auto_end_turn_prompt_panel.visible = false
|
auto_end_turn_prompt_panel.visible = false
|
||||||
@@ -6124,6 +6172,10 @@ func _process(delta: float) -> void:
|
|||||||
objective_notice_timer = maxf(0.0, objective_notice_timer - delta)
|
objective_notice_timer = maxf(0.0, objective_notice_timer - delta)
|
||||||
if objective_notice_timer <= 0.0 and objective_notice_panel != null:
|
if objective_notice_timer <= 0.0 and objective_notice_panel != null:
|
||||||
objective_notice_panel.visible = false
|
objective_notice_panel.visible = false
|
||||||
|
if combat_result_notice_timer > 0.0:
|
||||||
|
combat_result_notice_timer = maxf(0.0, combat_result_notice_timer - delta)
|
||||||
|
if combat_result_notice_timer <= 0.0 and combat_result_panel != null:
|
||||||
|
combat_result_panel.visible = false
|
||||||
|
|
||||||
if not floating_texts.is_empty():
|
if not floating_texts.is_empty():
|
||||||
var active_texts: Array[Dictionary] = []
|
var active_texts: Array[Dictionary] = []
|
||||||
@@ -12545,6 +12597,9 @@ func _on_dialogue_requested(lines: Array) -> void:
|
|||||||
var normalized_lines := _normalized_dialogue_lines(lines)
|
var normalized_lines := _normalized_dialogue_lines(lines)
|
||||||
if normalized_lines.is_empty():
|
if normalized_lines.is_empty():
|
||||||
return
|
return
|
||||||
|
if combat_result_panel != null:
|
||||||
|
combat_result_panel.visible = false
|
||||||
|
combat_result_notice_timer = 0.0
|
||||||
dialogue_queue.append(normalized_lines)
|
dialogue_queue.append(normalized_lines)
|
||||||
if _can_show_dialogue_now():
|
if _can_show_dialogue_now():
|
||||||
_show_next_dialogue()
|
_show_next_dialogue()
|
||||||
@@ -12580,6 +12635,100 @@ func _on_combat_feedback_requested(unit_id: String, text: String, kind: String)
|
|||||||
queue_redraw()
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_combat_result_reported(report: Dictionary) -> void:
|
||||||
|
if report.is_empty():
|
||||||
|
return
|
||||||
|
if _is_dialogue_visible() or not dialogue_queue.is_empty():
|
||||||
|
pending_combat_result_report = report.duplicate(true)
|
||||||
|
return
|
||||||
|
_show_combat_result_report(report)
|
||||||
|
|
||||||
|
|
||||||
|
func _show_combat_result_report(report: Dictionary) -> void:
|
||||||
|
if combat_result_panel == null or report.is_empty():
|
||||||
|
return
|
||||||
|
var lines := _format_combat_result_report_lines(report)
|
||||||
|
if lines.is_empty():
|
||||||
|
return
|
||||||
|
combat_result_title_label.text = "전공 보고"
|
||||||
|
combat_result_primary_label.text = lines[0]
|
||||||
|
combat_result_primary_label.tooltip_text = _join_strings(lines, "\n")
|
||||||
|
combat_result_detail_label.text = _join_strings(lines.slice(1), "\n")
|
||||||
|
combat_result_detail_label.tooltip_text = combat_result_primary_label.tooltip_text
|
||||||
|
combat_result_panel.tooltip_text = combat_result_primary_label.tooltip_text
|
||||||
|
combat_result_panel.visible = true
|
||||||
|
combat_result_notice_timer = COMBAT_RESULT_NOTICE_DURATION
|
||||||
|
combat_result_panel.move_to_front()
|
||||||
|
|
||||||
|
|
||||||
|
func _show_pending_combat_result_if_possible() -> void:
|
||||||
|
if pending_combat_result_report.is_empty() or _is_dialogue_visible() or not dialogue_queue.is_empty():
|
||||||
|
return
|
||||||
|
var report := pending_combat_result_report.duplicate(true)
|
||||||
|
pending_combat_result_report.clear()
|
||||||
|
_show_combat_result_report(report)
|
||||||
|
|
||||||
|
|
||||||
|
func _format_combat_result_report_lines(report: Dictionary) -> Array[String]:
|
||||||
|
var lines: Array[String] = []
|
||||||
|
var entries: Array = report.get("entries", [])
|
||||||
|
for entry_value in entries:
|
||||||
|
if typeof(entry_value) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var entry := entry_value as Dictionary
|
||||||
|
var line := _format_combat_result_entry(entry)
|
||||||
|
if not line.is_empty():
|
||||||
|
lines.append(line)
|
||||||
|
if int(report.get("attacker_merit", 0)) > 0:
|
||||||
|
lines.append("%s 공적 +%d" % [str(report.get("attacker_name", "아군")), int(report.get("attacker_merit", 0))])
|
||||||
|
if int(report.get("counter_merit", 0)) > 0:
|
||||||
|
var counter_name := _combat_result_counter_name(report)
|
||||||
|
lines.append("%s 공적 +%d" % [counter_name, int(report.get("counter_merit", 0))])
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
func _format_combat_result_entry(entry: Dictionary) -> String:
|
||||||
|
var attacker_name := str(entry.get("attacker_name", "부대"))
|
||||||
|
var target_name := str(entry.get("target_name", "적"))
|
||||||
|
var verb := "반격" if bool(entry.get("is_counter", false)) else "공격"
|
||||||
|
if not bool(entry.get("hit", false)):
|
||||||
|
return "%s %s %s · 헛침 · 명중 %d%%" % [
|
||||||
|
attacker_name,
|
||||||
|
verb,
|
||||||
|
target_name,
|
||||||
|
int(entry.get("hit_chance", 0))
|
||||||
|
]
|
||||||
|
var parts: Array[String] = [
|
||||||
|
"%s %s %s" % [attacker_name, verb, target_name],
|
||||||
|
"%d 피해" % int(entry.get("damage", 0))
|
||||||
|
]
|
||||||
|
var directional_bonus := int(entry.get("directional_bonus", 0))
|
||||||
|
if directional_bonus > 0:
|
||||||
|
parts.append("%s +%d" % [str(entry.get("directional_label", "방향")), directional_bonus])
|
||||||
|
var effective_bonus := int(entry.get("effective_bonus", 0))
|
||||||
|
if effective_bonus > 0:
|
||||||
|
parts.append("특효 +%d" % effective_bonus)
|
||||||
|
if bool(entry.get("defeated", false)):
|
||||||
|
parts.append("퇴각")
|
||||||
|
else:
|
||||||
|
var target_hp := int(entry.get("target_hp", 0))
|
||||||
|
var target_max_hp := int(entry.get("target_max_hp", 0))
|
||||||
|
if target_max_hp > 0:
|
||||||
|
parts.append("잔여 %d/%d" % [target_hp, target_max_hp])
|
||||||
|
return _join_strings(parts, " · ")
|
||||||
|
|
||||||
|
|
||||||
|
func _combat_result_counter_name(report: Dictionary) -> String:
|
||||||
|
var entries: Array = report.get("entries", [])
|
||||||
|
for entry_value in entries:
|
||||||
|
if typeof(entry_value) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var entry := entry_value as Dictionary
|
||||||
|
if bool(entry.get("is_counter", false)):
|
||||||
|
return str(entry.get("attacker_name", report.get("target_name", "반격 부대")))
|
||||||
|
return str(report.get("target_name", "반격 부대"))
|
||||||
|
|
||||||
|
|
||||||
func _combat_feedback_sfx_key(text: String, kind: String) -> String:
|
func _combat_feedback_sfx_key(text: String, kind: String) -> String:
|
||||||
if kind == "reinforcement" or text == "증원":
|
if kind == "reinforcement" or text == "증원":
|
||||||
return "footstep"
|
return "footstep"
|
||||||
@@ -13093,6 +13242,7 @@ func _advance_dialogue() -> void:
|
|||||||
_show_next_dialogue()
|
_show_next_dialogue()
|
||||||
else:
|
else:
|
||||||
_show_battle_start_notice_if_ready()
|
_show_battle_start_notice_if_ready()
|
||||||
|
_show_pending_combat_result_if_possible()
|
||||||
_update_hud()
|
_update_hud()
|
||||||
queue_redraw()
|
queue_redraw()
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,10 @@ func _capture_viewport_set(spec: Dictionary, output_dir: String, captures: Array
|
|||||||
await _settle()
|
await _settle()
|
||||||
await _capture_frame(scene, output_dir, label, "12_post_move_item_picker", captures, failures)
|
await _capture_frame(scene, output_dir, label, "12_post_move_item_picker", captures, failures)
|
||||||
|
|
||||||
|
_prepare_combat_result_frame(scene)
|
||||||
|
await _settle()
|
||||||
|
await _capture_frame(scene, output_dir, label, "13_battle_combat_result", captures, failures)
|
||||||
|
|
||||||
scene.queue_free()
|
scene.queue_free()
|
||||||
await _settle()
|
await _settle()
|
||||||
|
|
||||||
@@ -194,12 +198,48 @@ func _prepare_post_move_action_frame(scene, picker_mode: String) -> void:
|
|||||||
scene.queue_redraw()
|
scene.queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _prepare_combat_result_frame(scene) -> void:
|
||||||
|
scene._hide_command_hint()
|
||||||
|
scene._hide_post_move_menu()
|
||||||
|
scene._hide_post_move_picker()
|
||||||
|
scene._hide_tactic_menu()
|
||||||
|
scene._hide_item_menu()
|
||||||
|
scene._hide_equip_menu()
|
||||||
|
scene._clear_pending_move_state()
|
||||||
|
scene.selected_skill_id = ""
|
||||||
|
scene.selected_item_id = ""
|
||||||
|
scene.basic_attack_targeting = false
|
||||||
|
var cao_cao: Dictionary = scene.state.get_unit("cao_cao")
|
||||||
|
var enemy: Dictionary = scene.state.get_unit("yellow_turban_1")
|
||||||
|
if cao_cao.is_empty() or enemy.is_empty():
|
||||||
|
return
|
||||||
|
cao_cao["pos"] = Vector2i(2, 3)
|
||||||
|
cao_cao["moved"] = false
|
||||||
|
cao_cao["acted"] = false
|
||||||
|
cao_cao["atk"] = maxi(48, int(cao_cao.get("atk", 0)))
|
||||||
|
cao_cao["agi"] = 999
|
||||||
|
enemy["pos"] = Vector2i(3, 3)
|
||||||
|
enemy["alive"] = true
|
||||||
|
enemy["hp"] = 90
|
||||||
|
enemy["max_hp"] = 90
|
||||||
|
enemy["def"] = 1
|
||||||
|
scene.state.rng.seed = 1
|
||||||
|
scene.state.select_unit("cao_cao")
|
||||||
|
scene._refresh_ranges()
|
||||||
|
scene._focus_board_on_cell(Vector2i(3, 3))
|
||||||
|
scene.state.try_attack_selected("yellow_turban_1")
|
||||||
|
scene._update_hud()
|
||||||
|
scene.queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
func _screen_for_cell(scene, cell: Vector2i) -> Vector2:
|
func _screen_for_cell(scene, cell: Vector2i) -> Vector2:
|
||||||
var rect: Rect2 = scene._rect_for_cell(cell)
|
var rect: Rect2 = scene._rect_for_cell(cell)
|
||||||
return rect.position + rect.size * 0.5
|
return rect.position + rect.size * 0.5
|
||||||
|
|
||||||
|
|
||||||
func _capture_frame(_scene, output_dir: String, viewport_label: String, frame_label: String, captures: Array[Dictionary], failures: Array[String]) -> void:
|
func _capture_frame(_scene, output_dir: String, viewport_label: String, frame_label: String, captures: Array[Dictionary], failures: Array[String]) -> void:
|
||||||
|
Input.warp_mouse(Vector2(12, 12))
|
||||||
|
await _settle()
|
||||||
var texture := get_root().get_texture()
|
var texture := get_root().get_texture()
|
||||||
if texture == null:
|
if texture == null:
|
||||||
failures.append("viewport texture missing for %s/%s" % [viewport_label, frame_label])
|
failures.append("viewport texture missing for %s/%s" % [viewport_label, frame_label])
|
||||||
|
|||||||
@@ -619,6 +619,8 @@ func _check_scene_post_move_attack_targeting(failures: Array[String]) -> void:
|
|||||||
if unit_id == "cao_cao" and kind == "progress":
|
if unit_id == "cao_cao" and kind == "progress":
|
||||||
progress_feedback.append(text)
|
progress_feedback.append(text)
|
||||||
)
|
)
|
||||||
|
if not scene.state.combat_result_reported.is_connected(scene._on_combat_result_reported):
|
||||||
|
scene.state.combat_result_reported.connect(scene._on_combat_result_reported)
|
||||||
scene._on_post_move_attack_pressed()
|
scene._on_post_move_attack_pressed()
|
||||||
var enemy_hp_before := int(enemy.get("hp", 0))
|
var enemy_hp_before := int(enemy.get("hp", 0))
|
||||||
scene._handle_board_click(_screen_for_cell(Vector2i(3, 3)))
|
scene._handle_board_click(_screen_for_cell(Vector2i(3, 3)))
|
||||||
@@ -636,6 +638,12 @@ func _check_scene_post_move_attack_targeting(failures: Array[String]) -> void:
|
|||||||
failures.append("attack click should commit deferred movement event before resolving attack")
|
failures.append("attack click should commit deferred movement event before resolving attack")
|
||||||
if not _feedback_contains(progress_feedback, "공적 +"):
|
if not _feedback_contains(progress_feedback, "공적 +"):
|
||||||
failures.append("attack click should show visible merit feedback over the unit: %s" % str(progress_feedback))
|
failures.append("attack click should show visible merit feedback over the unit: %s" % str(progress_feedback))
|
||||||
|
if scene.combat_result_panel == null or not scene.combat_result_panel.visible:
|
||||||
|
failures.append("attack click should show a compact combat result report")
|
||||||
|
else:
|
||||||
|
var result_text := "%s\n%s" % [scene.combat_result_primary_label.text, scene.combat_result_detail_label.text]
|
||||||
|
if not result_text.contains("공격") or not result_text.contains("피해") or not result_text.contains("공적 +"):
|
||||||
|
failures.append("combat result report should summarize damage and merit: %s" % result_text)
|
||||||
|
|
||||||
scene.free()
|
scene.free()
|
||||||
|
|
||||||
|
|||||||
@@ -3881,6 +3881,35 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void:
|
|||||||
_check_panel_style_frame(failures, scene.cell_info_panel, "HUD terrain info panel", Color(0.42, 0.35, 0.24, 0.98), 2)
|
_check_panel_style_frame(failures, scene.cell_info_panel, "HUD terrain info panel", Color(0.42, 0.35, 0.24, 0.98), 2)
|
||||||
_check_panel_style_fill(failures, scene.forecast_panel, "HUD forecast info panel", Color(0.060, 0.056, 0.048, 0.982))
|
_check_panel_style_fill(failures, scene.forecast_panel, "HUD forecast info panel", Color(0.060, 0.056, 0.048, 0.982))
|
||||||
_check_panel_style_frame(failures, scene.forecast_panel, "HUD forecast info panel", Color(0.42, 0.35, 0.24, 0.98), 2)
|
_check_panel_style_frame(failures, scene.forecast_panel, "HUD forecast info panel", Color(0.42, 0.35, 0.24, 0.98), 2)
|
||||||
|
_check_panel_style_fill(failures, scene.combat_result_panel, "combat result readable frame", Color(0.060, 0.056, 0.048, 0.982))
|
||||||
|
_check_panel_style_frame(failures, scene.combat_result_panel, "combat result readable frame", Color(0.42, 0.35, 0.24, 0.98), 2)
|
||||||
|
if scene.combat_result_panel == null or scene.combat_result_panel.custom_minimum_size != BattleSceneScript.COMBAT_RESULT_PANEL_SIZE:
|
||||||
|
failures.append("combat result report should reserve a stable QHD layout size")
|
||||||
|
scene._on_combat_result_reported({
|
||||||
|
"attacker_name": "조조",
|
||||||
|
"target_name": "황건병",
|
||||||
|
"attacker_merit": 10,
|
||||||
|
"counter_merit": 0,
|
||||||
|
"entries": [{
|
||||||
|
"attacker_name": "조조",
|
||||||
|
"target_name": "황건병",
|
||||||
|
"is_counter": false,
|
||||||
|
"hit": true,
|
||||||
|
"defeated": false,
|
||||||
|
"damage": 18,
|
||||||
|
"hit_chance": 92,
|
||||||
|
"target_hp": 12,
|
||||||
|
"target_max_hp": 30,
|
||||||
|
"directional_bonus": 2,
|
||||||
|
"directional_label": "후방 공격",
|
||||||
|
"effective_bonus": 0
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
if scene.combat_result_panel == null or not scene.combat_result_panel.visible:
|
||||||
|
failures.append("combat result report should appear after a battle result signal")
|
||||||
|
elif not scene.combat_result_primary_label.text.contains("피해") or not scene.combat_result_detail_label.text.contains("공적 +10"):
|
||||||
|
failures.append("combat result report should show damage and merit in readable Korean: %s / %s" % [scene.combat_result_primary_label.text, scene.combat_result_detail_label.text])
|
||||||
|
scene.combat_result_panel.visible = false
|
||||||
_check_panel_uses_flat_readable_backing(failures, scene.title_load_panel, "title load readable panel")
|
_check_panel_uses_flat_readable_backing(failures, scene.title_load_panel, "title load readable panel")
|
||||||
_check_panel_uses_flat_readable_backing(failures, scene.title_settings_panel, "title settings readable panel")
|
_check_panel_uses_flat_readable_backing(failures, scene.title_settings_panel, "title settings readable panel")
|
||||||
_check_panel_uses_flat_readable_backing(failures, _find_descendant_by_name(scene.ui_root_control, "SettingsPanel") as PanelContainer, "battle settings readable panel")
|
_check_panel_uses_flat_readable_backing(failures, _find_descendant_by_name(scene.ui_root_control, "SettingsPanel") as PanelContainer, "battle settings readable panel")
|
||||||
|
|||||||
Reference in New Issue
Block a user