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:
Paul Bailey
2026-11-07 09:54:51 +00:00
parent 74bd8f5898
commit 5bcfa23710
18 changed files with 594 additions and 11 deletions

View File

@@ -60,6 +60,12 @@ const KINDS := {
"notice": 0.0, "notice_loud": 0.0, "calm": 30.0,
"damage": 20.0, "attack_range": 2.0, "attack_cd": 2.0, "bone": true,
"drops": {"iron_nugget": [1, 2]}},
# 0.6.0: the drowned crew (guard.gd, at a treasure dig: treasure.gd). The
# Cove's barrow guards: slow, hard-hitting, unafraid of fire.
"drowned": {"name": "Drowned Sailor", "hp": 70.0, "walk": 1.3, "run": 3.1, "turn": 4.0,
"notice": 0.0, "notice_loud": 0.0, "calm": 30.0,
"damage": 16.0, "attack_range": 2.0, "attack_cd": 1.8, "bone": true,
"drops": {"salt": [0, 1]}},
# P5: the Old Stag (old_stag.gd), called by the standing stones, and the
# spirit wolves it summons (they just run at you: the base "charge").
"old_stag": {"name": "Old Stag", "hp": 900.0, "walk": 2.0, "run": 13.0, "turn": 3.0,
@@ -140,7 +146,7 @@ func setup(k: String, w: Node3D, pos: Vector3, seed: int) -> void:
var eyes := MeshInstance3D.new()
eyes.mesh = parts.eyes
var em := StandardMaterial3D.new()
var eye: Color = {"alpha": Color(1.0, 0.18, 0.1), "guard": Color(0.45, 0.8, 1.0), "old_stag": Color(0.55, 0.95, 1.0),
var eye: Color = {"alpha": Color(1.0, 0.18, 0.1), "guard": Color(0.45, 0.8, 1.0), "drowned": Color(0.5, 1.0, 0.6), "old_stag": Color(0.55, 0.95, 1.0),
"spirit_wolf": Color(0.55, 0.95, 1.0)}.get(k, Color(1.0, 0.75, 0.3))
em.albedo_color = eye.lerp(Color.WHITE, 0.2)
em.emission_enabled = true
@@ -164,7 +170,7 @@ func setup(k: String, w: Node3D, pos: Vector3, seed: int) -> void:
cap.height = maxf(parts.height, parts.radius * 2.0 + 0.01)
cs.shape = cap
cs.position = Vector3(0, parts.height * 0.5, 0)
if k != "rabbit" and k != "guard": # (rabbits are round, guards stand upright)
if not k in ["rabbit", "guard", "drowned"]: # (rabbits are round, guards stand upright)
# Long bodies: lay the capsule along the spine.
cap.height = {"deer": 1.5, "boar": 1.4, "wolf": 1.25, "alpha": 1.65, "bear": 2.0, "fox": 0.8, "crow": 0.3, "old_stag": 3.3, "spirit_wolf": 1.25}.get(k, 1.4)
cs.rotation.x = PI * 0.5
@@ -516,6 +522,7 @@ const VOICES := {
"boar": {"alert": "boar_grunt", "angry": "boar_grunt", "attack": "boar_grunt", "hurt": "boar_squeal", "idle": "boar_grunt"},
"old_stag": {"alert": "stag_bellow", "angry": "stag_bellow", "attack": "stag_snort", "hurt": "stag_snort"},
"guard": {"alert": "guard_rattle", "angry": "guard_rattle", "attack": "guard_rattle"},
"drowned": {"alert": "guard_rattle", "angry": "guard_rattle", "attack": "guard_rattle"},
"crow": {"alert": "crow_caw", "hurt": "crow_caw", "idle": "crow_caw"},
"fox": {"alert": "fox_yip", "hurt": "fox_yip"},
}

View File

@@ -23,6 +23,7 @@ static func build(kind: String) -> Dictionary:
"fox": d = _scaled(_wolf_parts(Color(0.78, 0.4, 0.15), Color(0.92, 0.88, 0.8), 281), 0.6)
"crow": d = _crow()
"guard": d = _guard()
"drowned": d = _drowned()
"old_stag": d = _scaled(_stag(), 2.1)
"spirit_wolf": d = _wolf_parts(Color(0.62, 0.72, 0.78), Color(0.85, 0.92, 0.95), 351)
_: d = _boar()
@@ -253,6 +254,53 @@ static func _guard() -> Dictionary:
"hips": [Vector3(-0.1, 0.93, 0), Vector3(0.1, 0.93, 0)],
"radius": 0.3, "height": 1.85}
## A drowned sailor (0.6.0): the barrow guard's bones, sea-greyed and hung
## with weed, a rag of a shirt, a bandana, and a cutlass; no shield or helm.
static func _drowned() -> Dictionary:
var bone := Color(0.62, 0.66, 0.58)
var old := Color(0.44, 0.5, 0.42)
var weed := Color(0.16, 0.32, 0.18)
var rag := Color(0.36, 0.33, 0.27)
var red := Color(0.45, 0.12, 0.1)
var steel := Color(0.5, 0.52, 0.55)
var dark := Color(0.05, 0.07, 0.06)
var st := P._begin()
var r := _rng(331)
var n := _noise(331, 2.2)
P._blob(st, Vector3(0, 0.95, 0), Vector3(0.17, 0.07, 0.1), n, 0.2, old, r, 0) # pelvis
P._cylinder(st, Vector3(0, 0.97, 0.02), Vector3(0, 1.45, 0.0), 0.035, 0.03, 5, bone, r) # spine
for i in range(4): # ribs
var y := 1.18 + 0.075 * i
P._cylinder(st, Vector3(-0.15 + 0.01 * i, y, -0.02), Vector3(0.15 - 0.01 * i, y, -0.02), 0.018, 0.018, 4, bone, r)
P._blob(st, Vector3(0.02, 1.3, 0.0), Vector3(0.19, 0.17, 0.12), n, 0.45, rag, r, 0) # shirt, in tatters
P._cylinder(st, Vector3(-0.22, 1.47, 0), Vector3(0.22, 1.47, 0), 0.025, 0.025, 4, bone, r) # shoulders
P._blob(st, Vector3(0, 1.66, -0.02), Vector3(0.11, 0.13, 0.12), n, 0.12, bone, r, 1) # skull
P._blob(st, Vector3(0, 1.56, -0.08), Vector3(0.07, 0.04, 0.06), n, 0.1, old, r, 0) # jaw
P._blob(st, Vector3(0, 1.75, 0.0), Vector3(0.12, 0.05, 0.13), n, 0.15, red, r, 0) # bandana
for s in [-1.0, 1.0]:
P._blob(st, Vector3(s * 0.045, 1.67, -0.115), Vector3(0.03, 0.025, 0.012), n, 0.0, dark, r, 0) # sockets
for i in range(5): # weed hanging off
var x := -0.16 + i * 0.08
P._cylinder(st, Vector3(x, 1.45, -0.04), Vector3(x + 0.02, 1.05 - 0.05 * (i % 2), -0.06), 0.012, 0.004, 3, weed, r)
# Right arm forward with the cutlass, left hanging.
P._cylinder(st, Vector3(0.22, 1.47, 0), Vector3(0.28, 1.2, -0.12), 0.025, 0.022, 4, bone, r)
P._cylinder(st, Vector3(0.28, 1.2, -0.12), Vector3(0.27, 1.08, -0.38), 0.022, 0.02, 4, bone, r)
P._cylinder(st, Vector3(0.27, 1.08, -0.4), Vector3(0.33, 1.42, -0.85), 0.035, 0.008, 4, steel, r) # curved blade
P._cylinder(st, Vector3(0.21, 1.06, -0.38), Vector3(0.33, 1.1, -0.4), 0.02, 0.02, 4, steel, r) # guard
P._cylinder(st, Vector3(-0.22, 1.47, 0), Vector3(-0.27, 1.15, 0.02), 0.025, 0.022, 4, bone, r)
P._cylinder(st, Vector3(-0.27, 1.15, 0.02), Vector3(-0.26, 0.9, -0.02), 0.022, 0.02, 4, bone, r)
var est := P._begin()
for s in [-1.0, 1.0]:
P._blob(est, Vector3(s * 0.045, 1.67, -0.125), Vector3.ONE * 0.012, n, 0.0, Color(0.5, 1.0, 0.6), r, 0)
var lst := P._begin()
P._cylinder(lst, Vector3(0, 0, 0), Vector3(0, -0.45, 0.02), 0.03, 0.025, 4, bone, r)
P._cylinder(lst, Vector3(0, -0.45, 0.02), Vector3(0, -0.9, 0), 0.025, 0.022, 4, bone, r)
P._blob(lst, Vector3(0, -0.2, 0), Vector3(0.07, 0.18, 0.07), n, 0.4, rag, r, 0) # breeches
P._blob(lst, Vector3(0, -0.9, -0.05), Vector3(0.04, 0.025, 0.08), n, 0.1, old, r, 0)
return {"body": P._commit(st), "eyes": P._commit(est), "leg": P._commit(lst),
"hips": [Vector3(-0.1, 0.93, 0), Vector3(0.1, 0.93, 0)],
"radius": 0.3, "height": 1.85}
## The Old Stag (before scaling up): a pale, moss-hung spirit deer with a
## huge crown of antlers.
static func _stag() -> Dictionary:

View File

@@ -18,11 +18,12 @@ func display_name() -> String:
func interact_prompt(p: Node) -> String:
if is_dead():
return "Bones [E] Search"
var s := "Barrow guard"
# (A barrow guard, or a drowned sailor at a treasure dig: treasure.gd.)
var s: String = def.name
match state:
"rest": s = "Barrow guard (still... for now)"
"charge": s = "Barrow guard (coming for you!)"
"home": s = "Barrow guard (going back to its post)"
"rest": s += " (still... for now)"
"charge": s += " (coming for you!)"
"home": s += " (going back to its post)"
if not Actions.melee_stats(p).armed and not ItemDB.get_item(p.held_item_id()).has("bow"):
s += " (you need a weapon: a club cracks bone)"
return s
@@ -46,7 +47,7 @@ func _think(dt: float) -> void:
_goal_d = d
if d < def.attack_range and _attack_cd <= 0.0 and _can_reach(player):
_attack_cd = def.attack_cd
player.survival.damage(def.damage * world.difficulty_mult("damage"), global_position, "a barrow guard")
player.survival.damage(def.damage * world.difficulty_mult("damage"), global_position, "a %s" % String(def.name).to_lower())
"home":
if barrow.wakes_for(player) and barrow.center().distance_to(pp) < barrow.LEASH:
state = "charge"

View File

@@ -38,6 +38,10 @@ static func build(key: String) -> ArrayMesh:
"stick": m = _stick()
"driftwood": m = _driftwood()
"treasure_map": m = _treasure_map()
"ship_compass": m = _ship_compass()
"spyglass": m = _spyglass()
"pirate_gold": m = _pirate_gold()
"cutlass": m = _cutlass()
"stone": m = _pebble(11, STONE, Vector3(0.14, 0.10, 0.12))
"flint": m = _pebble(23, FLINT, Vector3(0.12, 0.07, 0.15), 0.5)
"wood": m = _wood()
@@ -206,6 +210,50 @@ static func _treasure_map() -> ArrayMesh:
P._cylinder(st, Vector3(-0.01, 0.04, 0), Vector3(0.01, 0.04, 0), 0.043, 0.043, 8, Color(0.45, 0.12, 0.08), r)
return P._commit(st)
const BRASS := Color(0.78, 0.6, 0.25)
const GOLD := Color(0.95, 0.75, 0.2)
## A brass sea compass: a flat round case, a glass face, a red needle.
static func _ship_compass() -> ArrayMesh:
var st := P._begin()
var r := _rng(47)
P._cylinder(st, Vector3(0, 0.0, 0), Vector3(0, 0.05, 0), 0.11, 0.11, 12, BRASS, r, -1.0, BRASS)
P._cylinder(st, Vector3(0, 0.05, 0), Vector3(0, 0.056, 0), 0.095, 0.095, 12, Color(0.9, 0.88, 0.8), r, -1.0, Color(0.9, 0.88, 0.8))
P._cylinder(st, Vector3(0, 0.06, 0.07), Vector3(0, 0.06, -0.07), 0.012, 0.004, 4, Color(0.75, 0.1, 0.08), r)
return P._commit(st)
## A brass telescope, drawn out: three tubes, the big end forward.
static func _spyglass() -> ArrayMesh:
var st := P._begin()
var r := _rng(49)
P._cylinder(st, Vector3(0, 0.04, 0.22), Vector3(0, 0.04, 0.06), 0.03, 0.03, 8, BRASS, r, -1.0, BRASS)
P._cylinder(st, Vector3(0, 0.04, 0.06), Vector3(0, 0.04, -0.1), 0.036, 0.036, 8, Color(0.3, 0.2, 0.12), r, -1.0, BRASS)
P._cylinder(st, Vector3(0, 0.04, -0.1), Vector3(0, 0.04, -0.26), 0.044, 0.048, 8, BRASS, r, -1.0, Color(0.6, 0.7, 0.8))
return P._commit(st)
## A heap of old gold coins, and a few spilled.
static func _pirate_gold() -> ArrayMesh:
var st := P._begin()
var r := _rng(53)
var n := FastNoiseLite.new()
n.seed = 53
P._blob(st, Vector3(0, 0.06, 0), Vector3(0.2, 0.09, 0.18), n, 0.25, GOLD, r, 1)
for i in range(7):
var a := i * 0.9
var c := Vector3(cos(a) * (0.16 + 0.05 * (i % 3)), 0.01, sin(a) * (0.16 + 0.05 * (i % 3)))
P._cylinder(st, c, c + Vector3(0, 0.012, 0), 0.03, 0.03, 8, GOLD.darkened(0.1 * (i % 3)), r, -1.0, GOLD)
return P._commit(st)
## A cutlass: a curved, broad blade, a brass basket guard, a dark grip.
static func _cutlass() -> ArrayMesh:
var st := P._begin()
var r := _rng(59)
_handle(st, 0.14, r)
P._cylinder(st, Vector3(-0.06, 0.15, 0), Vector3(0.06, 0.15, 0), 0.025, 0.025, 5, BRASS, r)
P._cylinder(st, Vector3(0, 0.16, 0), Vector3(0.02, 0.45, 0), 0.03, 0.028, 4, Color(0.72, 0.74, 0.78), r)
P._cylinder(st, Vector3(0.02, 0.45, 0), Vector3(0.08, 0.72, 0), 0.028, 0.006, 4, Color(0.72, 0.74, 0.78), r)
return P._commit(st)
static func _wood() -> ArrayMesh:
var st := P._begin()
var r := _rng(9)

View File

@@ -99,7 +99,7 @@ func _server_tick(w: Node3D, delta: float) -> void:
const PER_PACKET := 40
const ENTRY := 23
const KIND_IDS := ["rabbit", "deer", "boar", "wolf", "alpha", "bear", "crow", "fox", "guard", "old_stag", "spirit_wolf"]
const KIND_IDS := ["rabbit", "deer", "boar", "wolf", "alpha", "bear", "crow", "fox", "guard", "old_stag", "spirit_wolf", "drowned"]
const STATE_IDS := ["idle", "wander", "flee", "charge", "dead", "rest", "alert", "stalk", "lunge", "retreat", "home", "defend", "bash",
"warn", "raid", "eat", "fly", "fly_off", "peck", "sneak", "carry", "circle", "telegraph", "stunned", "summon", "stomp"]
@@ -198,7 +198,7 @@ func _s2c_life(packed: PackedByteArray, fl: Array) -> void:
var c: Node3D = got if is_instance_valid(got) else null
if c == null:
# (Kinds with their own look: the bear rears up, a crow flaps.)
var script: Script = {"bear": BearScript, "crow": CrowScript, "guard": GuardScript, "old_stag": StagScript}.get(kind, Creature)
var script: Script = {"bear": BearScript, "crow": CrowScript, "guard": GuardScript, "drowned": GuardScript, "old_stag": StagScript}.get(kind, Creature)
c = script.new()
c.puppet = true
c.setup(kind, world, e[2], cid)

View File

@@ -354,7 +354,11 @@ func _run(p: Node3D, what: String, a: Array) -> void:
"hit":
var n: Node3D = w.props.resource_nodes.get(_s(a, 0))
var at: Vector3 = _v(a, 1)
if n == null and w.tunnel != null:
if n == null and _s(a, 0) == "treasure:" + String(p.player_id) and w.treasure != null:
var m: Node3D = w.treasure.mound_for(String(p.player_id)) # (your own X: treasure.gd)
if m != null and at.is_finite() and at.distance_to(m.global_position) < 2.5:
Actions.harvest_hit(p, m, at)
elif n == null and w.tunnel != null:
var f: Node3D = w.tunnel.face_by_uid(_s(a, 0)) # (the cliff tunnel's seam or face)
if f != null and at.is_finite():
Actions.harvest_hit(p, f, at) # (the tunnel tells everyone: tunnel.gd)
@@ -529,7 +533,7 @@ func _run(p: Node3D, what: String, a: Array) -> void:
for m in w.get_tree().get_nodes_in_group("processor"):
m.step(float(a[0]) if a.size() > 0 and (a[0] is float or a[0] is int) else 60.0)
"debug_spawn":
if cheats and Creature.KINDS.has(_s(a, 0)) and not (_s(a, 0) in ["wolf", "alpha", "bear", "crow", "fox", "guard", "old_stag", "spirit_wolf"]) and _v(a, 1).is_finite():
if cheats and Creature.KINDS.has(_s(a, 0)) and not (_s(a, 0) in ["wolf", "alpha", "bear", "crow", "fox", "guard", "old_stag", "spirit_wolf", "drowned"]) and _v(a, 1).is_finite():
w.creatures.spawn(_s(a, 0), _v(a, 1))
"debug_boss":
# Tests: ["hold", secs] stands the Old Stag still (stunned);

View File

@@ -377,6 +377,18 @@ func tunnel_state(d: Dictionary) -> void:
if Lands.hub:
Lands.push_tunnel(int(d.get("dug", 0)))
## Whose treasure's been dug (and how far): everyone here sees their own
## mound go down, or go.
func treasure_state(d: Dictionary) -> void:
_all("_s2c_treasure", [d])
mark_changed()
@rpc("authority", "call_remote", "reliable")
func _s2c_treasure(d: Dictionary) -> void:
if not is_client() or world.treasure == null:
return
world.treasure.load_data(d)
## Who's had their share of the wreck's sea chest changed: everyone here
## sees whether it's empty for them.
func wreck_state(d: Dictionary) -> void:

View File

@@ -145,6 +145,7 @@ func _target() -> Object:
func _physics_process(delta: float) -> void:
_swing_cd = maxf(_swing_cd - delta, 0.0)
_zoom(delta)
if player.ui_open:
_ghost.visible = false
_set_prompt("")
@@ -176,6 +177,8 @@ func _physics_process(delta: float) -> void:
_set_prompt("[RMB] Wear %s" % def.name)
elif def.has("read"):
_set_prompt("[RMB] Read the %s" % String(def.name).to_lower())
elif def.has("zoom"):
_set_prompt("[hold RMB] Look through the %s" % String(def.name).to_lower())
elif def.has("bow"):
var n: int = Actions.ammo_count(player, def.bow)
_set_prompt("Drawing %d%%" % roundi(draw_power() * 100.0) if _drawing else "[hold LMB] Draw (%d arrows)" % n)
@@ -234,6 +237,21 @@ func _update_ghost(def: Dictionary) -> void:
_ghost.material_override = _ghost_ok_mat if _place_ok else _ghost_bad_mat
_ghost.visible = true
## The spyglass (0.6.0): holding right-click with it narrows the view by its
## "zoom", eased in and out.
var _base_fov := 0.0
func _zoom(delta: float) -> void:
var cam := get_parent() as Camera3D
if cam == null:
return
if _base_fov <= 0.0:
_base_fov = cam.fov
var z := float(_held_def().get("zoom", 0.0))
var looking: bool = z > 1.0 and not player.ui_open and Input.is_action_pressed("use_item") \
and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED
var want: float = _base_fov / z if looking else _base_fov
cam.fov = move_toward(cam.fov, want, delta * 160.0)
func _unhandled_input(event: InputEvent) -> void:
if player.ui_open:
return

View 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()

View File

@@ -0,0 +1 @@
uid://dn5ytepdehl8k

209
scripts/world/treasure.gd Normal file
View 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)

View File

@@ -0,0 +1 @@
uid://dscr3836h55qo

View 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 {}

View File

@@ -0,0 +1 @@
uid://dc0iu7ixkjwkv

View File

@@ -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.