The rowing boat: seagrass at the tide line and resin from pines; rope, pitch, and a boat at the workbench (driftwood, rope, pitch, nails); tied up at a berth of its own at the end of the pier, one each, saved; Set sail needs the ship's compass and the Highlands aren't charted yet; online too

This commit is contained in:
Paul Bailey
2026-11-07 11:16:15 +00:00
parent 9709d2c424
commit 2004fe193c
17 changed files with 579 additions and 5 deletions

22
scripts/world/boat.gd Normal file
View File

@@ -0,0 +1,22 @@
extends StaticBody3D
## A rowing boat tied up at the pier (moorings.gd): whose it is, and, for
## its owner, "Set sail".
const PostScript = preload("res://scripts/world/mooring_post.gd")
var moorings: Node3D
var owner_id := ""
var rest := Transform3D() ## where it's tied up (it bobs about this)
func mine(player: Node) -> bool:
return PostScript.id_of(player) == owner_id
func interact_prompt(player: Node) -> String:
if mine(player):
return "Your rowing boat [E] Set sail"
var who := String(moorings.moored.get(owner_id, {}).get("name", ""))
return "%s's rowing boat" % who if not who.is_empty() else "Someone's rowing boat"
func interact(player: Node) -> void:
if mine(player):
moorings.set_sail(player)

View File

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

View File

@@ -0,0 +1,25 @@
extends StaticBody3D
## The mooring post at the end of the pier (moorings.gd): E ties up the
## rowing boat in your bag. Online, your game asks the server.
var moorings: Node3D
## Whose boat: the player's id (a server's character, or, at home, yours).
static func id_of(player: Node) -> String:
var pid: Variant = player.get("player_id")
if pid != null and not String(pid).is_empty():
return String(pid)
return String(Net.get_identity().player_id)
func interact_prompt(player: Node) -> String:
if moorings.has_boat(id_of(player)):
return "Mooring post (your boat's tied up here)"
if player.inventory.count(moorings.BOAT_ITEM) > 0:
return "Mooring post [E] Tie up your boat"
return "Mooring post (build a rowing boat at a workbench, and tie it up here)"
func interact(player: Node) -> void:
if moorings.world.is_online():
PlayerNet.ask("moor", [])
return
moorings.moor(player, id_of(player))

View File

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

158
scripts/world/moorings.gd Normal file
View File

@@ -0,0 +1,158 @@
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()

View File

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

View File

@@ -409,6 +409,8 @@ func _scatter_trees() -> void:
node.drops_final = {"wood": [2, 3], "stick": [2, 4]}
node.max_hp = 3.0
node.hp = 3.0
elif vi in conifers:
node.drops_final["pine_resin"] = [1, 2] # (0.6.0: pitch for a boat)
var cs := CollisionShape3D.new()
cs.shape = cap
cs.position = Vector3(0, cap.height * 0.5 + 0.1, 0)
@@ -664,9 +666,9 @@ func _scatter_pickups() -> void:
rng.seed = world_seed + 6
# Order matters: new kinds go at the END so existing saves keep their ids.
var want := {"stick": 260, "stone": 170, "flint": 80, "mushroom": 110, "feather": 70, "fiber": 150,
"iron_nugget": 70, "clay": 60, "salt": 40, "driftwood": 45}
"iron_nugget": 70, "clay": 60, "salt": 40, "driftwood": 45, "seagrass": 40}
var radius: Dictionary = {"stick": 0.35, "stone": 0.25, "flint": 0.25, "mushroom": 0.25, "feather": 0.25, "fiber": 0.45,
"iron_nugget": 0.25, "clay": 0.3, "salt": 0.25, "driftwood": 0.5}
"iron_nugget": 0.25, "clay": 0.3, "salt": 0.25, "driftwood": 0.5, "seagrass": 0.4}
for id: String in want:
# Fibre grows as a plant (wild flax) you pick, 2 fibre a clump.
var mesh: Mesh = ItemMeshes.build("fiber_plant") if id == "fiber" else ItemDB.mesh_for(id)
@@ -718,8 +720,11 @@ func _scatter_pickups() -> void:
# Washed up along a beach (the Cove), near the waterline:
# what a boat's hull is made of.
chance = 0.95 if terrain.beach_weight(x, z) > 0.6 and h < terrain.water_level + 1.6 else 0.0
"seagrass":
# (0.6.0) Washed up at the tide line, for a boat's rope.
chance = 0.95 if terrain.beach_weight(x, z) > 0.6 and h < terrain.water_level + 0.8 else 0.0
# A few right by the spawn so the first craft is quick.
if Vector2(x - _spawn.x, z - _spawn.z).length() < 18.0 and not id in ["iron_nugget", "clay", "salt", "driftwood"]:
if Vector2(x - _spawn.x, z - _spawn.z).length() < 18.0 and not id in ["iron_nugget", "clay", "salt", "driftwood", "seagrass"]:
chance += 0.5
if keep > chance:
continue

View File

@@ -57,6 +57,7 @@ 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 moorings: Node3D ## the boats tied up at the pier (scripts/world/moorings.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
@@ -368,6 +369,15 @@ func _on_terrain_ready() -> void:
props.pier = pr
else:
pr.free()
# The moorings (0.6.0): rowing boats tied up at the end of the pier.
if pier != null and not menu_mode:
var mo := preload("res://scripts/world/moorings.gd").new()
mo.name = "Moorings"
add_child(mo)
if mo.setup(self):
moorings = mo
else:
mo.free()
# The wreck (0.6.0): on the Cove's beach, with the sea chest's treasure maps.
var wk := preload("res://scripts/world/wreck.gd").new()
wk.name = "Wreck"
@@ -717,6 +727,7 @@ func collect_save() -> Dictionary:
"tunnel": tunnel.to_data() if tunnel != null else {},
"wreck": wreck.to_data() if wreck != null else {},
"treasure": treasure.to_data() if treasure != null else {},
"moorings": moorings.to_data() if moorings != null else {},
"map": map_data(),
}
@@ -735,6 +746,7 @@ func _shared_state(harvested: Dictionary, regrow: Dictionary, taken: Array, drop
"tunnel": tunnel.to_data() if tunnel != null else {},
"wreck": wreck.to_data() if wreck != null else {},
"treasure": treasure.to_data() if treasure != null else {},
"moorings": moorings.to_data() if moorings != null else {},
}
## The live pieces (placed and not being removed).
@@ -872,6 +884,8 @@ func _apply_save(s: Dictionary, with_player: bool) -> void:
wreck.load_data(s.get("wreck", {}))
if treasure != null:
treasure.load_data(s.get("treasure", {}))
if moorings != null:
moorings.load_data(s.get("moorings", {}))
if with_player:
load_map_data(String(s.get("map", "")))
# The bear (bear_range.gd): applied when it's set up, after loading.