M2.5: wildlife, wolves, hunting, fishing and sleeping on the server
This commit is contained in:
@@ -168,6 +168,9 @@ func step(dt: float) -> void:
|
||||
_flash -= dt
|
||||
if _flash <= 0.0:
|
||||
_set_material(_mat)
|
||||
if puppet:
|
||||
_puppet_step(dt)
|
||||
return
|
||||
if is_dead():
|
||||
carcass_left -= dt
|
||||
if carcass_left <= 0.0:
|
||||
@@ -175,10 +178,58 @@ func step(dt: float) -> void:
|
||||
return
|
||||
_attack_cd = maxf(_attack_cd - dt, 0.0)
|
||||
_aggro = maxf(_aggro - dt, 0.0)
|
||||
_think(dt)
|
||||
# Who it's reacting to: the player (or on a server, the nearest one).
|
||||
var t := _pick_target()
|
||||
if t != null:
|
||||
player = t
|
||||
_think(dt)
|
||||
elif state in ["flee", "charge"]:
|
||||
state = "idle" # nobody about
|
||||
_move(dt)
|
||||
_animate(dt)
|
||||
|
||||
func _pick_target() -> Node3D:
|
||||
return world.nearest_player(global_position)
|
||||
|
||||
# --- on a client playing online: the server's animal, shown here ------------
|
||||
|
||||
## A puppet only shows what the server's copy is doing (creature_net.gd):
|
||||
## no thinking, no damage dealt. It can still be hit (that's sent to the
|
||||
## server) and butchered.
|
||||
var puppet := false
|
||||
var _net_pos := Vector3.ZERO
|
||||
var _net_yaw := 0.0
|
||||
|
||||
func net_update(pos: Vector3, y: float, st: String, spd: float, new_hp: float) -> void:
|
||||
if not puppet:
|
||||
return
|
||||
_net_pos = pos
|
||||
_net_yaw = y
|
||||
speed = spd
|
||||
if new_hp < hp - 0.01 and st != "dead":
|
||||
_flash = 0.15
|
||||
_set_material(_hit_mat)
|
||||
_burst()
|
||||
hp = new_hp
|
||||
if st == "dead" and state != "dead":
|
||||
_burst()
|
||||
_die(global_position + global_basis.z)
|
||||
return
|
||||
if state != "dead":
|
||||
state = st
|
||||
|
||||
func _puppet_step(dt: float) -> void:
|
||||
if is_dead():
|
||||
return
|
||||
var k := 1.0 - exp(-12.0 * dt)
|
||||
var p := global_position.lerp(_net_pos, k)
|
||||
if p.distance_to(_net_pos) > 6.0:
|
||||
p = _net_pos # (a jump: a wolf slipping home to the den)
|
||||
global_position = p
|
||||
yaw = lerp_angle(yaw, _net_yaw, k)
|
||||
rotation.y = yaw
|
||||
_animate(dt)
|
||||
|
||||
## Whether the player is close enough to be noticed. Sprinting, or moving
|
||||
## fast while swimming or jumping, is loud.
|
||||
func _notices_player(d: float) -> bool:
|
||||
@@ -313,6 +364,8 @@ func _animate(dt: float) -> void:
|
||||
## Applies a hit (Actions validates who hit what). `from` = where it came
|
||||
## from, for the reaction.
|
||||
func take_hit(damage: float, from: Vector3, embed_id: String = "") -> void:
|
||||
if puppet:
|
||||
return # (online the server's copy takes the hit)
|
||||
if is_dead():
|
||||
if not embed_id.is_empty():
|
||||
embedded[embed_id] = embedded.get(embed_id, 0) + 1
|
||||
@@ -349,6 +402,9 @@ func _die(from: Vector3) -> void:
|
||||
collision_layer = 2
|
||||
for l in _legs:
|
||||
l.rotation.x = 0.4
|
||||
if puppet:
|
||||
_visual.rotation.z = -PI * 0.5
|
||||
return
|
||||
# Topple onto its side, away from the blow.
|
||||
var side := 1.0 if (global_position - from).dot(global_basis.x) > 0.0 else -1.0
|
||||
if is_inside_tree() and DisplayServer.get_name() != "headless":
|
||||
|
||||
@@ -51,7 +51,7 @@ func step(dt: float) -> void:
|
||||
velocity.y -= gravity * dt
|
||||
var to := from + velocity * dt
|
||||
var ex: Array[RID] = []
|
||||
if shooter != null:
|
||||
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)
|
||||
@@ -66,6 +66,9 @@ func step(dt: float) -> void:
|
||||
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 "")
|
||||
@@ -83,5 +86,11 @@ func _on_hit(hit: Dictionary) -> void:
|
||||
_drop(Transform3D(b, at))
|
||||
|
||||
func _drop(xf: Transform3D) -> void:
|
||||
world.drop_item(item_id, 1, xf)
|
||||
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
|
||||
|
||||
@@ -40,10 +40,21 @@ func alive(kind: String = "") -> Array:
|
||||
|
||||
## Fills the budget straight away (world start), closer in than usual so
|
||||
## there's something to see, but not on top of the player.
|
||||
func populate() -> void:
|
||||
func populate(around: Vector3 = Vector3.INF) -> void:
|
||||
for kind in BUDGET:
|
||||
for i in range(BUDGET[kind]):
|
||||
_spawn_near(kind, 22.0, RING_MAX)
|
||||
_spawn_near(kind, 22.0, RING_MAX, around)
|
||||
|
||||
## A player joined a server: some wildlife around them straight away, up to
|
||||
## the island's budget.
|
||||
func populate_for(pos: Vector3) -> void:
|
||||
if not enabled:
|
||||
return
|
||||
var n: int = world.players().size()
|
||||
for kind in BUDGET:
|
||||
var near := alive(kind).filter(func(c): return c.global_position.distance_to(pos) < RING_MAX).size()
|
||||
for i in range(mini(BUDGET[kind] - near, budget(kind, n) - alive(kind).size())):
|
||||
_spawn_near(kind, 22.0, RING_MAX, pos)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not enabled:
|
||||
@@ -54,19 +65,32 @@ func _process(delta: float) -> void:
|
||||
if _check > 0.0:
|
||||
return
|
||||
_check = CHECK_EVERY
|
||||
var pp: Vector3 = world.player.global_position
|
||||
# Around every player (on a server there can be several, wherever they
|
||||
# are): an animal goes once it's far from all of them.
|
||||
var everyone: Array = world.players()
|
||||
if everyone.is_empty():
|
||||
return
|
||||
for c in get_children():
|
||||
if c.global_position.distance_to(pp) > DESPAWN:
|
||||
var near := false
|
||||
for p in everyone:
|
||||
near = near or c.global_position.distance_to(p.global_position) <= DESPAWN
|
||||
if not near:
|
||||
if c.is_in_group("creature"):
|
||||
_cooldown[c.kind] = RESPAWN_DELAY * 0.3
|
||||
c.queue_free()
|
||||
for kind in BUDGET:
|
||||
if _cooldown[kind] > 0.0:
|
||||
continue
|
||||
if alive(kind).size() < BUDGET[kind]:
|
||||
if _spawn_near(kind, RING_MIN, RING_MAX) != null:
|
||||
if alive(kind).size() < budget(kind, everyone.size()):
|
||||
var who: Node3D = everyone[0] if everyone.size() == 1 else everyone[_rng.randi() % everyone.size()]
|
||||
if _spawn_near(kind, RING_MIN, RING_MAX, who.global_position) != null:
|
||||
_cooldown[kind] = RESPAWN_DELAY * 0.25
|
||||
|
||||
## How many of a kind the island holds: the budget per player, up to twice
|
||||
## it however many are on (MULTIPLAYER_PLAN.md §11).
|
||||
static func budget(kind: String, players: int) -> int:
|
||||
return BUDGET[kind] * clampi(players, 1, 2)
|
||||
|
||||
func _on_died(c: Node) -> void:
|
||||
_cooldown[c.kind] = RESPAWN_DELAY
|
||||
|
||||
@@ -78,8 +102,8 @@ func _habitat(kind: String, x: float, z: float) -> float:
|
||||
"deer": return 1.0 - absf(f - 0.45) * 1.6
|
||||
_: return smoothstep(0.35, 0.75, f)
|
||||
|
||||
func _spawn_near(kind: String, rmin: float, rmax: float) -> Node3D:
|
||||
var pp: Vector3 = world.player.global_position
|
||||
func _spawn_near(kind: String, rmin: float, rmax: float, around: Vector3 = Vector3.INF) -> Node3D:
|
||||
var pp: Vector3 = around if around != Vector3.INF else world.player.global_position
|
||||
for _try in range(40):
|
||||
var a := _rng.randf() * TAU
|
||||
var r := _rng.randf_range(rmin, rmax)
|
||||
|
||||
@@ -33,6 +33,9 @@ func interact_prompt(p: Node) -> String:
|
||||
s += " (you need a weapon)"
|
||||
return s
|
||||
|
||||
func _pick_target() -> Node3D:
|
||||
return pack.target_for(self)
|
||||
|
||||
func _think(dt: float) -> void:
|
||||
var pp: Vector3 = player.global_position
|
||||
var d := global_position.distance_to(pp)
|
||||
@@ -85,7 +88,7 @@ func _think_hunt(pp: Vector3, d: float) -> void:
|
||||
match state:
|
||||
"lunge":
|
||||
# Break off first if you've reached a fire or it's taking too long.
|
||||
if _state_t <= 0.0 or not pack.may_close_in():
|
||||
if _state_t <= 0.0 or not pack.may_close_in(player):
|
||||
state = "stalk"
|
||||
pack.lunge_done(self)
|
||||
else:
|
||||
@@ -98,7 +101,7 @@ func _think_hunt(pp: Vector3, d: float) -> void:
|
||||
_:
|
||||
state = "stalk"
|
||||
_go_to(pack.circle_point(self))
|
||||
if pack.request_lunge(self):
|
||||
if pack.request_lunge(self, player):
|
||||
state = "lunge"
|
||||
_state_t = 4.0
|
||||
|
||||
|
||||
@@ -107,16 +107,19 @@ func step(dt: float) -> void:
|
||||
if not night and alive_count() < pack_size(day):
|
||||
fill_den(false)
|
||||
_gap = maxf(_gap - dt, 0.0)
|
||||
for t in _gaps.keys():
|
||||
_gaps[t] = maxf(float(_gaps[t]) - dt, 0.0)
|
||||
if not is_instance_valid(t):
|
||||
_gaps.erase(t)
|
||||
_circle_a += dt * 0.25
|
||||
if _lunger != null and (not is_instance_valid(_lunger) or _lunger.is_dead() or _lunger.state != "lunge"):
|
||||
_lunger = null
|
||||
# At dawn wolves head home; once one is well away from you (out of sight),
|
||||
# it slips straight back rather than picking its way across the island.
|
||||
if mode == "den":
|
||||
var pp: Vector3 = world.player.global_position
|
||||
for m in alive():
|
||||
if m.global_position.distance_to(den_pos) > 6.0 and m.global_position.distance_to(pp) > 45.0 \
|
||||
and pp.distance_to(den_pos) > 35.0:
|
||||
if m.global_position.distance_to(den_pos) > 6.0 and _far_from_everyone(m.global_position, 45.0) \
|
||||
and _far_from_everyone(den_pos, 35.0):
|
||||
m.global_position = den.slot_pos(m.slot)
|
||||
m.wounded = false
|
||||
m.state = "rest"
|
||||
@@ -146,9 +149,9 @@ func _warn(day: int) -> void:
|
||||
return
|
||||
_warned[day] = true
|
||||
if day == DEN_DAY:
|
||||
Actions.notify.emit("Wolves are howling up in the hills. They'll come hunting soon.")
|
||||
Actions.tell_all("Wolves are howling up in the hills. They'll come hunting soon.")
|
||||
elif day == HUNT_DAY:
|
||||
Actions.notify.emit("The wolves are hunting tonight. Stay near a fire or behind closed walls.")
|
||||
Actions.tell_all("The wolves are hunting tonight. Stay near a fire or behind closed walls.")
|
||||
_howl_left = _rng.randf_range(2.0, 6.0)
|
||||
|
||||
## Brings the pack up to size, wolves lying in the den. `force` ignores
|
||||
@@ -157,7 +160,7 @@ func fill_den(force: bool) -> void:
|
||||
var need: int = pack_size(effective_day()) - alive_count()
|
||||
if need <= 0:
|
||||
return
|
||||
if not force and world.player.global_position.distance_to(den_pos) < 35.0:
|
||||
if not force and not _far_from_everyone(den_pos, 35.0):
|
||||
return
|
||||
members = alive()
|
||||
var used := {}
|
||||
@@ -181,44 +184,119 @@ func fill_den(force: bool) -> void:
|
||||
func home_slot(w: Node) -> Vector3:
|
||||
return den.slot_pos(w.slot)
|
||||
|
||||
## The lit campfire you're sheltering by, if any.
|
||||
func player_fire() -> Node3D:
|
||||
var pp: Vector3 = world.player.global_position
|
||||
func _far_from_everyone(pos: Vector3, r: float) -> bool:
|
||||
for p in world.players():
|
||||
if p.global_position.distance_to(pos) < r:
|
||||
return false
|
||||
return true
|
||||
|
||||
# --- who the pack goes after ----------------------------------------------------
|
||||
# In single-player that's you. On a server the pack splits between the
|
||||
# players it's hunting: wolf 1 takes the first, wolf 2 the second, and so on,
|
||||
# preferring players away from a fire (you can't be got at by a fire, so
|
||||
# they'd rather go for someone who isn't). Each target gets their own circle
|
||||
# and their own one-wolf-at-a-time darts.
|
||||
|
||||
var _targets: Array = []
|
||||
var _target_t := 0.0
|
||||
|
||||
## Who wolf `w` is after right now (null: nobody about).
|
||||
func target_for(w: Node) -> Node3D:
|
||||
if not world.server_mode:
|
||||
return world.player
|
||||
if mode != "hunt":
|
||||
return world.nearest_player((w as Node3D).global_position)
|
||||
_refresh_targets()
|
||||
if _targets.is_empty():
|
||||
return null
|
||||
var i := alive().find(w)
|
||||
return _targets[maxi(i, 0) % _targets.size()]
|
||||
|
||||
## Players worth hunting, nearest the den first: those away from a fire if
|
||||
## there are any, otherwise everyone (they'll circle the fire).
|
||||
func _refresh_targets() -> void:
|
||||
var now := Time.get_ticks_msec() / 1000.0
|
||||
if now - _target_t < 1.0 and _targets.all(func(p): return is_instance_valid(p) and not p.survival.dead):
|
||||
return
|
||||
_target_t = now
|
||||
var everyone: Array = world.players()
|
||||
var open := everyone.filter(func(p): return fire_near(p) == null)
|
||||
_targets = open if not open.is_empty() else everyone
|
||||
_targets.sort_custom(func(a, b): return a.global_position.distance_to(den_pos) < b.global_position.distance_to(den_pos))
|
||||
|
||||
func _p(p: Node3D) -> Node3D:
|
||||
return p if p != null else world.player
|
||||
|
||||
## The lit campfire `p` is sheltering by, if any.
|
||||
func fire_near(p: Node3D) -> Node3D:
|
||||
var pp: Vector3 = _p(p).global_position
|
||||
for f in get_tree().get_nodes_in_group("station:campfire"):
|
||||
if f.station_active() and (f as Node3D).global_position.distance_to(pp) < FIRE_SAFE:
|
||||
return f
|
||||
return null
|
||||
|
||||
func _torch() -> bool:
|
||||
return world.player.hands != null and world.player.hands.held_heat() > 0.0
|
||||
## (Single-player name, used by tests.)
|
||||
func player_fire() -> Node3D:
|
||||
return fire_near(world.player)
|
||||
|
||||
func _torch(p: Node3D = null) -> bool:
|
||||
var who := _p(p)
|
||||
return who.get("hands") != null and who.hands.held_heat() > 0.0
|
||||
|
||||
## Wolves only close in when you're away from a fire.
|
||||
func may_close_in() -> bool:
|
||||
return player_fire() == null and not world.player.survival.dead
|
||||
func may_close_in(p: Node3D = null) -> bool:
|
||||
return fire_near(p) == null and not _p(p).survival.dead
|
||||
|
||||
## The wolves going after the same player as `w`.
|
||||
func _hunting_with(w: Node) -> Array:
|
||||
var t := target_for(w)
|
||||
return alive().filter(func(m): return target_for(m) == t)
|
||||
|
||||
## Where wolf `w` should be while circling.
|
||||
func circle_point(w: Node) -> Vector3:
|
||||
var fire := player_fire()
|
||||
var center: Vector3 = fire.global_position if fire != null else world.player.global_position
|
||||
var r := FIRE_RING if fire != null else RING * (1.3 if _torch() else 1.0)
|
||||
var live := alive()
|
||||
var n := maxi(live.size(), 1)
|
||||
var a := _circle_a + TAU * float(live.find(w)) / n
|
||||
var t := _p(target_for(w))
|
||||
var fire := fire_near(t)
|
||||
var center: Vector3 = fire.global_position if fire != null else t.global_position
|
||||
var r := FIRE_RING if fire != null else RING * (1.3 if _torch(t) else 1.0)
|
||||
var group := _hunting_with(w)
|
||||
var n := maxi(group.size(), 1)
|
||||
var a := _circle_a + TAU * float(maxi(group.find(w), 0)) / n
|
||||
return center + Vector3(cos(a), 0, sin(a)) * r
|
||||
|
||||
## A wolf asks for its turn to dart in.
|
||||
func request_lunge(w: Node) -> bool:
|
||||
if _lunger != null or _gap > 0.0 or not may_close_in() or alive_count() < 2:
|
||||
return false
|
||||
if w.global_position.distance_to(world.player.global_position) > RING * 1.8:
|
||||
## Darting in: one wolf at a time per player being hunted.
|
||||
var _lungers: Dictionary = {} ## target -> wolf
|
||||
var _gaps: Dictionary = {} ## target -> seconds until the next dart
|
||||
|
||||
## A wolf asks for its turn to dart in at `p`.
|
||||
func request_lunge(w: Node, p: Node3D = null) -> bool:
|
||||
var t := _p(p)
|
||||
if not world.server_mode:
|
||||
if _lunger != null or _gap > 0.0 or not may_close_in(t) or alive_count() < 2:
|
||||
return false
|
||||
else:
|
||||
var cur: Node = _lungers.get(t)
|
||||
if cur != null and is_instance_valid(cur) and not cur.is_dead() and cur.state == "lunge":
|
||||
return false
|
||||
if float(_gaps.get(t, 0.0)) > 0.0 or not may_close_in(t) or alive_count() < 2:
|
||||
return false
|
||||
if w.global_position.distance_to(t.global_position) > RING * 1.8:
|
||||
return false
|
||||
_lunger = w
|
||||
_lungers[t] = w
|
||||
return true
|
||||
|
||||
func lunge_done(w: Node) -> void:
|
||||
if _lunger == w:
|
||||
_lunger = null
|
||||
_gap = ATTACK_GAP * (2.5 if _torch() else 1.0) * _rng.randf_range(0.8, 1.3)
|
||||
if not world.server_mode:
|
||||
_gap = ATTACK_GAP * (2.5 if _torch() else 1.0) * _rng.randf_range(0.8, 1.3)
|
||||
return
|
||||
var t: Node3D = target_for(w) if w != null and is_instance_valid(w) else null
|
||||
for k in _lungers.keys():
|
||||
if _lungers[k] == w:
|
||||
_lungers.erase(k)
|
||||
if t != null:
|
||||
_gaps[t] = ATTACK_GAP * (2.5 if _torch(t) else 1.0) * _rng.randf_range(0.8, 1.3)
|
||||
|
||||
## A wolf was hit: the whole pack turns on you.
|
||||
func alert(_from: Vector3) -> void:
|
||||
@@ -227,7 +305,12 @@ func alert(_from: Vector3) -> void:
|
||||
|
||||
func howl() -> void:
|
||||
var live := alive()
|
||||
if _audio == null or live.is_empty():
|
||||
if live.is_empty():
|
||||
return
|
||||
if world.server_mode:
|
||||
CreatureNet.howl((live[0] as Node3D).global_position + Vector3(0, 1, 0)) # everyone hears it
|
||||
return
|
||||
if _audio == null:
|
||||
return
|
||||
_audio.global_position = (live[0] as Node3D).global_position + Vector3(0, 1, 0)
|
||||
_audio.pitch_scale = _rng.randf_range(0.9, 1.1)
|
||||
|
||||
Reference in New Issue
Block a user