extends SceneTree ## Rendered character-art gate for the focused Sakura second-skin pilot. ## Captures open/deploying/closed helmet states and verifies that active jet ## flames emerge from all four integrated vector-jet mouths. var _scene: Node3D var _camera: Camera3D var _models: Array[SkinnedPlayerModel] = [] var _jets: Array[PlayerJetVFX] = [] var _out_dir := "." func _initialize() -> void: var args := OS.get_cmdline_user_args() if not args.is_empty(): _out_dir = args[0] if not _out_dir.is_absolute_path(): _out_dir = ProjectSettings.globalize_path(_out_dir) DirAccess.make_dir_recursive_absolute(_out_dir) _run() func _run() -> void: _scene = Node3D.new() root.add_child(_scene) current_scene = _scene _build_studio() _add_pilot("default", "sakura", "res://assets/characters/skins/sakura_pilot_hd.glb", 0.0) for _i in 24: await process_frame for model in _models: model.update_state("ground", 0.0, false) model.set_locomotion(0.0, 1.0, 0.0) _camera.global_position = Vector3(0.0, 1.05, -3.2) _camera.look_at(Vector3(0.0, 0.92, 0.0), Vector3.UP) await _capture("pilot_open") _camera.global_position = Vector3(0.0, 1.58, -1.15) _camera.look_at(Vector3(0.0, 1.57, 0.0), Vector3.UP) await _capture("helmet_open_closeup") _models[0].set_helmet_closed(true) for _i in 5: await process_frame await _capture("pilot_helmet_deploy") for _i in 40: await process_frame await _capture("pilot_helmet_closed") _camera.global_position = Vector3(0.0, 1.08, 3.2) _camera.look_at(Vector3(0.0, 0.92, 0.0), Vector3.UP) await _capture("pilot_rear") for jet in _jets: jet.burst_double_jump() for _i in 4: await process_frame await _capture("pilot_thrusters") _models[0].set_wall_side(-1.0) _models[0].set_wall_glide_motion(Vector3.RIGHT * 13.0) for _i in 24: _models[0].update_state("wall_run", 13.0, false) _jets[0].set_wall_glide(true, Vector2.RIGHT) await process_frame _camera.global_position = Vector3(2.4, 1.45, -2.7) _camera.look_at(Vector3(0.0, 1.0, 0.0), Vector3.UP) await _capture("pilot_wall_glide") var failed := false if not _models[0].helmet_closed_debug() \ or _models[0].helmet_progress_debug() < 0.98: push_error("ANIME_MECHA_CAPTURE helmet failed to finish deployment") failed = true for model_index in _models.size(): var model := _models[model_index] var resolved_roles: Array[String] = [] var missing_roles: Array[String] = [] for role in ["hips", "shoulder.L", "shoulder.R", "upper_arm.L", "upper_arm.R", "forearm.L", "forearm.R", "thigh.L", "thigh.R", "shin.L", "shin.R", "foot.L", "foot.R"]: if model._role_bone(role, []) >= 0: resolved_roles.append(role) else: missing_roles.append(role) print("ANIME_MECHA_CAPTURE %s: %d detail parts, %d nozzles, " % [ model.skin_id, model.mecha_detail_part_count_debug(), model.mecha_nozzle_count_debug()], "resolved=", resolved_roles, " missing=", missing_roles) if not missing_roles.is_empty(): var bone_names: Array[String] = [] if model.skeleton: for bone_index in model.skeleton.get_bone_count(): bone_names.append(model.skeleton.get_bone_name(bone_index)) print("ANIME_MECHA_CAPTURE unresolved sidecar roles=", model._rig_info.get("roles", {}), " skeleton bones=", bone_names) if model.mecha_detail_part_count_debug() < 180: push_error("ANIME_MECHA_CAPTURE insufficient hardware detail on %s: %d parts" % [ model.skin_id, model.mecha_detail_part_count_debug()]) failed = true if model.mecha_nozzle_count_debug() != 4: push_error("ANIME_MECHA_CAPTURE missing four visible nozzles on %s" % model.skin_id) failed = true var nozzles := model.get_jet_socket_world_positions() var flames := _jets[model_index].get_flame_world_positions() if nozzles.size() != 4 or flames.size() != 4: push_error("ANIME_MECHA_CAPTURE socket count mismatch on %s" % model.skin_id) failed = true continue for nozzle_index in 4: if nozzles[nozzle_index].distance_to(flames[nozzle_index]) > 0.001: push_error("ANIME_MECHA_CAPTURE flame %d misses %s nozzle" % [ nozzle_index, model.skin_id]) failed = true print("ANIME_MECHA_CAPTURE %s" % ("FAIL" if failed else "PASS")) quit(1 if failed else 0) func _build_studio() -> void: var world := WorldEnvironment.new() var environment := Environment.new() environment.background_mode = Environment.BG_COLOR environment.background_color = Color("b8c0cc") environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR environment.ambient_light_color = Color("e8edf6") environment.ambient_light_energy = 1.15 environment.tonemap_mode = Environment.TONE_MAPPER_FILMIC world.environment = environment _scene.add_child(world) var sun := DirectionalLight3D.new() sun.rotation_degrees = Vector3(-42.0, -28.0, 0.0) sun.light_energy = 1.9 _scene.add_child(sun) _camera = Camera3D.new() _camera.fov = 38.0 _scene.add_child(_camera) _camera.current = true func _add_pilot(id: String, theme: String, path: String, x: float) -> void: var model := SkinnedPlayerModel.new() model.skin_id = id model.mecha_theme = theme model.model_path = path model.position.x = x _scene.add_child(model) # Static art QA does not need the expensive full-quality hair/cloth solver; # gameplay and the picker still use their normal distance-driven LOD. if model._spring_mod: model._spring_mod.lod = 3 model._spring_mod = null _models.append(model) var jet := PlayerJetVFX.new() jet.name = "JetVFX_%s" % id _scene.add_child(jet) jet.set_visual_model(model) jet.set_render_enabled(true) _jets.append(jet) func _capture(tag: String) -> void: for _i in 5: await process_frame var path := _out_dir.path_join(tag + ".png") root.get_texture().get_image().save_png(path) print("ANIME_MECHA_CAPTURE saved ", path)