extends Node3D ## A flying arrow or thrown spear. ## ## Each physics step it moves along its velocity (with gravity) and ray-casts ## the segment it covered, so fast arrows can't skip through a thin target. ## - Hits an animal: damage scaled by speed. An arrow sticks in the animal and ## comes back when you butcher it. A spear drops at the animal's feet. ## - Hits anything else: it sticks there and becomes a pickup. ## - After 8 s in the air (or below the world) it drops where it is. var item_id := "arrow" var damage := 10.0 var velocity := Vector3.ZERO var gravity := 9.8 var shooter: Node3D var world: Node3D var _life := 8.0 var _start_speed := 1.0 func setup(id: String, w: Node3D, from: Vector3, vel: Vector3, dmg: float, who: Node3D) -> void: item_id = id world = w velocity = vel damage = dmg shooter = who _start_speed = maxf(vel.length(), 0.1) gravity = 9.8 if id == "arrow" else 12.0 var mi := MeshInstance3D.new() mi.mesh = ItemDB.mesh_for(id) mi.material_override = ItemDB.item_material() if id == "spear": # The spear mesh runs along +Y; turn it to fly point-first along -Z. mi.transform = Transform3D(Basis(Vector3.RIGHT, -PI * 0.5), Vector3(0, 0, 0.35)) add_child(mi) position = from _orient() func _orient() -> void: if velocity.length() > 0.01 and is_inside_tree(): look_at(global_position + velocity, Vector3.UP if absf(velocity.normalized().y) < 0.99 else Vector3.RIGHT) func _ready() -> void: _orient() func _physics_process(delta: float) -> void: step(delta) func step(dt: float) -> void: _life -= dt var from := global_position velocity.y -= gravity * dt var to := from + velocity * dt var ex: Array[RID] = [] if shooter != null and is_instance_valid(shooter) and shooter.has_method("get_rid"): ex.append(shooter.get_rid()) var q := PhysicsRayQueryParameters3D.create(from, to, 1 | 8, ex) var hit := get_world_3d().direct_space_state.intersect_ray(q) if not hit.is_empty(): _on_hit(hit) return global_position = to _orient() if _life <= 0.0 or to.y < world.terrain.min_height - 20.0: _drop(Transform3D(Basis(), Vector3(to.x, world.terrain.sample_height(to.x, to.z), to.z))) func _on_hit(hit: Dictionary) -> void: var c: Object = hit.collider var dir := velocity.normalized() if visual: queue_free() return if c != null and (c as Node).is_in_group("creature"): var k := clampf(velocity.length() / _start_speed, 0.4, 1.0) Actions.projectile_hit(shooter, c, damage * k, global_position, item_id if item_id == "arrow" else "") if item_id == "spear": var at: Vector3 = (c as Node3D).global_position + Vector3(0, 0.05, 0) _drop(Transform3D(Basis(Vector3.UP, randf() * TAU) * Basis(Vector3.FORWARD, PI * 0.5), at)) else: queue_free() return # Stick into whatever it hit, point first, slightly buried. var at: Vector3 = hit.position - dir * (0.12 if item_id == "arrow" else 0.25) var b := Basis.looking_at(dir, Vector3.UP if absf(dir.y) < 0.99 else Vector3.RIGHT) if item_id == "spear": b = b * Basis(Vector3.RIGHT, -PI * 0.5) # mesh +Y along the flight _drop(Transform3D(b, at)) func _drop(xf: Transform3D) -> void: if not visual: world.drop_item(item_id, 1, xf) queue_free() ## Online, our game shows the server's arrows and spears in flight ## (creature_net.gd). They hit nothing: the server's copy does that, and ## anything that lands comes from the server as a dropped item. var visual := false