Files
TheTree_mock/tests/ui/capture_pixel_tower.gd
T

185 lines
7.8 KiB
GDScript

extends SceneTree
## godot --path . --script res://tests/ui/capture_pixel_tower.gd
## THE_TREE_PIXEL_PLAY=1 keeps an interactive, disposable scenario open instead.
## THE_TREE_CAPTURE_SMOKE=1 validates the real scenario with --headless.
## Optional: THE_TREE_CAPTURE_PATH, WIDTH, HEIGHT, MINUTES, GAP_MINUTES, LAYER.
## THE_TREE_CAPTURE_ZOOM=1 (0.5..4); FOCUS_ROOMS=1 centers the built room area.
## THE_TREE_CAPTURE_ANIMATION=1 also saves 72 live frames at 12 fps.
## PATH names the first PNG; the second PNG and a state JSON use its basename.
const Scenario := preload("res://scripts/ui/pixel_tower_demo.gd")
const DEFAULT_PATH := "res://artifacts/pixel-tower/pixel-tower.png"
var _scenario: RefCounted
var _runtime: Variant
var _clock: Variant
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_clock = root.get_node("GameClock")
_runtime = root.get_node("SimulationRuntime")
var state: Variant = root.get_node("GameState")
_clock.reset(1, 6, 0)
state.reset()
var initialized: Dictionary = _runtime.initialize(_clock, state, 424242)
_clock.set_paused(true)
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
if not bool(initialized.get(&"valid", false)):
push_error("Could not initialize pixel viewing scenario.")
quit(1)
return
_scenario = Scenario.new()
if not await _scenario.prepare(_runtime, _clock, self):
quit(1)
return
if not await _scenario.advance(_env_int("THE_TREE_CAPTURE_MINUTES", 83, 0, 180)):
quit(1)
return
if OS.get_environment("THE_TREE_CAPTURE_SMOKE") == "1":
print("PIXEL_SCENARIO ", JSON.stringify(_scenario.describe()))
quit(0)
return
var capture_size := Vector2i(_env_int("THE_TREE_CAPTURE_WIDTH", 1440, 1024, 7680),
_env_int("THE_TREE_CAPTURE_HEIGHT", 810, 640, 4320))
var play := OS.get_environment("THE_TREE_PIXEL_PLAY") == "1"
var container: Node = root
var capture_viewport: SubViewport
if play:
root.size = capture_size
root.title = "The Tree · ドット絵の見学(保存なし)"
else:
capture_viewport = SubViewport.new()
capture_viewport.size = capture_size
capture_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
root.add_child(capture_viewport)
container = capture_viewport
var main := load("res://scenes/main/main.tscn").instantiate() as Control
if main == null or not main.has_method("_set_layer"):
push_error("Main scene did not load; run godot --headless --editor --path . --import first.")
quit(ERR_CANT_ACQUIRE_RESOURCE)
return
container.add_child(main)
main.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
if main.get("world_preview") == null:
push_error("Main scene failed to create the live world preview.")
quit(ERR_CANT_ACQUIRE_RESOURCE)
return
# main enables its production save policy in _ready; immediately turn it off
# for this disposable scenario and make the manual save/load controls inert.
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
for button: Node in main.find_children("*", "Button", true, false):
var save_control := button as Button
for connection: Dictionary in save_control.pressed.get_connections():
var callback: Callable = connection[&"callable"]
if callback.get_method() in [&"_save_manual", &"_load_manual"]:
save_control.disabled = true
save_control.tooltip_text = "この見学モードは保存しません。"
var layer := _env_int("THE_TREE_CAPTURE_LAYER", 0, 0, 1)
main.call("_set_layer", layer)
main.call("_clear_active_tool")
for frame: int in 8:
await process_frame
var preview: Variant = main.get("world_preview")
preview.set_zoom(_env_float("THE_TREE_CAPTURE_ZOOM", 1.0, 0.5, 4.0))
if OS.get_environment("THE_TREE_CAPTURE_FOCUS_ROOMS") == "1":
preview.pan_offset += preview.size * 0.5 - preview.cell_to_canvas(Vector3i(-2, 1, layer))
preview.call("_clamp_pan")
preview.queue_redraw()
if play:
main.call("_set_status", "ドット絵の見学:実際の客・従業員・建設済み施設で運営中(保存なし)", true, true)
_clock.set_paused(false)
print("PIXEL_PLAY: disposable scenario ready; normal saves are disabled.")
return
var path := OS.get_environment("THE_TREE_CAPTURE_PATH")
if path.is_empty():
path = DEFAULT_PATH
var absolute_path := ProjectSettings.globalize_path(path)
var directory_result := DirAccess.make_dir_recursive_absolute(absolute_path.get_base_dir())
if directory_result != OK:
push_error("Could not create capture directory.")
quit(directory_result)
return
var records: Array[Dictionary] = []
for index: int in 2:
for frame: int in 8:
await process_frame
await RenderingServer.frame_post_draw
var image := capture_viewport.get_texture().get_image()
if image == null or image.is_empty():
push_error("Capture needs a rendering display; use CAPTURE_SMOKE for headless validation.")
quit(ERR_CANT_ACQUIRE_RESOURCE)
return
var output := absolute_path if index == 0 else absolute_path.get_basename() + "-later.png"
var result := image.save_png(output)
if result != OK:
push_error("Could not write %s: %s" % [output, error_string(result)])
quit(result)
return
var description: Dictionary = _scenario.describe()
description["image"] = output
description["tracked_actor_count"] = (preview.get("_actor_tracks") as Dictionary).size()
description["viewport_size"] = [capture_size.x, capture_size.y]
records.append(description)
print("PIXEL_CAPTURE: %s at %s" % [output, _clock.formatted_time()])
if index == 0 and not await _scenario.advance(_env_int("THE_TREE_CAPTURE_GAP_MINUTES", 18, 1, 180)):
quit(1)
return
var animation: Dictionary = {}
if OS.get_environment("THE_TREE_CAPTURE_ANIMATION") == "1":
var frames_path := absolute_path.get_basename() + "-frames"
var frames_directory_result := DirAccess.make_dir_recursive_absolute(frames_path)
if frames_directory_result != OK:
push_error("Could not create animation frames directory.")
quit(frames_directory_result)
return
animation = {"directory": frames_path, "fps": 12, "frame_count": 72,
"start": _scenario.describe()}
_clock.set_speed(1)
_clock.set_paused(false)
main.call("_update_pause_and_speed")
# Normal real-time processing owns the clock and actor movement here.
# Never manually advance simulation ticks or animation time for a frame.
for frame: int in 72:
await create_timer(1.0 / 12.0).timeout
await RenderingServer.frame_post_draw
var frame_image := capture_viewport.get_texture().get_image()
if frame_image == null or frame_image.is_empty():
_clock.set_paused(true)
push_error("Could not read a live animation frame.")
quit(ERR_CANT_ACQUIRE_RESOURCE)
return
var frame_output := frames_path.path_join("%03d.png" % frame)
var frame_result := frame_image.save_png(frame_output)
if frame_result != OK:
_clock.set_paused(true)
push_error("Could not write %s: %s" % [frame_output, error_string(frame_result)])
quit(frame_result)
return
_clock.set_paused(true)
animation["end"] = _scenario.describe()
print("PIXEL_ANIMATION: %s (72 frames at 12 fps)" % frames_path)
var manifest := FileAccess.open(absolute_path.get_basename() + "-state.json", FileAccess.WRITE)
if manifest == null:
quit(FileAccess.get_open_error())
return
var manifest_data := {"scenario": "disposable production simulation", "captures": records}
if not animation.is_empty():
manifest_data["animation"] = animation
manifest.store_string(JSON.stringify(manifest_data, "\t"))
quit(0)
func _env_int(name: String, fallback: int, minimum: int, maximum: int) -> int:
var value := OS.get_environment(name)
return clampi(int(value), minimum, maximum) if value.is_valid_int() else fallback
func _env_float(name: String, fallback: float, minimum: float, maximum: float) -> float:
var value := OS.get_environment(name)
return clampf(float(value), minimum, maximum) if value.is_valid_float() else fallback