class_name SpearProjectile extends RigidBody3D # A thrown spear. It flies a ballistic arc (gravity does the work — the launch # velocity is solved by the matador to land on the bull) and keeps its shaft aimed # along its own velocity, so it noses over at the apex and drives in point-first. # On impact it embeds where it struck: the tip buried just past the contact point # with the shaft trailing back along the flight line, so it reads as stuck-in rather # than floating alongside. A hit on the bull leaves the spear riding it like a spine # (see shed_all, which the bull's slam calls to fling them all off); other hits pin # it in place until the despawn timer clears it. const _DESPAWN_SEC: float = 8.0 const _STUCK_GROUP: StringName = &"stuck_spear" # Shaft geometry in spear.glb local space: tip at +Z, butt behind the origin. const _TIP_Z: float = 1.915 const _EMBED_DEPTH: float = 0.45 # how far past the surface the tip buries var _mesh: Node3D = null var _stopped: bool = false var _flight_dir: Vector3 = Vector3.ZERO # last pre-impact travel direction # Build, seat and launch a spear from an already-instantiated mesh (the one that was # in the matador's hand). `origin` is the release point; `velocity` is the full # ballistic launch vector; `gravity` is the RigidBody gravity_scale for the arc. static func launch(parent: Node, mesh: Node3D, origin: Vector3, velocity: Vector3, gravity: float) -> SpearProjectile: var s := SpearProjectile.new() s.gravity_scale = gravity s.collision_layer = 0 s.collision_mask = 1 s.contact_monitor = true s.max_contacts_reported = 6 parent.add_child(s) # Seat the body so its local +Z (the shaft, see spear.glb) points along the throw. s.global_transform = Transform3D(_aim_basis(velocity), origin) mesh.reparent(s, false) mesh.transform = Transform3D.IDENTITY s._mesh = mesh s.add_child(_shaft_collider()) s.linear_velocity = velocity _despawn(s) return s # Shed every spear stuck in `bull` — pop each off as a tumbling body that flies clear # and clears itself on the despawn timer. Called by the bull's slam. static func shed_all(bull: Node) -> void: if not (bull is Node3D): return var scene: Node = bull.get_tree().current_scene if scene == null: scene = bull var center: Vector3 = (bull as Node3D).global_position for child in bull.get_children(): if not child.is_in_group(_STUCK_GROUP): continue var mesh := child as Node3D var gx := mesh.global_transform var rb := RigidBody3D.new() rb.collision_layer = 0 rb.collision_mask = 1 scene.add_child(rb) rb.global_transform = gx mesh.reparent(rb, false) mesh.transform = Transform3D.IDENTITY mesh.remove_from_group(_STUCK_GROUP) rb.add_child(_shaft_collider()) # Fling outward from the bull with a bit of lift and spin. var out := gx.origin - center out.y = 0.0 out = out.normalized() if out.length() > 0.01 else Vector3(randf() - 0.5, 0.0, randf() - 0.5) rb.linear_velocity = out * randf_range(3.0, 6.0) + Vector3.UP * randf_range(3.0, 5.0) rb.angular_velocity = Vector3(randf_range(-10.0, 10.0), randf_range(-10.0, 10.0), randf_range(-10.0, 10.0)) _despawn(rb) static func _shaft_collider() -> CollisionShape3D: # Collider along local +Z (mesh runs z≈-0.34 … 1.92), fattened so a fast throw # can't tunnel past the bull's collision spheres. var cap := CapsuleShape3D.new() cap.radius = 0.13 cap.height = 2.2 var cs := CollisionShape3D.new() cs.shape = cap cs.position = Vector3(0.0, 0.0, 0.79) cs.rotation_degrees = Vector3(90.0, 0.0, 0.0) return cs # Continuous bleed from a spear lodged in the bull: a steady spurt of dark-red gobs out # of the entry wound. Parented to the stuck mesh so it tracks the wound as the bull # moves; world-space particles (local_coords off) so they arc off and fall behind as a # trail. Dies with the mesh when the spear is shed or despawned. static func _wound_emitter() -> CPUParticles3D: var ramp := Gradient.new() ramp.set_color(0, Color(0.5, 0.02, 0.02, 1.0)) ramp.set_color(1, Color(0.22, 0.0, 0.0, 0.0)) var sc := Curve.new() sc.add_point(Vector2(0.0, 1.0)) sc.add_point(Vector2(0.6, 0.7)) sc.add_point(Vector2(1.0, 0.0)) var p := CPUParticles3D.new() # Seat at the wound (where the shaft meets the hide) and spray back out along the # shaft: mesh local -Z is the exit direction, rotated into world by the node basis. p.position = Vector3(0.0, 0.0, _TIP_Z - _EMBED_DEPTH) p.emitting = true p.amount = 22 p.lifetime = 0.6 p.explosiveness = 0.0 p.randomness = 0.6 p.local_coords = false p.direction = Vector3(0.0, 0.55, -1.0).normalized() p.spread = 32.0 p.gravity = Vector3(0.0, -12.0, 0.0) p.initial_velocity_min = 1.6 p.initial_velocity_max = 4.0 p.scale_amount_min = 0.5 p.scale_amount_max = 1.4 p.mesh = _blood_mesh() p.color_ramp = ramp p.scale_amount_curve = sc return p # Small unshaded, vertex-coloured sphere for blood gobs. static func _blood_mesh() -> SphereMesh: var mat := StandardMaterial3D.new() mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA mat.vertex_color_use_as_albedo = true mat.cull_mode = BaseMaterial3D.CULL_DISABLED var s := SphereMesh.new() s.radius = 0.045 s.height = 0.09 s.radial_segments = 4 s.rings = 2 s.material = mat return s static func _despawn(node: Node) -> void: node.get_tree().create_timer(_DESPAWN_SEC).timeout.connect(func() -> void: if is_instance_valid(node): node.queue_free() ) # Basis whose local +Z points along `v` (looking_at points -Z at its target). static func _aim_basis(v: Vector3) -> Basis: var dir := v.normalized() if dir.length_squared() < 0.0001: return Basis.IDENTITY var up := Vector3.UP if absf(dir.dot(Vector3.UP)) < 0.99 else Vector3.FORWARD return Basis.looking_at(-dir, up) # While flying, keep the shaft aligned with the velocity; on the first contact freeze # and hand off to _embed so the spear sticks where it struck instead of bouncing off. func _integrate_forces(state: PhysicsDirectBodyState3D) -> void: if _stopped: return if state.get_contact_count() > 0: _stopped = true # Use the remembered pre-impact heading — state.linear_velocity is already the # bounced (post-collision) velocity by now, which would aim the shaft wrong. var contact := state.get_contact_collider_position(0) var other := state.get_contact_collider_object(0) state.linear_velocity = Vector3.ZERO state.angular_velocity = Vector3.ZERO _embed.call_deferred(other, contact, _flight_dir) return var v := state.linear_velocity if v.length_squared() > 0.04: _flight_dir = v.normalized() var t := state.transform t.basis = _aim_basis(v) state.transform = t state.angular_velocity = Vector3.ZERO # Compose the stuck pose (tip buried _EMBED_DEPTH past the contact, shaft along the # flight line) and plant the spear. A bull hit hands the mesh to the bull so it rides # along as a spine; anything else pins the body in place for the despawn timer. func _embed(other: Object, contact: Vector3, flight: Vector3) -> void: var f := flight.normalized() if f.length_squared() < 0.0001: f = global_transform.basis.z.normalized() var pose := Transform3D(_aim_basis(f), contact + f * (_EMBED_DEPTH - _TIP_Z)) var bull := other as Node if bull != null and bull.is_in_group(&"player"): bull.call(&"take_sword_hit", "thrown", contact) if is_instance_valid(_mesh) and is_instance_valid(bull): _mesh.reparent(bull, false) _mesh.global_transform = pose _mesh.add_to_group(_STUCK_GROUP) if Settings.gore: _mesh.add_child(_wound_emitter()) # keep bleeding from the wound while lodged queue_free() return freeze = true global_transform = pose