287 lines
9.5 KiB
GDScript
287 lines
9.5 KiB
GDScript
extends SceneTree
|
|
|
|
const BattleSceneScript := preload("res://scripts/scenes/battle_scene.gd")
|
|
|
|
const SAVE_PATH := "user://campaign_save.json"
|
|
const MANUAL_SAVE_PATH := "user://campaign_manual_save.json"
|
|
const SETTINGS_PATH := "user://heros_settings.json"
|
|
const CAPTURE_BASE_DIR := "user://readability_captures"
|
|
const SETTLE_FRAMES := 3
|
|
const VIEWPORT_SPECS := [
|
|
{"label": "desktop", "size": Vector2i(1280, 720)},
|
|
{"label": "qhd", "size": Vector2i(2560, 1440)}
|
|
]
|
|
|
|
|
|
func _init() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
var save_backup := _backup_user_file(SAVE_PATH)
|
|
var manual_backup := _backup_user_file(MANUAL_SAVE_PATH)
|
|
var settings_backup := _backup_user_file(SETTINGS_PATH)
|
|
var failures: Array[String] = []
|
|
var captures: Array[Dictionary] = []
|
|
var output_dir := ""
|
|
_clear_user_file(SAVE_PATH)
|
|
_clear_user_file(MANUAL_SAVE_PATH)
|
|
_clear_user_file(SETTINGS_PATH)
|
|
|
|
if _is_headless_display():
|
|
failures.append("readability frame capture needs a rendering display; run without --headless")
|
|
else:
|
|
output_dir = _make_output_dir(failures)
|
|
if not output_dir.is_empty():
|
|
for spec in VIEWPORT_SPECS:
|
|
await _capture_viewport_set(spec, output_dir, captures, failures)
|
|
_write_manifest(output_dir, captures)
|
|
|
|
if not _restore_user_file(SAVE_PATH, save_backup):
|
|
failures.append("could not restore campaign save after readability capture")
|
|
if not _restore_user_file(MANUAL_SAVE_PATH, manual_backup):
|
|
failures.append("could not restore manual campaign save after readability capture")
|
|
if not _restore_user_file(SETTINGS_PATH, settings_backup):
|
|
failures.append("could not restore settings after readability capture")
|
|
|
|
if failures.is_empty():
|
|
print("readability frame capture ok")
|
|
print("capture directory: %s" % ProjectSettings.globalize_path(output_dir))
|
|
quit(0)
|
|
return
|
|
|
|
for failure in failures:
|
|
push_error(failure)
|
|
quit(1)
|
|
|
|
|
|
func _is_headless_display() -> bool:
|
|
return DisplayServer.get_name().to_lower().contains("headless")
|
|
|
|
|
|
func _capture_viewport_set(spec: Dictionary, output_dir: String, captures: Array[Dictionary], failures: Array[String]) -> void:
|
|
var label := str(spec.get("label", "viewport"))
|
|
var size: Vector2i = spec.get("size", Vector2i(1280, 720))
|
|
_set_viewport_size(size)
|
|
var scene = BattleSceneScript.new()
|
|
get_root().add_child(scene)
|
|
await _settle()
|
|
|
|
await _capture_frame(scene, output_dir, label, "01_title", captures, failures)
|
|
|
|
scene._show_opening_prologue()
|
|
await _settle()
|
|
await _capture_frame(scene, output_dir, label, "02_opening_rebellion", captures, failures)
|
|
|
|
scene.opening_prologue_index = BattleSceneScript.OPENING_PROLOGUE_PAGES.size() - 1
|
|
scene._update_opening_prologue_page()
|
|
await _settle()
|
|
await _capture_frame(scene, output_dir, label, "03_opening_contact", captures, failures)
|
|
|
|
scene._hide_opening_prologue()
|
|
scene._hide_title_menu()
|
|
scene._load_scenario(scene.campaign_state.get_start_scenario_id())
|
|
scene._show_briefing()
|
|
await _settle()
|
|
await _capture_frame(scene, output_dir, label, "04_briefing", captures, failures)
|
|
|
|
scene._start_battle_from_briefing()
|
|
await _settle()
|
|
await _capture_frame(scene, output_dir, label, "05_battle_dialogue", captures, failures)
|
|
|
|
var guard := 0
|
|
while scene._is_dialogue_visible() and guard < 16:
|
|
scene._advance_dialogue()
|
|
guard += 1
|
|
await _settle()
|
|
if scene.objective_notice_panel != null and scene.objective_notice_panel.visible:
|
|
await _capture_frame(scene, output_dir, label, "06_battle_start_notice", captures, failures)
|
|
if scene.objective_notice_panel != null:
|
|
scene.objective_notice_panel.visible = false
|
|
scene._update_hud()
|
|
await _settle()
|
|
await _capture_frame(scene, output_dir, label, "07_battle_idle", captures, failures)
|
|
|
|
_prepare_auto_attack_hint_frame(scene)
|
|
await _settle()
|
|
await _capture_frame(scene, output_dir, label, "08_battle_auto_attack_hint", captures, failures)
|
|
|
|
scene._update_threat_button()
|
|
scene._on_command_hint_button_mouse_entered(scene.threat_button)
|
|
await _settle()
|
|
await _capture_frame(scene, output_dir, label, "09_battle_threat_hint", captures, failures)
|
|
|
|
_prepare_post_move_action_frame(scene, "")
|
|
await _settle()
|
|
await _capture_frame(scene, output_dir, label, "10_post_move_actions", captures, failures)
|
|
|
|
_prepare_post_move_action_frame(scene, "tactic")
|
|
await _settle()
|
|
await _capture_frame(scene, output_dir, label, "11_post_move_tactic_picker", captures, failures)
|
|
|
|
_prepare_post_move_action_frame(scene, "item")
|
|
await _settle()
|
|
await _capture_frame(scene, output_dir, label, "12_post_move_item_picker", captures, failures)
|
|
|
|
scene.queue_free()
|
|
await _settle()
|
|
|
|
|
|
func _prepare_auto_attack_hint_frame(scene) -> void:
|
|
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(1, 3)
|
|
cao_cao["moved"] = false
|
|
cao_cao["acted"] = false
|
|
enemy["pos"] = Vector2i(4, 3)
|
|
enemy["alive"] = true
|
|
enemy["hp"] = maxi(1, int(enemy.get("hp", 24)))
|
|
scene.state.select_unit("cao_cao")
|
|
scene.hover_cell = Vector2i(4, 3)
|
|
scene._refresh_ranges()
|
|
scene._focus_board_on_cell(Vector2i(4, 3))
|
|
scene._update_hud()
|
|
scene.queue_redraw()
|
|
|
|
|
|
func _prepare_post_move_action_frame(scene, picker_mode: String) -> 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
|
|
scene.state.set_inventory_snapshot({"bean": 2})
|
|
var cao_cao: Dictionary = scene.state.get_unit("cao_cao")
|
|
var enemy: Dictionary = scene.state.get_unit("yellow_turban_1")
|
|
var xiahou_dun: Dictionary = scene.state.get_unit("xiahou_dun")
|
|
if cao_cao.is_empty() or enemy.is_empty() or xiahou_dun.is_empty():
|
|
return
|
|
cao_cao["pos"] = Vector2i(1, 3)
|
|
cao_cao["moved"] = false
|
|
cao_cao["acted"] = false
|
|
cao_cao["mp"] = maxi(10, int(cao_cao.get("mp", 0)))
|
|
enemy["pos"] = Vector2i(3, 3)
|
|
enemy["alive"] = true
|
|
enemy["hp"] = maxi(18, int(enemy.get("hp", 0)))
|
|
xiahou_dun["pos"] = Vector2i(2, 4)
|
|
xiahou_dun["alive"] = true
|
|
xiahou_dun["hp"] = 12
|
|
scene.state.select_unit("cao_cao")
|
|
scene._refresh_ranges()
|
|
scene._focus_board_on_cell(Vector2i(2, 3))
|
|
scene._handle_board_click(_screen_for_cell(scene, Vector2i(2, 3)))
|
|
if picker_mode == "tactic":
|
|
scene._on_post_move_tactic_pressed()
|
|
elif picker_mode == "item":
|
|
scene._on_post_move_item_pressed()
|
|
scene._update_hud()
|
|
scene.queue_redraw()
|
|
|
|
|
|
func _screen_for_cell(scene, cell: Vector2i) -> Vector2:
|
|
var rect: Rect2 = scene._rect_for_cell(cell)
|
|
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:
|
|
var texture := get_root().get_texture()
|
|
if texture == null:
|
|
failures.append("viewport texture missing for %s/%s" % [viewport_label, frame_label])
|
|
return
|
|
var image := texture.get_image()
|
|
if image == null or image.is_empty():
|
|
failures.append("viewport image missing for %s/%s" % [viewport_label, frame_label])
|
|
return
|
|
var path := "%s/%s_%s.png" % [output_dir, viewport_label, frame_label]
|
|
var result := image.save_png(path)
|
|
if result != OK:
|
|
failures.append("could not save capture %s: %s" % [path, str(result)])
|
|
return
|
|
captures.append({
|
|
"viewport": viewport_label,
|
|
"frame": frame_label,
|
|
"path": ProjectSettings.globalize_path(path),
|
|
"size": [image.get_width(), image.get_height()]
|
|
})
|
|
|
|
|
|
func _settle() -> void:
|
|
for _index in range(SETTLE_FRAMES):
|
|
await process_frame
|
|
|
|
|
|
func _set_viewport_size(size: Vector2i) -> void:
|
|
get_root().size = size
|
|
DisplayServer.window_set_size(size)
|
|
|
|
|
|
func _make_output_dir(failures: Array[String]) -> String:
|
|
var stamp := _safe_timestamp()
|
|
var output_dir := "%s/%s" % [CAPTURE_BASE_DIR, stamp]
|
|
var absolute_dir := ProjectSettings.globalize_path(output_dir)
|
|
var result := DirAccess.make_dir_recursive_absolute(absolute_dir)
|
|
if result != OK:
|
|
failures.append("could not create capture directory %s: %s" % [absolute_dir, str(result)])
|
|
return ""
|
|
return output_dir
|
|
|
|
|
|
func _safe_timestamp() -> String:
|
|
var stamp := Time.get_datetime_string_from_system(false, true)
|
|
for token in [":", "-", "T", " "]:
|
|
stamp = stamp.replace(token, "")
|
|
return stamp
|
|
|
|
|
|
func _write_manifest(output_dir: String, captures: Array[Dictionary]) -> void:
|
|
var manifest := {
|
|
"generated_at": Time.get_datetime_string_from_system(false, true),
|
|
"project": "Heroes of Three Kingdoms",
|
|
"purpose": "readability QA reference frames",
|
|
"captures": captures
|
|
}
|
|
var file := FileAccess.open("%s/manifest.json" % output_dir, FileAccess.WRITE)
|
|
if file != null:
|
|
file.store_string(JSON.stringify(manifest, "\t"))
|
|
|
|
|
|
func _backup_user_file(path: String) -> Dictionary:
|
|
if not FileAccess.file_exists(path):
|
|
return {"exists": false, "text": ""}
|
|
return {
|
|
"exists": true,
|
|
"text": FileAccess.get_file_as_string(path)
|
|
}
|
|
|
|
|
|
func _restore_user_file(path: String, backup: Dictionary) -> bool:
|
|
if bool(backup.get("exists", false)):
|
|
var file := FileAccess.open(path, FileAccess.WRITE)
|
|
if file == null:
|
|
return false
|
|
file.store_string(str(backup.get("text", "")))
|
|
return true
|
|
return _clear_user_file(path)
|
|
|
|
|
|
func _clear_user_file(path: String) -> bool:
|
|
if not FileAccess.file_exists(path):
|
|
return true
|
|
return DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) == OK
|