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() # Disposable Demo fixture funds exact Doran signing_cost 300; DEFAULT5000 and paid hire stay unchanged. state.resources[&"gold"] += 300 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}) var load_menu := main.find_child("SaveLoadMenu", true, false) as MenuButton if load_menu != null: load_menu.disabled = true load_menu.tooltip_text = "この見学モードは保存・読込を行いません。" 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