Files
heros/tools/smoke_directional_combat.gd

286 lines
13 KiB
GDScript

extends SceneTree
const BattleStateScript := preload("res://scripts/core/battle_state.gd")
const BattleSceneScript := preload("res://scripts/scenes/battle_scene.gd")
func _init() -> void:
var failures: Array[String] = []
_check_directional_damage_bonuses(failures)
_check_attack_updates_facing(failures)
_check_move_cancel_restores_facing(failures)
_check_scene_pixel_facing(failures)
_check_scene_directional_attack_badges(failures)
if failures.is_empty():
print("directional combat smoke ok")
quit(0)
return
for failure in failures:
push_error(failure)
quit(1)
func _check_directional_damage_bonuses(failures: Array[String]) -> void:
var state = _loaded_state(failures, "directional damage")
if state == null:
return
var attacker: Dictionary = state.get_unit("cao_cao")
var target: Dictionary = state.get_unit("yellow_turban_1")
if attacker.is_empty() or target.is_empty():
failures.append("missing units for directional damage")
return
_configure_duel_units(attacker, target)
target["pos"] = Vector2i(5, 9)
target["facing_x"] = 1
attacker["pos"] = Vector2i(6, 9)
var front_damage: int = state.calculate_damage(attacker, target)
var front_info: Dictionary = state.get_directional_attack_info(attacker, target)
attacker["pos"] = Vector2i(5, 8)
var side_damage: int = state.calculate_damage(attacker, target)
var side_info: Dictionary = state.get_directional_attack_info(attacker, target)
attacker["pos"] = Vector2i(4, 9)
var back_damage: int = state.calculate_damage(attacker, target)
var back_info: Dictionary = state.get_directional_attack_info(attacker, target)
if str(front_info.get("kind", "")) != BattleStateScript.DIRECTIONAL_FRONT_ATTACK:
failures.append("front attack should be classified as front: %s" % str(front_info))
if str(side_info.get("kind", "")) != BattleStateScript.DIRECTIONAL_SIDE_ATTACK:
failures.append("same-file attack should be classified as side: %s" % str(side_info))
if str(back_info.get("kind", "")) != BattleStateScript.DIRECTIONAL_BACK_ATTACK:
failures.append("rear attack should be classified as back: %s" % str(back_info))
if side_damage != front_damage + BattleStateScript.DIRECTIONAL_SIDE_DAMAGE_BONUS:
failures.append("side damage should add side bonus: front %d side %d" % [front_damage, side_damage])
if back_damage != front_damage + BattleStateScript.DIRECTIONAL_BACK_DAMAGE_BONUS:
failures.append("back damage should add back bonus: front %d back %d" % [front_damage, back_damage])
var preview: Dictionary = state.get_damage_preview("cao_cao", "yellow_turban_1")
if str(preview.get("directional_attack", "")) != BattleStateScript.DIRECTIONAL_BACK_ATTACK:
failures.append("damage preview should expose rear attack: %s" % str(preview))
if int(preview.get("directional_bonus", 0)) != BattleStateScript.DIRECTIONAL_BACK_DAMAGE_BONUS:
failures.append("damage preview should expose rear bonus: %s" % str(preview))
func _check_attack_updates_facing(failures: Array[String]) -> void:
var state = _loaded_state(failures, "attack facing")
if state == null:
return
var attacker: Dictionary = state.get_unit("cao_cao")
var target: Dictionary = state.get_unit("yellow_turban_1")
if attacker.is_empty() or target.is_empty():
failures.append("missing units for attack facing")
return
_configure_duel_units(attacker, target)
attacker["pos"] = Vector2i(4, 9)
attacker["facing_x"] = -1
target["pos"] = Vector2i(5, 9)
target["facing_x"] = 1
target["controllable"] = false
var expected_damage: int = state.calculate_damage(attacker, target)
var start_hp: int = int(target.get("hp", 0))
if not state.select_unit("cao_cao"):
failures.append("could not select Cao Cao for attack facing")
return
if not state.try_attack_selected("yellow_turban_1"):
failures.append("rear attack should execute")
return
if int(target.get("hp", 0)) != start_hp - expected_damage:
failures.append("rear attack damage mismatch: expected %d got %d" % [expected_damage, start_hp - int(target.get("hp", 0))])
if state.unit_facing_x(attacker) != 1:
failures.append("attacker should face target after attack")
func _check_move_cancel_restores_facing(failures: Array[String]) -> void:
var state = _loaded_state(failures, "move cancel facing")
if state == null:
return
var unit: Dictionary = state.get_unit("cao_cao")
if unit.is_empty():
failures.append("missing Cao Cao for move cancel facing")
return
unit["facing_x"] = -1
if not state.select_unit("cao_cao"):
failures.append("could not select Cao Cao for move cancel facing")
return
if not state.try_move_selected(Vector2i(2, 3), true):
failures.append("pending move should succeed for facing cancel")
return
if state.unit_facing_x(unit) != 1:
failures.append("pending move to the east should face right")
if not state.cancel_pending_move("cao_cao", Vector2i(1, 3), Vector2i(2, 3)):
failures.append("pending move cancel should succeed for facing restore")
return
if state.unit_facing_x(unit) != -1:
failures.append("canceling a pending move should restore previous facing")
func _check_scene_pixel_facing(failures: Array[String]) -> void:
var scene = BattleSceneScript.new()
if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
failures.append("could not load opening battle for pixel facing")
scene.free()
return
var player: Dictionary = scene.state.get_unit("cao_cao")
var enemy: Dictionary = scene.state.get_unit("yellow_turban_1")
player["facing_x"] = -1
enemy["facing_x"] = 1
if scene._unit_pixel_facing(player) != -1:
failures.append("pixel facing should use player facing_x")
if scene._unit_pixel_facing(enemy) != 1:
failures.append("pixel facing should use enemy facing_x")
if scene._unit_facing_text(player) != "서쪽":
failures.append("unit facing text should localize west-facing units")
if scene._unit_facing_text(enemy) != "동쪽":
failures.append("unit facing text should localize east-facing units")
var marker_rect := Rect2(Vector2(12.0, 20.0), Vector2(scene.TILE_SIZE, scene.TILE_SIZE))
var left_points: PackedVector2Array = scene._unit_facing_marker_points(marker_rect, player)
var right_points: PackedVector2Array = scene._unit_facing_marker_points(marker_rect, enemy)
if left_points.size() != 3 or right_points.size() != 3:
failures.append("unit facing marker should draw a compact triangular marker")
elif left_points[0].x >= left_points[1].x or right_points[0].x <= right_points[1].x:
failures.append("unit facing marker should point toward the unit facing: left=%s right=%s" % [str(left_points), str(right_points)])
var closed_points: PackedVector2Array = scene._closed_polyline_points(left_points)
if closed_points.size() != 4 or closed_points[0] != closed_points[3]:
failures.append("unit facing marker outline should close the triangle: %s" % str(closed_points))
var marker_color: Color = scene._unit_facing_marker_fill_color(player, Color(0.2, 0.4, 0.8, 1.0))
if marker_color.a <= 0.0:
failures.append("unit facing marker fill should be visible")
scene.unit_action_motion_by_unit["cao_cao"] = {
"from": Vector2i(4, 3),
"to": Vector2i(3, 3)
}
if scene._unit_pixel_facing(player) != -1:
failures.append("pixel facing should prefer active action motion")
scene.free()
func _check_scene_directional_attack_badges(failures: Array[String]) -> void:
var scene = BattleSceneScript.new()
if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
failures.append("could not load opening battle for directional attack badges")
scene.free()
return
var attacker: Dictionary = scene.state.get_unit("cao_cao")
var target: Dictionary = scene.state.get_unit("yellow_turban_1")
if attacker.is_empty() or target.is_empty():
failures.append("missing units for directional attack badge")
scene.free()
return
for unit in scene.state.units:
if str(unit.get("id", "")) != "cao_cao" and str(unit.get("id", "")) != "yellow_turban_1":
unit["alive"] = false
_configure_duel_units(attacker, target)
attacker["move"] = 4
attacker["pos"] = Vector2i(4, 9)
target["pos"] = Vector2i(5, 9)
target["facing_x"] = 1
target["controllable"] = false
scene.battle_started = true
if not scene.state.select_unit("cao_cao"):
failures.append("could not select Cao Cao for directional attack badge")
scene.free()
return
scene._refresh_ranges()
scene.hover_cell = Vector2i(5, 9)
var badge := scene._target_preview_badge()
if not str(badge.get("text", "")).contains("후+%d" % BattleStateScript.DIRECTIONAL_BACK_DAMAGE_BONUS):
failures.append("rear attack hover badge should expose compact rear bonus: %s" % str(badge))
if not str(badge.get("tooltip", "")).contains("후방 공격 +%d 피해" % BattleStateScript.DIRECTIONAL_BACK_DAMAGE_BONUS):
failures.append("rear attack hover badge should explain rear damage in tooltip: %s" % str(badge))
var rear_detail_text := scene._target_preview_detail_badge_text(str(badge.get("tooltip", "")))
if not rear_detail_text.contains("후방 공격 +%d 피해" % BattleStateScript.DIRECTIONAL_BACK_DAMAGE_BONUS):
failures.append("rear attack detail strip should prioritize rear damage: %s" % rear_detail_text)
var marker_text := ""
var marker_tooltip := ""
for marker in scene._attack_target_marker_entries():
if str(marker.get("target_id", "")) == "yellow_turban_1":
marker_text = str(marker.get("text", ""))
marker_tooltip = str(marker.get("tooltip", ""))
if not marker_text.contains("후+%d" % BattleStateScript.DIRECTIONAL_BACK_DAMAGE_BONUS):
failures.append("rear attack target marker should expose compact rear bonus: %s" % marker_text)
if not marker_tooltip.contains("후방 공격 +%d 피해" % BattleStateScript.DIRECTIONAL_BACK_DAMAGE_BONUS):
failures.append("rear attack target marker should explain rear damage in tooltip: %s" % marker_tooltip)
attacker["pos"] = Vector2i(5, 8)
scene._refresh_ranges()
scene.hover_cell = Vector2i(5, 9)
var side_badge: Dictionary = scene._target_preview_badge()
if not str(side_badge.get("text", "")).contains("측+%d" % BattleStateScript.DIRECTIONAL_SIDE_DAMAGE_BONUS):
failures.append("side attack hover badge should expose compact side bonus: %s" % str(side_badge))
if not str(side_badge.get("tooltip", "")).contains("측면 공격 +%d 피해" % BattleStateScript.DIRECTIONAL_SIDE_DAMAGE_BONUS):
failures.append("side attack hover badge should explain side damage in tooltip: %s" % str(side_badge))
var side_detail_text := scene._target_preview_detail_badge_text(str(side_badge.get("tooltip", "")))
if not side_detail_text.contains("측면 공격 +%d 피해" % BattleStateScript.DIRECTIONAL_SIDE_DAMAGE_BONUS):
failures.append("side attack detail strip should prioritize side damage: %s" % side_detail_text)
attacker["pos"] = Vector2i(2, 9)
attacker["facing_x"] = -1
target["pos"] = Vector2i(5, 9)
target["facing_x"] = 1
scene._refresh_ranges()
scene.hover_cell = Vector2i(5, 9)
var auto_badge: Dictionary = scene._target_preview_badge()
if str(auto_badge.get("text", "")).contains("행공") or not str(auto_badge.get("tooltip", "")).contains("이동 후 공격"):
failures.append("auto-move attack hover badge should use a clear hint instead of jargon: %s" % str(auto_badge))
if not str(auto_badge.get("text", "")).contains("후+%d" % BattleStateScript.DIRECTIONAL_BACK_DAMAGE_BONUS):
failures.append("auto-move attack hover badge should expose compact rear bonus: %s" % str(auto_badge))
if not str(auto_badge.get("tooltip", "")).contains("후방 공격 +%d 피해" % BattleStateScript.DIRECTIONAL_BACK_DAMAGE_BONUS):
failures.append("auto-move attack hover badge should explain rear damage in tooltip: %s" % str(auto_badge))
var auto_detail_text := scene._target_preview_detail_badge_text(str(auto_badge.get("tooltip", "")))
if not auto_detail_text.contains("후방 공격 +%d 피해" % BattleStateScript.DIRECTIONAL_BACK_DAMAGE_BONUS):
failures.append("auto-move attack detail strip should prioritize rear damage: %s" % auto_detail_text)
if scene._compact_directional_badge_label(BattleStateScript.DIRECTIONAL_SIDE_ATTACK) != "":
failures.append("directional badge helper should compact side attacks")
if not scene._format_directional_forecast_text({
"directional_bonus": BattleStateScript.DIRECTIONAL_BACK_DAMAGE_BONUS,
"directional_label": "후방"
}, "directional_bonus", "directional_label").contains("후방 공격 +%d" % BattleStateScript.DIRECTIONAL_BACK_DAMAGE_BONUS):
failures.append("directional forecast helper should explain rear attack bonus")
if scene._format_directional_badge_suffix({
"directional_attack": BattleStateScript.DIRECTIONAL_FRONT_ATTACK,
"directional_bonus": 0,
"directional_label": "정면"
}) != "":
failures.append("front/no-bonus directional badge suffix should stay empty")
scene.free()
func _configure_duel_units(attacker: Dictionary, target: Dictionary) -> void:
attacker["atk"] = 30
attacker["agi"] = 50
attacker["hp"] = 80
attacker["max_hp"] = 80
attacker["range"] = 1
attacker["min_range"] = 1
attacker["acted"] = false
attacker["moved"] = false
target["def"] = 10
target["atk"] = 1
target["agi"] = 0
target["hp"] = 80
target["max_hp"] = 80
target["range"] = 1
target["min_range"] = 1
target["alive"] = true
target["acted"] = false
target["moved"] = false
func _loaded_state(failures: Array[String], label: String):
var state = BattleStateScript.new()
if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
failures.append("%s could not load opening battle" % label)
return null
return state