The Cove's wreck: driftwood and salt on the beach, the captain's sea chest (a treasure map, nails, salt; once each), each player's own X on the map; online too

This commit is contained in:
Paul Bailey
2026-11-07 02:00:31 +00:00
parent ec8caa9ef4
commit 419c71e638
17 changed files with 470 additions and 7 deletions

149
scripts/world/wreck.gd Normal file
View File

@@ -0,0 +1,149 @@
extends Node3D
## The Cove's wreck (docs/WORLDLETS_DESIGN.md §5, 0.6.0): an old ship driven
## up on the sand at the far end of the bay from the sea cave, her bow in the
## shallows, one side stove in. Inside, the captain's sea chest: a torn
## treasure map and a few useful things, **once for each player** (single-
## player and online alike: who's had theirs is saved with the Cove).
##
## Placed from the terrain alone, like the pier, so it's the same for
## everyone; only `opened` (who's had their share) is saved.
const Pier = preload("res://scripts/world/pier.gd")
const OAK := Color(0.36, 0.25, 0.16)
const DARK := Color(0.24, 0.17, 0.11)
const WET := Color(0.2, 0.18, 0.15)
const IRON := Color(0.22, 0.22, 0.24)
## What's in the sea chest, for each player once.
const LOOT := {"treasure_map": 1, "nails": 4, "salt": 2}
var world: Node3D
var opened: Array = [] ## the player ids who've had their share
var chest: StaticBody3D
var front := Vector3.ZERO ## where you stand to reach into the chest
func place(w: Node3D) -> bool:
world = w
var t: Node3D = w.terrain
if not w.has_place("wreck") or not t._has_beach:
return false
# The far end of the beach from the sea cave, where the sand meets the water.
var a := deg_to_rad(t.beach_angle_deg - t.beach_half_width_deg * 0.8)
var dir := Vector2(cos(a), sin(a))
var r: float = t._shore_d() - t._rim_offset(dir.x, dir.y)
var at2 := dir * (r - 1.0)
var at := Vector3(at2.x, maxf(t.sample_height(at2.x, at2.y), t.water_level - 0.3), at2.y)
# Bow out to sea (+Z local), a little askew, heeled over on her side.
var out := Vector3(dir.x, 0.0, dir.y).rotated(Vector3.UP, 0.35)
global_transform = Transform3D(Basis.looking_at(-out, Vector3.UP), at)
_build()
add_to_group("wreck")
return true
func _build() -> void:
var st := SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLES)
var body := StaticBody3D.new()
body.name = "Hull"
add_child(body)
var heel := Basis(Vector3(0, 0, 1), deg_to_rad(-14.0))
var parts: Array = [
# [centre, size, colour] in the hull's frame (+Z the bow, +X starboard)
[Vector3(0, 0.12, 0), Vector3(0.35, 0.25, 9.0), DARK], # keel
[Vector3(-1.45, 0.75, 0), Vector3(0.14, 1.3, 8.2), OAK], # port side, whole
[Vector3(1.45, 0.75, 2.6), Vector3(0.14, 1.3, 3.0), OAK], # starboard, forward
[Vector3(1.45, 0.55, -3.0), Vector3(0.14, 0.9, 2.2), OAK], # starboard, aft (stove in between)
[Vector3(0, 0.75, -4.1), Vector3(2.9, 1.3, 0.14), OAK], # transom
[Vector3(0, 0.05, 0), Vector3(2.8, 0.1, 7.6), WET], # bilge boards
[Vector3(0, 1.8, 1.4), Vector3(0.22, 2.6, 0.22), DARK], # mast stump
]
for p in parts:
var c: Vector3 = heel * p[0]
Pier._box(st, c, p[1], p[2])
var cs := CollisionShape3D.new()
var box := BoxShape3D.new()
box.size = p[1]
cs.shape = box
cs.transform = Transform3D(heel, c)
body.add_child(cs)
# Ribs over the open hull, some broken off short.
for i in range(6):
var z := -3.4 + i * 1.3
for side in [-1.0, 1.0]:
if side > 0.0 and z > -1.8 and z < 1.0:
continue # (the stove-in side)
var h := 1.9 if (i + int(side)) % 3 != 0 else 1.2
Pier._box(st, heel * Vector3(side * 1.3, h * 0.5 + 0.5, z), Vector3(0.12, h, 0.14), DARK)
# Planks strewn on the sand on the open side.
var rng := RandomNumberGenerator.new()
rng.seed = 919
for i in range(5):
var off := Vector3(2.6 + rng.randf() * 2.5, 0.05, -2.0 + rng.randf() * 4.0)
Pier._box(st, off, Vector3(0.3, 0.06, 1.6 + rng.randf()), OAK.darkened(rng.randf() * 0.3))
st.generate_normals()
var mi := MeshInstance3D.new()
mi.name = "Ship"
mi.mesh = st.commit()
var mat := StandardMaterial3D.new()
mat.vertex_color_use_as_albedo = true
mat.roughness = 0.95
mi.material_override = mat
add_child(mi)
# The captain's sea chest, wedged inside by the break.
chest = StaticBody3D.new()
chest.name = "SeaChest"
chest.collision_layer = 1 | 2
chest.set_script(preload("res://scripts/world/wreck_chest.gd"))
chest.wreck = self
var cst := SurfaceTool.new()
cst.begin(Mesh.PRIMITIVE_TRIANGLES)
Pier._box(cst, Vector3(0, 0.25, 0), Vector3(0.9, 0.5, 0.55), Color(0.3, 0.2, 0.12))
Pier._box(cst, Vector3(0, 0.55, 0), Vector3(0.92, 0.12, 0.57), Color(0.27, 0.18, 0.1))
for x in [-0.32, 0.32]:
Pier._box(cst, Vector3(x, 0.32, 0), Vector3(0.06, 0.64, 0.6), IRON)
cst.generate_normals()
var cmi := MeshInstance3D.new()
cmi.mesh = cst.commit()
cmi.material_override = mat
chest.add_child(cmi)
var ccs := CollisionShape3D.new()
var cb := BoxShape3D.new()
cb.size = Vector3(0.9, 0.62, 0.55)
ccs.shape = cb
ccs.position = Vector3(0, 0.31, 0)
chest.add_child(ccs)
add_child(chest)
chest.transform = Transform3D(heel * Basis(Vector3.UP, 0.4), heel * Vector3(0.5, 0.12, -0.4))
front = global_transform * (heel * Vector3(2.4, 0.0, -0.4))
front.y = world.terrain.sample_height(front.x, front.z)
# --- the sea chest -------------------------------------------------------------
## Whether player `id` has had their share of the chest.
func had(id: String) -> bool:
return id in opened
## Gives player `p` (id `id`) their share: the map and the rest, once.
## Returns false (and says why) when they've had it, or their bag's too full.
func open_for(p: Node, id: String) -> bool:
if had(id):
Actions.tell(p, "The sea chest's empty: you've had what was in it")
return false
for item in LOOT:
if p.inventory.space_for(item) < int(LOOT[item]):
Actions.tell(p, "Make room in your bag first")
return false
for item in LOOT:
p.inventory.add(item, int(LOOT[item]))
opened.append(id)
Actions.tell(p, "In the captain's sea chest: a torn treasure map, %d nails and %d salt. Right-click the map to read it" % [
int(LOOT.nails), int(LOOT.salt)])
Sfx.at("door_open", chest.global_position)
if Net.is_server():
WorldNet.mark_changed()
return true
func to_data() -> Dictionary:
return {"opened": opened.duplicate()}
func load_data(d: Dictionary) -> void:
opened = (d.get("opened", []) as Array).duplicate()