extends Node3D ## The Cove's moorings (0.6.0, docs/WORLDLETS_DESIGN.md ยง5): berths along ## the end of the pier, and a mooring post on the deck. Build a rowing boat at ## a workbench, carry it down to the pier and press E at the post: it's tied ## up at a berth of its own, one boat for each player, and it stays there ## (saved with the Cove; online the server decides, and everyone sees ## everyone's boat). Your own boat's prompt is "Set sail", but nobody's ## charted the way to the Highlands yet: that's the next part of the journey, ## and the ship's compass from the pirate chest is what it'll need. const ItemMeshes = preload("res://scripts/items/item_meshes.gd") const BoatScript = preload("res://scripts/world/boat.gd") const PostScript = preload("res://scripts/world/mooring_post.gd") const BERTHS := 6 ## three each side (any further in, it shallows) const BOAT_SCALE := 8.0 ## the item's model (0.55 m) up to a boat (4.4 m) const BERTH_GAP := 5.0 const BOAT_ITEM := "rowing_boat" var world: Node3D var pier: Node3D var moored: Dictionary = {} ## player id -> {"berth": i, "name": who} var boats: Dictionary = {} ## player id -> boat.gd var post: StaticBody3D var _t := 0.0 func setup(w: Node3D) -> bool: world = w pier = w.pier if pier == null: return false add_to_group("moorings") post = StaticBody3D.new() post.name = "MooringPost" post.collision_layer = 1 | 2 post.set_script(PostScript) post.moorings = self var st := SurfaceTool.new() st.begin(Mesh.PRIMITIVE_TRIANGLES) pier._box(st, Vector3(0, 0.45, 0), Vector3(0.24, 0.9, 0.24), pier.POST) pier._box(st, Vector3(0, 0.8, 0), Vector3(0.32, 0.08, 0.32), pier.POST.darkened(0.2)) pier._box(st, Vector3(0, 0.62, 0), Vector3(0.27, 0.1, 0.27), Color(0.62, 0.55, 0.38)) # (a coil of rope) st.generate_normals() var mi := MeshInstance3D.new() mi.mesh = st.commit() var mat := StandardMaterial3D.new() mat.vertex_color_use_as_albedo = true mat.roughness = 0.9 mi.material_override = mat post.add_child(mi) var cs := CollisionShape3D.new() var box := BoxShape3D.new() box.size = Vector3(0.3, 0.9, 0.3) cs.shape = box cs.position = Vector3(0, 0.45, 0) post.add_child(cs) add_child(post) post.global_position = pier.end - pier.dir * 1.2 + _right() * (pier.WIDTH * 0.5 - 0.3) return true func _right() -> Vector3: return pier.dir.cross(Vector3.UP).normalized() ## Where berth `i` is: alongside the end of the pier, in pairs, bow out to sea. func berth_xf(i: int) -> Transform3D: var side := 1.0 if i % 2 == 0 else -1.0 var along: Vector3 = pier.end - pier.dir * (2.0 + (i / 2) * BERTH_GAP) var p: Vector3 = along + _right() * side * (pier.WIDTH * 0.5 + 1.0) p.y = world.terrain.sea_level - 0.08 return Transform3D(Basis.looking_at(pier.dir, Vector3.UP), p) func has_boat(id: String) -> bool: return moored.has(id) func _free_berth() -> int: var used := moored.values().map(func(m): return int(m.berth)) for i in range(BERTHS): if not i in used: return i return -1 ## Player `p` (id `id`) ties up the boat in their bag. Says why not and ## returns false when they can't. func moor(p: Node, id: String) -> bool: if has_boat(id): Actions.tell(p, "Your boat's already tied up here") return false if p.inventory.count(BOAT_ITEM) <= 0: Actions.tell(p, "Build a rowing boat at a workbench and bring it here to tie it up") return false var b := _free_berth() if b < 0: Actions.tell(p, "Every berth along the pier is taken") return false p.inventory.remove(BOAT_ITEM, 1) var who := String(p.get("display")) if p.get("display") != null else "" moored[id] = {"berth": b, "name": who} _sync() Actions.tell(p, "You tie your boat up at the end of the pier") Sfx.at("creak", post.global_position) if Net.is_server(): WorldNet.mark_changed() return true ## Your boat: off to the Highlands. Not yet. func set_sail(p: Node) -> void: if p.inventory.count("ship_compass") <= 0: Actions.tell(p, "Past the bay it's open sea: without a ship's compass you'd be lost within the hour") return Actions.tell(p, "The compass needle points north over the water, but nobody's charted the way to the Highlands yet. (That's the next part of the journey: coming in a later version.)") func _sync() -> void: for id in boats.keys(): if not moored.has(id): boats[id].queue_free() boats.erase(id) for id in moored: if boats.has(id): continue var b: StaticBody3D = StaticBody3D.new() b.name = "Boat_%d" % int(moored[id].berth) b.collision_layer = 1 | 2 b.set_script(BoatScript) b.moorings = self b.owner_id = String(id) var mi := MeshInstance3D.new() mi.name = "Hull" mi.mesh = ItemMeshes.build(BOAT_ITEM) mi.material_override = ItemDB.item_material() mi.scale = Vector3.ONE * BOAT_SCALE b.add_child(mi) var cs := CollisionShape3D.new() var box := BoxShape3D.new() box.size = Vector3(1.4, 0.6, 4.2) cs.shape = box cs.position = Vector3(0, 0.3, 0) b.add_child(cs) add_child(b) b.global_transform = berth_xf(int(moored[id].berth)) b.rest = b.global_transform boats[id] = b ## A gentle bob on the swell (just the look). func _process(delta: float) -> void: _t += delta for id in boats: var b: Node3D = boats[id] var k := float(int(moored[id].berth)) * 1.7 b.global_transform = b.rest.translated(Vector3(0, sin(_t * 1.1 + k) * 0.05, 0)) * Transform3D(Basis(Vector3(0, 0, 1), sin(_t * 0.8 + k) * 0.03), Vector3.ZERO) func to_data() -> Dictionary: return {"moored": moored.duplicate(true)} func load_data(d: Dictionary) -> void: moored = {} var m: Dictionary = d.get("moored", {}) for id in m: moored[String(id)] = {"berth": int(m[id].get("berth", 0)), "name": String(m[id].get("name", ""))} _sync()