150 lines
5.9 KiB
GDScript
150 lines
5.9 KiB
GDScript
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()
|