Files
heros/tools/capture_readability_frames.gd

231 lines
7.6 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)
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 _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