111 lines
4.5 KiB
GDScript
111 lines
4.5 KiB
GDScript
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 ItemDB.get_item(id).has("ammo") else 12.0
|
|
var mi := MeshInstance3D.new()
|
|
mi.mesh = ItemDB.mesh_for(id)
|
|
mi.material_override = ItemDB.item_material()
|
|
if ItemDB.get_item(id).get("weapon", {}).has("throw"):
|
|
# 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 mask := 1 | 8
|
|
if world.server_mode and preload("res://scripts/net/rules.gd").pvp():
|
|
mask |= 16 # other players (server_player.gd); with PvP off arrows pass through them
|
|
var q := PhysicsRayQueryParameters3D.create(from, to, mask, 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.get("player_id") != null and c.get("survival") != null:
|
|
# Another player, on a server with PvP on.
|
|
var k := clampf(velocity.length() / _start_speed, 0.4, 1.0)
|
|
var by: String = String(shooter.display) if shooter != null and is_instance_valid(shooter) and shooter.get("display") != null else "an arrow"
|
|
(c as Node).survival.damage(damage * k, global_position - dir, by)
|
|
Actions.tell(shooter, "Hit %s" % String(c.display))
|
|
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))
|
|
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 ItemDB.get_item(item_id).has("ammo") else "")
|
|
if ItemDB.get_item(item_id).get("weapon", {}).has("throw"):
|
|
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 ItemDB.get_item(item_id).has("ammo") else 0.25)
|
|
var b := Basis.looking_at(dir, Vector3.UP if absf(dir.y) < 0.99 else Vector3.RIGHT)
|
|
if ItemDB.get_item(item_id).get("weapon", {}).has("throw"):
|
|
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 and not breaks:
|
|
world.drop_item(item_id, 1, xf, extra)
|
|
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
|
|
var extra: Dictionary = {} ## carried onto the item where it lands (a spear's wear)
|
|
var breaks := false ## its last throw: nothing is left where it lands
|