The pirate treasure: each player's own mound at their X, dug with a pickaxe; the drowned crew rise at night or when the chest comes up; the ship's compass, a spyglass, a cutlass and pirate gold; online too
This commit is contained in:
47
scripts/world/crew_site.gd
Normal file
47
scripts/world/crew_site.gd
Normal file
@@ -0,0 +1,47 @@
|
||||
extends Node3D
|
||||
## Where a band of the drowned crew rose (treasure.gd): what their guard
|
||||
## brains (guard.gd) ask of their "barrow". They're up and angry from the
|
||||
## start, chase whoever's nearest within LEASH of the dig, and go back to
|
||||
## stand round it when nobody is. By day, once nobody's been near for a
|
||||
## while, they sink back into the ground.
|
||||
|
||||
const LEASH := 20.0
|
||||
const SINK_AFTER := 30.0 ## seconds with nobody near, by day
|
||||
|
||||
var world: Node3D
|
||||
var spot := Vector3.ZERO
|
||||
var alarmed := true
|
||||
var crew: Array = []
|
||||
var _alone := 0.0
|
||||
|
||||
func center() -> Vector3:
|
||||
return spot
|
||||
|
||||
func wakes_for(p: Node3D) -> bool:
|
||||
return alarmed and not p.survival.dead
|
||||
|
||||
func alarm() -> void:
|
||||
alarmed = true
|
||||
|
||||
## (Out in the open: they walk on the ground.)
|
||||
func floor_y_at(_p: Vector3) -> float:
|
||||
return -INF
|
||||
|
||||
func is_night() -> bool:
|
||||
var h: float = world.day_night.hours()
|
||||
return h >= 19.5 or h < 5.5
|
||||
|
||||
func alive() -> Array:
|
||||
return crew.filter(func(g): return is_instance_valid(g) and not g.is_dead())
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
var anyone := false
|
||||
for p in world.players():
|
||||
if not p.survival.dead and p.global_position.distance_to(spot) < LEASH:
|
||||
anyone = true
|
||||
_alone = 0.0 if anyone or is_night() else _alone + delta
|
||||
if alive().is_empty() or _alone > SINK_AFTER:
|
||||
for g in crew:
|
||||
if is_instance_valid(g) and not g.is_dead():
|
||||
g.queue_free()
|
||||
queue_free()
|
||||
1
scripts/world/crew_site.gd.uid
Normal file
1
scripts/world/crew_site.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dn5ytepdehl8k
|
||||
209
scripts/world/treasure.gd
Normal file
209
scripts/world/treasure.gd
Normal file
@@ -0,0 +1,209 @@
|
||||
extends Node3D
|
||||
## The pirate treasure (0.6.0, docs/WORLDLETS_DESIGN.md §5). Each player's
|
||||
## treasure map (the wreck's sea chest) has its own X (world.treasure_spot).
|
||||
## While you carry your map, a mound of loose earth by a marker stone shows
|
||||
## at your X (only to you); a pickaxe digs it, and the pirate chest comes up:
|
||||
## a ship's compass, a spyglass, a cutlass and pirate gold. The map's used up.
|
||||
## Once for each player.
|
||||
##
|
||||
## The drowned crew guard it: digging at night brings them up round you on
|
||||
## the first blow; by day they rise as the chest comes up. They're the Cove's
|
||||
## barrow guards (guard.gd, kind "drowned", at a crew_site.gd).
|
||||
##
|
||||
## Single-player, all here. Online, the server keeps whose treasure's been
|
||||
## dug and how far, gives the loot and runs the crew; your game only shows
|
||||
## your own mound and asks the server to dig it.
|
||||
|
||||
const MoundScript = preload("res://scripts/world/treasure_mound.gd")
|
||||
const CrewSite = preload("res://scripts/world/crew_site.gd")
|
||||
const GuardScript = preload("res://scripts/creatures/guard.gd")
|
||||
const RockPile = preload("res://scripts/world/rock_pile.gd")
|
||||
const ProcMesh = preload("res://scripts/world/proc_mesh.gd")
|
||||
const LOOT := {"ship_compass": 1, "spyglass": 1, "cutlass": 1, "pirate_gold": 3}
|
||||
const CREW := 3
|
||||
const DIG_HP := 18.0
|
||||
|
||||
var world: Node3D
|
||||
var dug: Array = [] ## the player ids whose treasure's been dug up
|
||||
var left: Dictionary = {} ## player id -> what's left of their mound (dug a bit)
|
||||
var mounds: Dictionary = {} ## player id -> treasure_mound.gd
|
||||
var crews: Dictionary = {} ## player id -> crew_site.gd, while they're up
|
||||
var force_crew := false ## (tests) raise the crew even with creatures off
|
||||
var _look_left := 0.0
|
||||
|
||||
func setup(w: Node3D) -> bool:
|
||||
world = w
|
||||
if w.wreck == null:
|
||||
return false
|
||||
Actions.harvested.connect(_on_harvested)
|
||||
add_to_group("treasure")
|
||||
return true
|
||||
|
||||
# --- the mounds ---------------------------------------------------------------------
|
||||
|
||||
## Player `id`'s mound (made if need be), or null if theirs is dug or they
|
||||
## have no X here.
|
||||
func mound_for(id: String) -> StaticBody3D:
|
||||
if id in dug:
|
||||
return null
|
||||
if mounds.has(id) and is_instance_valid(mounds[id]):
|
||||
return mounds[id]
|
||||
var at: Vector3 = world.treasure_spot(id)
|
||||
if at == Vector3.INF:
|
||||
return null
|
||||
var m: StaticBody3D = MoundScript.new()
|
||||
m.name = "Mound_%s" % id.substr(0, 8)
|
||||
m.treasure = self
|
||||
m.owner_id = id
|
||||
m.uid = "treasure:" + id
|
||||
m.max_hp = DIG_HP
|
||||
m.hp = clampf(float(left.get(id, DIG_HP)), 0.5, DIG_HP)
|
||||
m.collision_layer = 1
|
||||
m.add_to_group("harvestable")
|
||||
var cs := CollisionShape3D.new()
|
||||
var box := BoxShape3D.new()
|
||||
box.size = Vector3(1.6, 0.6, 1.6)
|
||||
cs.shape = box
|
||||
cs.position = Vector3(0, 0.2, 0)
|
||||
m.add_child(cs)
|
||||
if not world.server_mode:
|
||||
_dress(m, id)
|
||||
add_child(m)
|
||||
m.global_position = at
|
||||
mounds[id] = m
|
||||
return m
|
||||
|
||||
## How a mound looks: loose earth, a marker stone, two sticks crossed in it.
|
||||
func _dress(m: Node3D, id: String) -> void:
|
||||
var st := ProcMesh._begin()
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = hash(id)
|
||||
var n := FastNoiseLite.new()
|
||||
n.seed = hash(id) % 1000
|
||||
n.frequency = 1.5
|
||||
ProcMesh._blob(st, Vector3(0, 0.05, 0), Vector3(0.9, 0.28, 0.8), n, 0.3, Color(0.34, 0.25, 0.16), r, 0)
|
||||
ProcMesh._cylinder(st, Vector3(-0.25, 0.1, -0.25), Vector3(0.25, 0.7, 0.25), 0.03, 0.025, 4, Color(0.4, 0.3, 0.18), r)
|
||||
ProcMesh._cylinder(st, Vector3(0.25, 0.1, -0.25), Vector3(-0.25, 0.7, 0.25), 0.03, 0.025, 4, Color(0.4, 0.3, 0.18), r)
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = ProcMesh._commit(st)
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.vertex_color_use_as_albedo = true
|
||||
mat.vertex_color_is_srgb = true
|
||||
mat.roughness = 1.0
|
||||
mi.material_override = mat
|
||||
m.add_child(mi)
|
||||
RockPile.add(m, null, hash(id) % 997, Vector3(1.3, 0.35, 0.2), Vector3(0.35, 0.8, 0.3), 0.6, RockPile.material(world.terrain), true)
|
||||
|
||||
## (Your game) your mound shows while you carry your map and it's not dug.
|
||||
func _process(delta: float) -> void:
|
||||
if world.server_mode:
|
||||
return
|
||||
_look_left -= delta
|
||||
if _look_left > 0.0:
|
||||
return
|
||||
_look_left = 0.5
|
||||
var me: String = world.my_id()
|
||||
var want: bool = not me in dug and world.player.inventory.count("treasure_map") > 0
|
||||
if want:
|
||||
mound_for(me)
|
||||
elif mounds.has(me) and is_instance_valid(mounds[me]):
|
||||
mounds[me].queue_free()
|
||||
mounds.erase(me)
|
||||
|
||||
# --- digging ------------------------------------------------------------------------
|
||||
|
||||
## A blow on a mound (treasure_mound.gd): remembered, and at night the first
|
||||
## one brings the crew up.
|
||||
func mound_hit(m: Node) -> void:
|
||||
left[m.owner_id] = m.hp
|
||||
if Net.is_server():
|
||||
WorldNet.treasure_state(to_data())
|
||||
if _night() and not crews.has(m.owner_id):
|
||||
_raise_crew(m.owner_id, m.global_position)
|
||||
|
||||
func _on_harvested(p: Node, node: Node, felled: bool) -> void:
|
||||
if not felled or node == null or node.get_script() != MoundScript or node.treasure != self:
|
||||
return
|
||||
_dug_up(p, String(node.owner_id), node.global_position)
|
||||
|
||||
## The chest comes up: the loot for the player who dug it, the map used up.
|
||||
func _dug_up(p: Node, id: String, at: Vector3) -> void:
|
||||
if id in dug:
|
||||
return
|
||||
dug.append(id)
|
||||
left.erase(id)
|
||||
if mounds.has(id) and is_instance_valid(mounds[id]):
|
||||
mounds[id].queue_free()
|
||||
mounds.erase(id)
|
||||
p.inventory.remove("treasure_map", 1)
|
||||
var spilled := false
|
||||
for item in LOOT:
|
||||
var rest: int = p.inventory.add(item, int(LOOT[item]))
|
||||
if rest > 0:
|
||||
spilled = true
|
||||
world.drop_item(item, rest, Transform3D(Basis(), at + Vector3(0, 0.3, 0)))
|
||||
Actions.tell(p, "The pirate chest! A ship's compass, a spyglass, a cutlass and %d pirate gold%s" % [
|
||||
int(LOOT.pirate_gold), " (some at your feet: your bag's full)" if spilled else ""])
|
||||
Sfx.at("door_open", at)
|
||||
if not crews.has(id):
|
||||
_raise_crew(id, at)
|
||||
if crews.has(id):
|
||||
Actions.tell(p, "Something's clawing its way up out of the ground...")
|
||||
if Net.is_server():
|
||||
WorldNet.treasure_state(to_data())
|
||||
|
||||
func _night() -> bool:
|
||||
var h: float = world.day_night.hours()
|
||||
return h >= 19.5 or h < 5.5
|
||||
|
||||
## The drowned crew rise round `at` (where creatures run: here alone, or the
|
||||
## server; not with --no-creatures, unless a test asks).
|
||||
func _raise_crew(id: String, at: Vector3) -> void:
|
||||
if world.is_online():
|
||||
return
|
||||
var running: bool = force_crew or (not "--no-creatures" in OS.get_cmdline_user_args() \
|
||||
and (world.server_mode or world.creatures.enabled))
|
||||
if not running:
|
||||
return
|
||||
var site: Node3D = CrewSite.new()
|
||||
site.name = "Crew_%s" % id.substr(0, 8)
|
||||
site.world = world
|
||||
site.spot = at
|
||||
add_child(site)
|
||||
var t: Node3D = world.terrain
|
||||
for i in range(CREW):
|
||||
var a := TAU * i / CREW + float(hash(id) % 100) * 0.06
|
||||
var post := at + Vector3(cos(a), 0, sin(a)) * 5.0
|
||||
post.y = t.sample_height(post.x, post.z)
|
||||
var g: AnimatableBody3D = GuardScript.new()
|
||||
g.setup("drowned", world, post, hash(id) % 10000 + i)
|
||||
g.barrow = site
|
||||
g.post = post
|
||||
g.post_yaw = atan2(-(at - post).x, -(at - post).z)
|
||||
g.state = "charge"
|
||||
g.yaw = g.post_yaw
|
||||
g.rotation.y = g.yaw
|
||||
g.top_level = true
|
||||
site.add_child(g)
|
||||
g.global_position = post
|
||||
site.crew.append(g)
|
||||
crews[id] = site
|
||||
site.tree_exited.connect(func(): crews.erase(id))
|
||||
|
||||
# --- saving ------------------------------------------------------------------------
|
||||
|
||||
func to_data() -> Dictionary:
|
||||
return {"dug": dug.duplicate(), "left": left.duplicate()}
|
||||
|
||||
func load_data(d: Dictionary) -> void:
|
||||
dug = (d.get("dug", []) as Array).duplicate()
|
||||
left = (d.get("left", {}) as Dictionary).duplicate()
|
||||
for id in mounds.keys():
|
||||
var m: Node = mounds[id]
|
||||
if not is_instance_valid(m):
|
||||
mounds.erase(id)
|
||||
elif id in dug:
|
||||
m.queue_free()
|
||||
mounds.erase(id)
|
||||
else:
|
||||
m.hp = clampf(float(left.get(id, DIG_HP)), 0.5, DIG_HP)
|
||||
1
scripts/world/treasure.gd.uid
Normal file
1
scripts/world/treasure.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dscr3836h55qo
|
||||
35
scripts/world/treasure_mound.gd
Normal file
35
scripts/world/treasure_mound.gd
Normal file
@@ -0,0 +1,35 @@
|
||||
extends StaticBody3D
|
||||
## Your X, on the ground (treasure.gd): a mound of loose earth by a marker
|
||||
## stone, with two sticks crossed in it. It answers like a boulder does
|
||||
## (Actions.harvest_hit), so a pickaxe digs it; the treasure decides the rest.
|
||||
|
||||
var treasure: Node3D
|
||||
var owner_id := ""
|
||||
var uid := ""
|
||||
var tool_kind := "pickaxe"
|
||||
var min_tier := 1
|
||||
var max_hp := 18.0
|
||||
var hp := 18.0
|
||||
var depleted := false
|
||||
var kind := "rock"
|
||||
var chip_color := Color(0.36, 0.27, 0.18)
|
||||
|
||||
func display_name() -> String:
|
||||
return "Loose earth"
|
||||
|
||||
func refusal() -> String:
|
||||
return ""
|
||||
|
||||
func interact_prompt(_player: Node) -> String:
|
||||
return "Loose earth by a marker stone: X marks the spot. [LMB] Dig with a pickaxe (about %d blows left)" % ceili(hp)
|
||||
|
||||
func interact(player: Node) -> void:
|
||||
Actions.tell(player, interact_prompt(player))
|
||||
|
||||
## One blow (from Actions.harvest_hit).
|
||||
func hit(damage: float, _at: Vector3, _from: Vector3) -> Dictionary:
|
||||
hp = maxf(hp - damage, 0.0)
|
||||
treasure.mound_hit(self)
|
||||
if hp <= 0.0 and not depleted:
|
||||
depleted = true
|
||||
return {}
|
||||
1
scripts/world/treasure_mound.gd.uid
Normal file
1
scripts/world/treasure_mound.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dc0iu7ixkjwkv
|
||||
@@ -56,6 +56,7 @@ var hud: CanvasLayer
|
||||
var pier: Node3D ## the Cove's pier (scripts/world/pier.gd), or null
|
||||
var tunnel: Node3D ## the cliff tunnel (scripts/world/tunnel.gd, J1), or null
|
||||
var wreck: Node3D ## the Cove's wreck (scripts/world/wreck.gd, 0.6.0), or null
|
||||
var treasure: Node3D ## the pirate treasure (scripts/world/treasure.gd), or null
|
||||
var sea_cave: Node3D ## the Cove's end of it (scripts/world/sea_cave.gd), or null
|
||||
var traveling := false ## on the way to another worldlet (the scene is about to change)
|
||||
var _session_time := 0.0 ## seconds played since the last save
|
||||
@@ -373,6 +374,13 @@ func _on_terrain_ready() -> void:
|
||||
if not menu_mode and wk.place(self):
|
||||
wreck = wk
|
||||
props.keep_clear.append([wk.global_position, 9.0])
|
||||
var tr := preload("res://scripts/world/treasure.gd").new()
|
||||
tr.name = "Treasure"
|
||||
add_child(tr)
|
||||
if tr.setup(self):
|
||||
treasure = tr
|
||||
else:
|
||||
tr.free()
|
||||
else:
|
||||
wk.free()
|
||||
# The journey's ways on (J1): the Valley's cliff tunnel and the Cove's sea
|
||||
@@ -701,6 +709,7 @@ func collect_save() -> Dictionary:
|
||||
"stones": stones.save_data() if stones != null else {},
|
||||
"tunnel": tunnel.to_data() if tunnel != null else {},
|
||||
"wreck": wreck.to_data() if wreck != null else {},
|
||||
"treasure": treasure.to_data() if treasure != null else {},
|
||||
"map": map_data(),
|
||||
}
|
||||
|
||||
@@ -718,6 +727,7 @@ func _shared_state(harvested: Dictionary, regrow: Dictionary, taken: Array, drop
|
||||
"stones": stones.save_data() if stones != null else {},
|
||||
"tunnel": tunnel.to_data() if tunnel != null else {},
|
||||
"wreck": wreck.to_data() if wreck != null else {},
|
||||
"treasure": treasure.to_data() if treasure != null else {},
|
||||
}
|
||||
|
||||
## The live pieces (placed and not being removed).
|
||||
@@ -853,6 +863,8 @@ func _apply_save(s: Dictionary, with_player: bool) -> void:
|
||||
tunnel.load_data(s.get("tunnel", {}))
|
||||
if wreck != null:
|
||||
wreck.load_data(s.get("wreck", {}))
|
||||
if treasure != null:
|
||||
treasure.load_data(s.get("treasure", {}))
|
||||
if with_player:
|
||||
load_map_data(String(s.get("map", "")))
|
||||
# The bear (bear_range.gd): applied when it's set up, after loading.
|
||||
|
||||
Reference in New Issue
Block a user