233 lines
8.0 KiB
GDScript
233 lines
8.0 KiB
GDScript
extends Node3D
|
|
## Thieves (docs/PROGRESSION_DESIGN.md §4): crows by day and foxes at dusk
|
|
## and night, after food left lying on the ground.
|
|
##
|
|
## Food on the ground that nobody has been within UNWATCHED of for
|
|
## NOTICE_AFTER seconds draws a thief. It comes from COME_FROM away, out of
|
|
## sight of everyone. A crow pecks the stack away one at a time; a fox runs
|
|
## off with all of it. Either one gives up if you come near (crow.gd,
|
|
## fox.gd). Food in a chest is never touched (that's what chests are for);
|
|
## the bear (bear_range.gd) is another matter.
|
|
##
|
|
## In a land with gulls (the Cove, 0.6.0: its worldlet's "wildlife"), food
|
|
## left on the sand draws a gull by day instead of a crow (gull.gd), and so
|
|
## does fish on a smoking rack that nobody's watching: a gull snatches one
|
|
## and flies off with it.
|
|
##
|
|
## On a server this runs for everyone (thieves are wildlife: creature_net.gd).
|
|
|
|
const CrowScript = preload("res://scripts/creatures/crow.gd")
|
|
const FoxScript = preload("res://scripts/creatures/fox.gd")
|
|
const GullScript = preload("res://scripts/creatures/gull.gd")
|
|
|
|
const CHECK_EVERY := 5.0
|
|
const UNWATCHED := 10.0
|
|
const NOTICE_AFTER := 20.0
|
|
const MAX_THIEVES := 3
|
|
const COME_FROM := 35.0
|
|
const OUT_OF_SIGHT := 45.0 ## this far from everyone, a thief that's leaving is gone
|
|
|
|
signal stolen(thief: Node, id: String, count: int)
|
|
|
|
var world: Node3D
|
|
var enabled := true
|
|
var gulls := false ## this land has gulls (the Cove)
|
|
var _acc := 0.0
|
|
var _unwatched: Dictionary = {} ## pickup instance id -> seconds left alone
|
|
var _rng := RandomNumberGenerator.new()
|
|
|
|
func setup(w: Node3D) -> void:
|
|
world = w
|
|
_rng.seed = w.terrain.noise_seed + 997
|
|
gulls = w.has_wildlife("gulls")
|
|
|
|
func _physics_process(delta: float) -> void:
|
|
if enabled:
|
|
step(delta)
|
|
|
|
## Living thieves, coming or going.
|
|
func alive() -> Array:
|
|
return get_children().filter(func(c): return is_instance_valid(c) and c.has_method("is_dead") and not c.is_dead() and not c.is_queued_for_deletion())
|
|
|
|
static func is_food(id: String) -> bool:
|
|
return ItemDB.get_item(id).has("food")
|
|
|
|
func night() -> bool:
|
|
var h: float = world.day_night.hours()
|
|
return h >= 17.5 or h < 6.5
|
|
|
|
## Someone (alive) within `r` of `pos`.
|
|
func watched(pos: Vector3, r: float) -> bool:
|
|
for p in world.players():
|
|
if not p.survival.dead and p.global_position.distance_to(pos) < r:
|
|
return true
|
|
return false
|
|
|
|
func out_of_sight(pos: Vector3) -> bool:
|
|
return not watched(pos, OUT_OF_SIGHT)
|
|
|
|
func still_there(food: Node3D) -> bool:
|
|
return food != null and is_instance_valid(food) and not food.is_queued_for_deletion() and not food.taken and int(food.amount) > 0
|
|
|
|
func _targeted(food: Node3D) -> bool:
|
|
for t in alive():
|
|
if t.food == food:
|
|
return true
|
|
return false
|
|
|
|
func step(dt: float) -> void:
|
|
_acc += dt
|
|
if _acc < CHECK_EVERY:
|
|
return
|
|
var since := _acc
|
|
_acc = 0.0
|
|
var seen := {}
|
|
for d in world.all_dropped():
|
|
if not is_food(d.item_id):
|
|
continue
|
|
var k: int = d.get_instance_id()
|
|
seen[k] = true
|
|
if watched(d.global_position, UNWATCHED):
|
|
_unwatched.erase(k)
|
|
continue
|
|
_unwatched[k] = float(_unwatched.get(k, 0.0)) + since
|
|
if float(_unwatched[k]) >= NOTICE_AFTER and not _targeted(d) and alive().size() < MAX_THIEVES:
|
|
send(d)
|
|
if gulls and not night():
|
|
for r in racks():
|
|
if not gull_still_there(r):
|
|
continue
|
|
var k: int = r.get_instance_id()
|
|
seen[k] = true
|
|
if watched(r.global_position, UNWATCHED):
|
|
_unwatched.erase(k)
|
|
continue
|
|
_unwatched[k] = float(_unwatched.get(k, 0.0)) + since
|
|
if float(_unwatched[k]) >= NOTICE_AFTER and not _targeted(r) and alive().size() < MAX_THIEVES:
|
|
send(r)
|
|
for k in _unwatched.keys():
|
|
if not seen.has(k):
|
|
_unwatched.erase(k)
|
|
|
|
## Sends a thief after `food`: a fox at dusk and night, otherwise a crow (a
|
|
## gull on the sand, or at a smoking rack, where there are gulls).
|
|
func send(food: Node3D) -> Node3D:
|
|
var fox := night()
|
|
var gull := not fox and gulls and (food.is_in_group("processor") or on_sand(food.global_position))
|
|
var fp: Vector3 = spot(food)
|
|
var from := Vector3.INF
|
|
for _i in range(12):
|
|
var a := _rng.randf() * TAU
|
|
var p := fp + Vector3(cos(a), 0, sin(a)) * COME_FROM
|
|
var ground: float = world.terrain.sample_height(p.x, p.z)
|
|
if world.terrain.inside_boundary(p.x, p.z, 6.0) and (not fox or ground > world.terrain.water_level + 0.3) \
|
|
and not watched(p, 20.0):
|
|
from = p
|
|
break
|
|
if from == Vector3.INF:
|
|
return null
|
|
var t: AnimatableBody3D = (FoxScript if fox else (GullScript if gull else CrowScript)).new()
|
|
t.setup("fox" if fox else ("gull" if gull else "crow"), world, from, _rng.randi())
|
|
t.thieves = self
|
|
t.food = food
|
|
t.state = "sneak" if fox else "fly"
|
|
add_child(t)
|
|
if not fox:
|
|
t.global_position.y = world.terrain.sample_height(from.x, from.z) + t.CRUISE
|
|
var to := fp - from
|
|
t.yaw = atan2(-to.x, -to.z)
|
|
t.rotation.y = t.yaw
|
|
return t
|
|
|
|
## Whoever left `food` there (anyone within 60 m) hears about it, once.
|
|
func _tell(food_pos: Vector3, text: String) -> void:
|
|
for p in world.players():
|
|
if p.global_position.distance_to(food_pos) < 60.0:
|
|
Actions.tell(p, text)
|
|
|
|
func landed(crow: Node, food: Node3D) -> void:
|
|
_tell(food.global_position, "A crow is pecking at your %s!" % ItemDB.item_name(food.item_id).to_lower())
|
|
|
|
## A crow takes one. True when that was the last of it.
|
|
func peck(crow: Node, food: Node3D) -> bool:
|
|
if not still_there(food):
|
|
return true
|
|
food.amount = int(food.amount) - 1
|
|
stolen.emit(crow, food.item_id, 1)
|
|
if int(food.amount) <= 0:
|
|
world.take_dropped(food)
|
|
return true
|
|
return false
|
|
|
|
## A fox takes the lot. Returns what it's carrying.
|
|
func grab(fox: Node, food: Node3D) -> Dictionary:
|
|
var c := {"id": String(food.item_id), "count": int(food.amount), "extra": food.extra.duplicate()}
|
|
_tell(food.global_position, "A fox has run off with your %s!" % ItemDB.item_name(c.id).to_lower())
|
|
world.take_dropped(food)
|
|
stolen.emit(fox, c.id, c.count)
|
|
return c
|
|
|
|
## A thief left with what it took (or empty-handed).
|
|
func gone(t: Node) -> void:
|
|
t.queue_free()
|
|
|
|
# --- gulls (the Cove) ----------------------------------------------------------------
|
|
|
|
func on_sand(pos: Vector3) -> bool:
|
|
return world.terrain.on_sand(pos.x, pos.z)
|
|
|
|
static func is_fish(id: String) -> bool:
|
|
return Dictionary(ItemDB.get_item(id).get("food", {})).get("diet", {}).has("fish")
|
|
|
|
## This land's smoking racks.
|
|
func racks() -> Array:
|
|
return get_tree().get_nodes_in_group("processor").filter(func(r): return is_instance_valid(r) and not r.is_queued_for_deletion() \
|
|
and String(r.item_id) == "smoking_rack" and world.is_ancestor_of(r))
|
|
|
|
## Fish on a rack: the first id there is, or "".
|
|
func _rack_fish(r: Node3D) -> String:
|
|
for st in r.inventory.slots:
|
|
if st != null and is_fish(String(st.id)):
|
|
return String(st.id)
|
|
return ""
|
|
|
|
## Whether a gull's target is still worth it: food on the ground, or a rack
|
|
## with fish on it.
|
|
func gull_still_there(t: Node3D) -> bool:
|
|
if t == null or not is_instance_valid(t) or t.is_queued_for_deletion():
|
|
return false
|
|
if t.is_in_group("processor"):
|
|
return not _rack_fish(t).is_empty()
|
|
return still_there(t)
|
|
|
|
## Where a thief lands for `t`: on food, or on the ground beside a rack.
|
|
func spot(t: Node3D) -> Vector3:
|
|
if t.is_in_group("processor"):
|
|
var p: Vector3 = t.global_position + t.global_basis.z * 0.8
|
|
return Vector3(p.x, world.terrain.sample_height(p.x, p.z), p.z)
|
|
return t.global_position
|
|
|
|
func gull_landed(_gull: Node, t: Node3D) -> void:
|
|
if t.is_in_group("processor"):
|
|
_tell(t.global_position, "A gull's at the fish on your smoking rack!")
|
|
else:
|
|
_tell(t.global_position, "A gull's landed by your %s!" % ItemDB.item_name(t.item_id).to_lower())
|
|
|
|
## A gull takes one and flies off with it. Returns what it's carrying.
|
|
func snatch(gull: Node, t: Node3D) -> Dictionary:
|
|
if not gull_still_there(t):
|
|
return {}
|
|
var c := {}
|
|
if t.is_in_group("processor"):
|
|
var id := _rack_fish(t)
|
|
t.inventory.remove(id, 1)
|
|
c = {"id": id, "count": 1, "extra": {}}
|
|
else:
|
|
c = {"id": String(t.item_id), "count": 1, "extra": t.extra.duplicate()}
|
|
t.amount = int(t.amount) - 1
|
|
if int(t.amount) <= 0:
|
|
world.take_dropped(t)
|
|
_tell(t.global_position, "A gull has snatched your %s!" % ItemDB.item_name(String(c.id)).to_lower())
|
|
stolen.emit(gull, String(c.id), 1)
|
|
return c
|