From 30136a9b905850516551d9c557964b8947aff3cb Mon Sep 17 00:00:00 2001 From: Richard Aasa Date: Mon, 25 May 2026 19:39:02 +0300 Subject: [PATCH] bull legs not clipped, arena not scaled --- Player.tscn | 6 +- bull_legs.gd | 261 +----------------------------------------------- debug_params.gd | 6 ++ matador.gd | 120 +++++++++++++++++++--- scene.tscn | 2 - 5 files changed, 117 insertions(+), 278 deletions(-) diff --git a/Player.tscn b/Player.tscn index c519778..9c0b02a 100644 --- a/Player.tscn +++ b/Player.tscn @@ -22,15 +22,15 @@ collision_layer = 3 script = ExtResource("1_xhfnw") [node name="CollisionChest" type="CollisionShape3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -0.2) +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.505, -0.2) shape = SubResource("SphereShape3D_chest") [node name="CollisionBelly" type="CollisionShape3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0.1) +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.505, 0.1) shape = SubResource("SphereShape3D_belly") [node name="CollisionHips" type="CollisionShape3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0.4) +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.505, 0.4) shape = SubResource("SphereShape3D_hips") [node name="SpringArmPivot" type="Node3D" parent="." unique_id=1671817680] diff --git a/bull_legs.gd b/bull_legs.gd index 6e55e2b..e3d0b14 100644 --- a/bull_legs.gd +++ b/bull_legs.gd @@ -1,54 +1,17 @@ extends Node -## Procedural leg stepping, body wobble, and tail simulation. +## Body wobble and tail simulation. Legs stay in rest pose (no IK, no ragdoll). ## Created at runtime by player.gd; call setup() before the first _process tick. -# ── Leg State Machine ───────────────────────────────────────────────────────── -enum LegState { GALLOP, LAUNCH, RAGDOLL, TUCK, LAND } - -# Leg IK config — rest offsets are in bull_v10 local space -const LEG_CFG: Dictionary = { - "FL": { "root": &"front_leg_1_L", "tip": &"front_leg_2_L_end", - "rest": Vector3(-0.30, -0.25, -0.50) }, - "FR": { "root": &"front_leg_1_R", "tip": &"front_leg_2_R_end", - "rest": Vector3( 0.30, -0.25, -0.50) }, - "BL": { "root": &"rear_leg_1_L", "tip": &"rear_leg_3_L_end", - "rest": Vector3(-0.30, -0.25, 0.60) }, - "BR": { "root": &"rear_leg_1_R", "tip": &"rear_leg_3_R_end", - "rest": Vector3( 0.30, -0.25, 0.60) }, -} - -# Gallop gait phase offset per leg (0.0–1.0 cycle). -const LEG_PHASES: Dictionary = { "BL": 0.0, "FL": 0.2, "BR": 0.5, "FR": 0.7 } - -# Per-leg ragdoll phase offsets for variety -const RAGDOLL_PHASE: Dictionary = { "FL": 0.0, "FR": 1.3, "BL": 2.6, "BR": 3.9 } - var _player: CharacterBody3D var _bull: Node3D var _skeleton: Skeleton3D -# Per-leg state -var _ik: Dictionary = {} # id -> SkeletonIK3D -var _foot: Dictionary = {} # id -> current foot (world) -var _step_from: Dictionary = {} # id -> Vector3 -var _step_to: Dictionary = {} # id -> Vector3 -var _step_t: Dictionary = {} # id -> float (1.0 = planted) - # Body wobble var _prev_vel: Vector3 = Vector3.ZERO var _tilt_x: float = 0.0 var _tilt_z: float = 0.0 var _aerial_pitch: float = 0.0 -# Leg state machine -var _leg_state: LegState = LegState.GALLOP -var _state_time: float = 0.0 # time spent in current state -var _was_on_floor: bool = true -var _launch_feet: Dictionary = {} # frozen foot positions at launch - -# Gait -var _gait_phase: float = 0.0 - # Tail — Verlet chain simulation const TAIL_BONES: Array = [ &"tail_1", &"tail_2", &"tail_3", &"tail_4", @@ -70,31 +33,13 @@ func setup(player: CharacterBody3D, bull: Node3D) -> void: push_error("BullLegs: Skeleton3D not found under bull_v10") return - # Disable any physical bone simulator — we don't need it + # Disable any physical bone simulator var phys_sim := _find_physical_bones(_skeleton) if phys_sim: phys_sim.active = false _setup_tail() - for id: String in LEG_CFG: - var cfg: Dictionary = LEG_CFG[id] - - var ik := SkeletonIK3D.new() - ik.root_bone = cfg["root"] - ik.tip_bone = cfg["tip"] - ik.interpolation = 1.0 - ik.min_distance = 0.01 - _skeleton.add_child(ik) - ik.start() - _ik[id] = ik - - var world_rest: Vector3 = _bull.to_global(cfg["rest"]) - _foot[id] = world_rest - _step_from[id] = world_rest - _step_to[id] = world_rest - _step_t[id] = 1.0 - func _setup_tail() -> void: for bone_name: StringName in TAIL_BONES: @@ -108,34 +53,27 @@ func _setup_tail() -> void: push_error("BullLegs: not enough tail bones found") return - # Initialise world positions first for idx: int in _tail_idx: var world: Vector3 = _skeleton.to_global(_skeleton.get_bone_global_rest(idx).origin) _tail_world.append(world) _tail_prev.append(world) - # Detect degenerate rest pose (FBX bones all at same position) var max_sep: float = 0.0 for i in range(_tail_world.size() - 1): max_sep = maxf(max_sep, _tail_world[i].distance_to(_tail_world[i + 1])) if max_sep < 0.01: push_warning("BullLegs: tail bones have zero separation in rest pose, distributing along spine") var root: Vector3 = _tail_world[0] - # Backward-downward direction in world space from the skeleton var back: Vector3 = (_skeleton.global_transform.basis.z + Vector3.DOWN * 0.3).normalized() for i in range(_tail_world.size()): _tail_world[i] = root + back * (i * 0.09) _tail_prev[i] = _tail_world[i] - # Segment lengths in world space — used by the Verlet length constraint const MIN_SEG: float = 0.06 for i in range(_tail_world.size() - 1): var seg_len: float = _tail_world[i].distance_to(_tail_world[i + 1]) _tail_lengths.append(maxf(seg_len, MIN_SEG)) - # Rest directions in SKELETON-LOCAL space — must match point_dir_skel in _apply_tail - # so that Quaternion(rest_dir, point_dir_skel) operates in a consistent frame. - # _tail_world holds world positions; to_local converts them to skeleton space. for i in range(_tail_world.size() - 1): var p1_skel: Vector3 = _skeleton.to_local(_tail_world[i]) var p2_skel: Vector3 = _skeleton.to_local(_tail_world[i + 1]) @@ -148,9 +86,6 @@ func _process(delta: float) -> void: if not _skeleton: return _wobble_body(delta) - _update_leg_state(delta) - _step_feet(delta) - _apply_ik() _update_tail(delta) _apply_tail() @@ -180,190 +115,6 @@ func _wobble_body(delta: float) -> void: _bull.rotation.z = _tilt_z -# ── Leg State Machine ───────────────────────────────────────────────────────── - -func _update_leg_state(delta: float) -> void: - var on_floor: bool = _player.is_on_floor() - _state_time += delta - - match _leg_state: - LegState.GALLOP: - if not on_floor and _was_on_floor: - # Just left the ground — enter LAUNCH - _leg_state = LegState.LAUNCH - _state_time = 0.0 - # Freeze current foot positions - for id: String in _foot: - _launch_feet[id] = _foot[id] - - LegState.LAUNCH: - if on_floor: - _leg_state = LegState.GALLOP - _state_time = 0.0 - elif _state_time > DP.f("launch_hold_time"): - _leg_state = LegState.RAGDOLL - _state_time = 0.0 - - LegState.RAGDOLL: - if on_floor: - _leg_state = LegState.GALLOP - _state_time = 0.0 - elif _player.velocity.y < DP.f("tuck_apex_threshold"): - # Approaching or past apex — start tucking - _leg_state = LegState.TUCK - _state_time = 0.0 - - LegState.TUCK: - if on_floor: - _leg_state = LegState.GALLOP - _state_time = 0.0 - else: - # Check for ground proximity — transition to LAND - var space := _player.get_world_3d().direct_space_state - var start := _player.global_position - var end_pt := start + Vector3.DOWN * DP.f("jump_land_threshold") - var params := PhysicsRayQueryParameters3D.create(start, end_pt) - params.exclude = [_player.get_rid()] - var hit := space.intersect_ray(params) - if hit: - _leg_state = LegState.LAND - _state_time = 0.0 - - LegState.LAND: - if on_floor: - _leg_state = LegState.GALLOP - _state_time = 0.0 - - _was_on_floor = on_floor - - -# ── Feet Stepping ───────────────────────────────────────────────────────────── - -func _step_feet(delta: float) -> void: - match _leg_state: - LegState.GALLOP: - _step_gallop(delta) - LegState.LAUNCH: - _step_launch(delta) - LegState.RAGDOLL: - _step_ragdoll(delta) - LegState.TUCK: - _step_tuck(delta) - LegState.LAND: - _step_land(delta) - - -func _step_gallop(delta: float) -> void: - var threshold: float = DP.f("leg_step_threshold") - var duration: float = DP.f("leg_step_duration") - var height: float = DP.f("leg_step_height") - var overshoot: float = DP.f("leg_step_overshoot") - var flat_spd: float = Vector2(_player.velocity.x, _player.velocity.z).length() - - var prev_phase: float = _gait_phase - _gait_phase = fmod(_gait_phase + flat_spd * DP.f("gait_freq") * delta, 1.0) - - # Advance in-progress steps - for id: String in _step_t: - if _step_t[id] >= 1.0: - continue - _step_t[id] = minf(_step_t[id] + delta / duration, 1.0) - var t: float = _step_t[id] - var e: float = t * t * (3.0 - 2.0 * t) - var arc: float = height * sin(t * PI) - _foot[id] = _step_from[id].lerp(_step_to[id], e) + Vector3(0.0, arc, 0.0) - - # Trigger steps when gait phase crosses each leg's offset - for id: String in LEG_PHASES: - if _step_t[id] < 1.0: - continue - if not _phase_crossed(prev_phase, _gait_phase, LEG_PHASES[id]): - continue - var ideal: Vector3 = _ideal_foot(id) - if _foot[id].distance_to(ideal) < threshold: - continue - _step_from[id] = _foot[id] - _step_to[id] = ideal + _player.velocity * duration * overshoot - _step_t[id] = 0.0 - - -func _step_launch(_delta: float) -> void: - # Hold feet at their frozen launch positions - for id: String in _foot: - if _launch_feet.has(id): - _foot[id] = _launch_feet[id] - - -func _step_ragdoll(_delta: float) -> void: - # Procedural floppy dangle — NOT real physics ragdoll - var time: float = _state_time - var amp: float = DP.f("ragdoll_wobble_amp") - # Amplitude grows over time (legs spread more the longer airborne) - var grow: float = clampf(time * 0.8, 0.0, 1.5) - - for id: String in _foot: - var rest: Vector3 = LEG_CFG[id]["rest"] - var phase: float = RAGDOLL_PHASE[id] - var dangle_offset := Vector3( - sin(time * 3.0 + phase) * amp * grow, # side sway - -0.4 - sin(time * 2.0 + phase) * 0.1 * grow, # gravity pull down - cos(time * 2.5 + phase) * amp * grow # forward/back wobble - ) - _foot[id] = _bull.to_global(rest + dangle_offset) - - -func _step_tuck(delta: float) -> void: - # Smoothly pull legs into tucked position under the body - var speed: float = DP.f("jump_tuck_speed") - for id: String in _foot: - var tucked: Vector3 = _get_tucked_foot(id) - _foot[id] = _foot[id].lerp(tucked, clampf(delta * speed, 0.0, 1.0)) - - -func _step_land(delta: float) -> void: - # Extend legs from tucked pose toward ideal ground positions, resume gallop cycle - var speed: float = DP.f("jump_tuck_speed") * 1.5 # slightly faster extend - for id: String in _foot: - var ideal: Vector3 = _ideal_foot(id) - _foot[id] = _foot[id].lerp(ideal, clampf(delta * speed, 0.0, 1.0)) - - -# ── Helpers ─────────────────────────────────────────────────────────────────── - -func _phase_crossed(prev: float, curr: float, target: float) -> bool: - if curr >= prev: - return target >= prev and target < curr - else: - return target >= prev or target < curr - - -func _ideal_foot(id: String) -> Vector3: - var world_rest: Vector3 = _bull.to_global(LEG_CFG[id]["rest"]) - - # Raycast for ground - var space := _player.get_world_3d().direct_space_state - var params := PhysicsRayQueryParameters3D.create( - world_rest + Vector3(0.0, 0.6, 0.0), - world_rest + Vector3(0.0, -1.4, 0.0), - ) - params.exclude = [_player.get_rid()] - var hit := space.intersect_ray(params) - return hit["position"] if hit else world_rest - - -func _get_tucked_foot(id: String) -> Vector3: - var rest: Vector3 = LEG_CFG[id]["rest"] - var tucked_local := rest + Vector3(0.0, DP.f("jump_tuck_up"), DP.f("jump_tuck_fwd")) - return _bull.to_global(tucked_local) - - -func _apply_ik() -> void: - var skel_inv: Transform3D = _skeleton.global_transform.affine_inverse() - for id: String in _ik: - var ik: SkeletonIK3D = _ik[id] - ik.target = Transform3D(Basis.IDENTITY, skel_inv * _foot[id]) - - # ── Tail ────────────────────────────────────────────────────────────────────── func _update_tail(delta: float) -> void: @@ -373,26 +124,21 @@ func _update_tail(delta: float) -> void: var gravity := Vector3(0.0, -DP.f("tail_gravity"), 0.0) var damping := 1.0 - clampf(DP.f("tail_damping"), 0.0, 0.99) - # Re-anchor root joint var anchor: Vector3 = _skeleton.to_global(_skeleton.get_bone_global_rest(_tail_idx[0]).origin) _tail_world[0] = anchor _tail_prev[0] = anchor - # Idle wag force var flat_speed: float = Vector2(_player.velocity.x, _player.velocity.z).length() var wag_strength: float = lerpf(DP.f("tail_wag_idle"), 0.0, clampf(flat_speed / 3.0, 0.0, 1.0)) var time: float = Time.get_ticks_msec() / 1000.0 - # Verlet integrate for i in range(1, _tail_world.size()): var vel: Vector3 = (_tail_world[i] - _tail_prev[i]) * damping _tail_prev[i] = _tail_world[i] - # Add wag force (sinusoidal side-to-side) var wag := _bull.global_transform.basis.x \ * sin(time * DP.f("tail_wag_freq") + i * 0.5) * wag_strength _tail_world[i] = _tail_world[i] + vel + gravity * (delta * delta) + wag * delta - # Length constraint for i in range(1, _tail_world.size()): var dir: Vector3 = _tail_world[i] - _tail_world[i - 1] var len: float = dir.length() @@ -402,12 +148,9 @@ func _update_tail(delta: float) -> void: else: _tail_world[i] = _tail_world[i - 1] + Vector3(0.0, -target_len, 0.0) - # Body collision pass — prevent tail from clipping through torso - # Use bull's local space; body is approximated as a capsule along Z axis var body_radius: float = DP.f("tail_body_radius") for i in range(1, _tail_world.size()): var p := _bull.to_local(_tail_world[i]) - # Capsule centered at roughly (0, 0, 0.1) in bull local space, half-length 0.4 along Z var closest := Vector3(0.0, 0.0, clampf(p.z, -0.3, 0.5)) var diff := p - closest var dist := diff.length() diff --git a/debug_params.gd b/debug_params.gd index 5fcb805..3262361 100644 --- a/debug_params.gd +++ b/debug_params.gd @@ -87,6 +87,12 @@ func _register_all() -> void: _reg_f("Matador", "mat_idle_max", 2.5, 0.5, 10.0, 0.1) _reg_f("Matador", "mat_hit_threshold", 4.0, 1.0, 30.0) _reg_f("Matador", "mat_ragdoll_impulse", 3.0, 0.5, 20.0) + _reg_f("Matador", "mat_flee_range", 10.0, 2.0, 40.0) + _reg_f("Matador", "mat_flee_speed", 4.0, 0.5, 12.0) + _reg_f("Matador", "mat_dodge_range", 3.5, 1.0, 10.0) + _reg_f("Matador", "mat_dodge_speed", 7.0, 1.0, 20.0) + _reg_f("Matador", "mat_dodge_duration", 0.5, 0.1, 2.0, 0.05) + _reg_f("Matador", "mat_dodge_cooldown", 1.5, 0.0, 5.0, 0.1) # ── Debug ───────────────────────────────────────────────────────────────── _reg_b("Debug", "show_collisions", false) diff --git a/matador.gd b/matador.gd index d34019f..7481863 100644 --- a/matador.gd +++ b/matador.gd @@ -1,6 +1,6 @@ extends CharacterBody3D -enum State { WANDER, RAGDOLL } +enum State { WANDER, FLEE, DODGE, RAGDOLL } var _state: State = State.WANDER var _skeleton: Skeleton3D = null @@ -8,6 +8,10 @@ var _sim: PhysicalBoneSimulator3D = null var _anim_player: AnimationPlayer = null var _wander_target: Vector3 = Vector3.ZERO var _idle_timer: float = 0.0 +var _bull: CharacterBody3D = null +var _dodge_dir: Vector3 = Vector3.ZERO +var _dodge_timer: float = 0.0 +var _dodge_cd: float = 0.0 @onready var _mesh: Node3D = $matador_v02 @onready var _hit_area: Area3D = $HitArea @@ -35,13 +39,42 @@ func _ready() -> void: func _physics_process(delta: float) -> void: + if _bull == null: + var players := get_tree().get_nodes_in_group(&"player") + if not players.is_empty(): + _bull = players[0] as CharacterBody3D + if not is_on_floor(): velocity += get_gravity() * delta + + _dodge_cd = maxf(0.0, _dodge_cd - delta) + + if _state != State.RAGDOLL and _bull != null: + _update_ai_state() + match _state: State.WANDER: _tick_wander(delta) + State.FLEE: _tick_flee(delta) + State.DODGE: _tick_dodge(delta) State.RAGDOLL: _tick_ragdoll(delta) +func _update_ai_state() -> void: + var dist := global_position.distance_to(_bull.global_position) + match _state: + State.WANDER: + if dist < DP.f("mat_flee_range"): + _state = State.FLEE + State.FLEE: + if dist < DP.f("mat_dodge_range") and _dodge_cd <= 0.0: + _start_dodge() + elif dist > DP.f("mat_flee_range") * 1.3: + _state = State.WANDER + _pick_wander_target() + State.DODGE: + pass + + func _tick_wander(delta: float) -> void: if _idle_timer > 0.0: _idle_timer -= delta @@ -67,9 +100,54 @@ func _tick_wander(delta: float) -> void: move_and_slide() -func _tick_ragdoll(delta: float) -> void: - velocity.x = move_toward(velocity.x, 0.0, 15.0 * delta) - velocity.z = move_toward(velocity.z, 0.0, 15.0 * delta) +func _tick_flee(delta: float) -> void: + var away := (global_position - _bull.global_position) + away.y = 0.0 + if away.length() < 0.01: + away = Vector3(randf() - 0.5, 0.0, randf() - 0.5) + away = away.normalized() + var spd := DP.f("mat_flee_speed") + velocity.x = away.x * spd + velocity.z = away.z * spd + _mesh.rotation.y = lerp_angle(_mesh.rotation.y, atan2(velocity.x, velocity.z), delta * 8.0) + _play_anim(_WALK_ANIM) + move_and_slide() + + +func _start_dodge() -> void: + _state = State.DODGE + var bull_vel_flat := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z) + var bull_fwd: Vector3 + if bull_vel_flat.length() > 1.0: + bull_fwd = bull_vel_flat.normalized() + else: + bull_fwd = (_bull.global_position - global_position) + bull_fwd.y = 0.0 + bull_fwd = bull_fwd.normalized() + # Step sideways relative to the bull's travel direction, biased slightly backward + var side := 1.0 if randf() > 0.5 else -1.0 + var perp := bull_fwd.rotated(Vector3.UP, PI * 0.5 * side) + _dodge_dir = (perp * 0.7 + (-bull_fwd) * 0.3).normalized() + _dodge_timer = DP.f("mat_dodge_duration") + + +func _tick_dodge(delta: float) -> void: + _dodge_timer -= delta + if _dodge_timer <= 0.0: + _state = State.FLEE + _dodge_cd = DP.f("mat_dodge_cooldown") + return + var spd := DP.f("mat_dodge_speed") + velocity.x = _dodge_dir.x * spd + velocity.z = _dodge_dir.z * spd + _mesh.rotation.y = lerp_angle(_mesh.rotation.y, atan2(velocity.x, velocity.z), delta * 12.0) + _play_anim(_WALK_ANIM) + move_and_slide() + + +func _tick_ragdoll(_delta: float) -> void: + velocity.x = 0.0 + velocity.z = 0.0 move_and_slide() @@ -90,18 +168,30 @@ func _enter_ragdoll(hit_dir: Vector3, bull_speed: float) -> void: hit_dir.y = 0.0 hit_dir = hit_dir.normalized() - # Cap lateral slide so matadors don't cross the arena; fixed upward pop - var lateral: float = minf(bull_speed * 0.2, 4.0) - velocity = Vector3(hit_dir.x * lateral, 2.0, hit_dir.z * lateral) + # Keep the CharacterBody3D stationary: physics bones simulate relative to the + # skeleton root, so sliding the root makes every bone pose diverge. + velocity = Vector3.ZERO - # Disable body capsule so a fast bull doesn't bounce off a ragdolling matador - _body_col.disabled = true + # Remove from collision layer so the bull passes through, but keep the + # shape enabled — move_and_slide() still needs it to detect the floor. + collision_layer = 0 if _anim_player: - _anim_player.stop() + _anim_player.pause() # pause keeps current frame; stop() resets to T-pose if not _sim: return + # Teleport each physics body to its current bone world transform, then + # enable per-bone simulation. _sim.active alone does NOT set simulate_physics; + # without it _process_modification() skips every bone and writes nothing. + for child: Node in _sim.get_children(): + if not (child is PhysicalBone3D): + continue + var pb := child as PhysicalBone3D + var bone_idx := _skeleton.find_bone(pb.bone_name) + if bone_idx >= 0: + pb.global_transform = _skeleton.global_transform * _skeleton.get_bone_global_pose(bone_idx) + pb.simulate_physics = true _sim.active = true var strength := DP.f("mat_ragdoll_impulse") for child: Node in _sim.get_children(): @@ -181,10 +271,12 @@ func _setup_physical_bones() -> PhysicalBoneSimulator3D: if _skeleton.find_bone(bname) == -1: continue var pb := PhysicalBone3D.new() - pb.bone_name = bname - pb.joint_type = PhysicalBone3D.JOINT_TYPE_PIN - pb.linear_damp = 1.0 - pb.angular_damp = 1.0 + pb.bone_name = bname + pb.joint_type = PhysicalBone3D.JOINT_TYPE_PIN + pb.linear_damp = 3.0 + pb.angular_damp = 4.0 + pb.collision_layer = 1 + pb.collision_mask = 1 var cs := CollisionShape3D.new() var use_capsule: bool = entry.size() >= 3 if use_capsule: diff --git a/scene.tscn b/scene.tscn index 8be86e1..28f5653 100644 --- a/scene.tscn +++ b/scene.tscn @@ -16,7 +16,6 @@ transform = Transform3D(0.030703627, 0.98235905, -0.18446636, -0.1612905, 0.1870 shadow_enabled = true [node name="Node3D" type="Node3D" parent="." unique_id=1081100876] -transform = Transform3D(10, 0, 0, 0, 1, 0, 0, 0, 10, 0, 0, 0) [node name="StaticBody3D" type="StaticBody3D" parent="Node3D" unique_id=2072095624] @@ -25,7 +24,6 @@ transform = Transform3D(30, 0, 0, 0, 1, 0, 0, 0, 30, 0, -0.5, 0) shape = SubResource("BoxShape3D_ulcgi") [node name="arena_placeholder_v01" parent="Node3D" unique_id=1341659233 instance=ExtResource("2_nxogm")] -transform = Transform3D(0.28002706, 0, 0, 0, 1, 0, 0, 0, 0.43545616, -0.2506242, 0, 0) [node name="MatadorSpawner" type="Node3D" parent="." unique_id=3001000001] script = ExtResource("3_spawn")