class_name OperatorGallery extends PanelContainer var ui: GameUI var viewport := SubViewport.new() var world := Node3D.new() var model: ImportedOperator var camera := Camera3D.new() var clips := OptionButton.new() var scrub := HSlider.new() var status := Label.new() var playing := true var elapsed := 0.0 var selected := "steve" var yaw := 0.0 var distance := 2.5 func _ready() -> void: set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) var background := StyleBoxFlat.new() background.bg_color = Color("101820") add_theme_stylebox_override("panel", background) var margin := MarginContainer.new() for side: String in ["left", "right", "top", "bottom"]: margin.add_theme_constant_override("margin_" + side, 24) add_child(margin) var column := VBoxContainer.new() column.add_theme_constant_override("separation", 12) margin.add_child(column) var title := Label.new() title.text = "OPERATOR / ANIMATIONSSTUDIO" title.add_theme_font_size_override("font_size", 28) column.add_child(title) var row := HBoxContainer.new() column.add_child(row) var select := OptionButton.new() select.add_item("STEVE / SOLDAT") select.add_item("GAS MASK / SWAT") select.selected = 1 if ui.game.operator_id == "gas_mask" else 0 row.add_child(select) select.item_selected.connect(func(index: int): select_model("gas_mask" if index == 1 else "steve")) var equip := Button.new() equip.text = "OPERATOR AUSRÜSTEN" row.add_child(equip) equip.pressed.connect(func(): ui.game.operator_id = selected ui.preferences_changed.emit() status.text = "Gespeichert. Dein Operator ist im nächsten Match aktiv.") var preview := SubViewportContainer.new() preview.size_flags_vertical = Control.SIZE_EXPAND_FILL preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL preview.stretch = true preview.custom_minimum_size = Vector2(300, 220) column.add_child(preview) viewport.own_world_3d = true viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS viewport.msaa_3d = get_viewport().msaa_3d preview.add_child(viewport) viewport.add_child(world) preview.gui_input.connect(inspect_input) world.add_child(camera) camera.fov = 45 camera.make_current() var environment := WorldEnvironment.new() environment.environment = Environment.new() environment.environment.background_mode = Environment.BG_COLOR environment.environment.background_color = Color("151e29") environment.environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR environment.environment.ambient_light_color = Color("b4cae2") environment.environment.ambient_light_energy = 0.75 world.add_child(environment) var key := DirectionalLight3D.new() key.rotation_degrees = Vector3(-35, -30, 0) key.light_energy = 1.6 key.shadow_enabled = true world.add_child(key) var fill := DirectionalLight3D.new() fill.rotation_degrees = Vector3(-20, 145, 0) fill.light_energy = 0.8 fill.light_color = Color("8cb8ed") world.add_child(fill) var floor_mesh := MeshInstance3D.new() var plane := PlaneMesh.new() plane.size = Vector2(8, 8) floor_mesh.mesh = plane var mat := StandardMaterial3D.new() mat.albedo_color = Color("283440") mat.roughness = 0.9 floor_mesh.material_override = mat world.add_child(floor_mesh) select_model("gas_mask" if select.selected == 1 else "steve") for clip: String in OperatorPose.library.get_animation_list(): clips.add_item(clip.replace("__", " / ").replace("_", " ")) clips.set_item_metadata(clips.item_count - 1, clip) if clip == "pro_rifle__idle_aiming": clips.selected = clips.item_count - 1 clips.item_selected.connect(func(_index: int): elapsed = 0) column.add_child(clips) var playback := HBoxContainer.new() column.add_child(playback) var pause := Button.new() pause.text = "PAUSE / PLAY" pause.pressed.connect(func(): playing = not playing) playback.add_child(pause) scrub.min_value = 0 scrub.max_value = 1 scrub.step = 0.001 scrub.size_flags_horizontal = Control.SIZE_EXPAND_FILL scrub.value_changed.connect(func(value: float): playing = false elapsed = value * model.pose.clip(clip_key()).length) playback.add_child(scrub) status.text = "%d Clips · Ziehen: drehen · Mausrad: zoomen · Zeitleiste: Einzelpose" % clips.item_count column.add_child(status) var back := Button.new() back.text = "← ZURÜCK" back.pressed.connect(ui.main_menu) column.add_child(back) func select_model(variant: String) -> void: if model != null: world.remove_child(model) model.queue_free() selected = variant model = ImportedOperator.new() world.add_child(model) model.setup(variant) model.weapon_mount.add_child(WeaponModels.build(ui.game.loadout)) elapsed = 0 func clip_key() -> String: return str(clips.get_item_metadata(clips.selected)) if clips.item_count > 0 else "pro_rifle__idle_aiming" func _process(delta: float) -> void: if model == null: return var key := clip_key() var length := model.pose.clip(key).length if playing: elapsed = fposmod(elapsed + delta, length + 0.5) model.preview(key, minf(elapsed, length)) scrub.set_value_no_signal(minf(1, elapsed / length)) model.rotation.y = yaw camera.position = Vector3(0.4, 1.25, -distance) camera.look_at(Vector3(0, 0.9, 0)) func inspect_input(event: InputEvent) -> void: if event is InputEventMouseMotion and event.button_mask & MOUSE_BUTTON_MASK_LEFT: yaw += event.relative.x * 0.012 if event is InputEventMouseButton and event.pressed: if event.button_index == MOUSE_BUTTON_WHEEL_UP: distance = maxf(1.2, distance - 0.2) if event.button_index == MOUSE_BUTTON_WHEEL_DOWN: distance = minf(5, distance + 0.2)