286 lines
13 KiB
GDScript
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
|