J1: the cliff tunnel (seam, six propped stretches, dark, lights), travel Valley <-> Cove through the sea cave, a save file per worldlet; New World has no Where (--dev-worldlets to try others)

This commit is contained in:
Paul Bailey
2026-11-06 17:51:58 +00:00
parent 793b214786
commit 980ee53579
22 changed files with 1181 additions and 33 deletions

View File

@@ -54,6 +54,9 @@ var _spawn_yaw := 0.0
var _pieces: Node3D
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 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
var _autosave_left := AUTOSAVE_SECONDS
var _menu_cam: Camera3D
@@ -362,6 +365,25 @@ func _on_terrain_ready() -> void:
props.pier = pr
else:
pr.free()
# The journey's ways on (J1): the Valley's cliff tunnel and the Cove's sea
# cave. Single-player for now (J3 brings travel online).
if not server_mode and not menu_mode and not is_online():
var tn := preload("res://scripts/world/tunnel.gd").new()
tn.name = "Tunnel"
add_child(tn)
if tn.setup(self):
tunnel = tn
props.keep_clear.append([tn.seam_at, 4.0])
else:
tn.free()
var sc := preload("res://scripts/world/sea_cave.gd").new()
sc.name = "SeaCave"
add_child(sc)
if sc.place(self):
sea_cave = sc
props.keep_clear.append([sc.global_position, 6.0])
else:
sc.free()
props.place_on(terrain, player if not menu_mode else _menu_cam, _spawn)
# The wolf den: part of the scenery, so it's there from day 1 (empty).
den = preload("res://scripts/creatures/den.gd").new()
@@ -424,6 +446,7 @@ func _on_terrain_ready() -> void:
if not state.is_empty():
apply_save(state)
print("[world] loaded '%s'" % Saves.current_id)
_arrive(Saves.take_arrival(Saves.current_id))
player.set_physics_process(true)
set_physics_process(true)
creatures.setup(self)
@@ -652,6 +675,7 @@ func collect_save() -> Dictionary:
"bear": bears.save_data() if bears != null else _saved_bear,
"barrow": barrow.save_data() if barrow != null else {},
"stones": stones.save_data() if stones != null else {},
"tunnel": tunnel.to_data() if tunnel != null else {},
}
func _shared_state(harvested: Dictionary, regrow: Dictionary, taken: Array, dropped: Array, pieces: Array) -> Dictionary:
@@ -797,6 +821,8 @@ func _apply_save(s: Dictionary, with_player: bool) -> void:
barrow.load_data(s.get("barrow", {}))
if stones != null:
stones.load_data(s.get("stones", {}))
if tunnel != null:
tunnel.load_data(s.get("tunnel", {}))
# The bear (bear_range.gd): applied when it's set up, after loading.
_saved_bear = s.get("bear", {})
if bears != null:
@@ -822,14 +848,82 @@ func _respawn() -> void:
player.get_node("Camera3D").rotation.x = 0.0
func _physics_process(delta: float) -> void:
_current(delta)
var p := player.global_position
var under := underground_at(p)
day_night.underground = move_toward(day_night.underground, 1.0 if under else 0.0, delta * 3.0)
if weather_fx != null:
weather_fx.visible = not under
# (No water down there: the tunnel is far below the lake's surface.)
player.water_level = -INF if under else terrain.water_level
if under:
return # (the tunnel is its own space, out of sight under the island)
_current(delta)
var half: float = terrain.size * 0.5
if p.y < terrain.min_height - 30.0 or absf(p.x) > half or absf(p.z) > half \
or not terrain.inside_boundary(p.x, p.z, -4.0):
push_warning("[world] player left the map at %s; respawning" % p)
_respawn()
## In the cliff tunnel (J1): no daylight, no weather, sheltered.
func underground_at(p: Vector3) -> bool:
return tunnel != null and tunnel.contains(p)
# --- the journey (J1, docs/WORLDLETS_DESIGN.md §10) ------------------------------
## Goes on to worldlet `to`: saves this one, takes your character (bag,
## stats, what you wear) and the clock along, and loads the other, arriving
## at `arrive` ("sea_cave", "tunnel_bottom"). Each worldlet keeps its own
## state file (Saves.state_path), so what you built here waits for you.
func travel(to: String, arrive: String) -> void:
if traveling or not Saves.has_world() or is_online() or server_mode:
return
traveling = true
var id := Saves.current_id
var here: Dictionary = collect_save()
Saves.save_state(id, here, _session_time)
_session_time = 0.0
var there: Dictionary = Saves.load_state(id, to)
if there.is_empty():
there = {"time": here.time} # (a new place: the same day and hour)
there["player"] = here.player
Saves.save_state(id, there, 0.0, to)
Saves.set_worldlet(id, to, arrive)
print("[world] travelling to %s (%s)" % [to, arrive])
if hud != null:
hud.toast("On to the %s..." % Terrain.worldlet_name(to))
travelled.emit(to)
if get_tree().current_scene == self: # (in a test the world isn't the scene: the test loads the next one)
get_tree().change_scene_to_file.call_deferred(scene_file_path if not scene_file_path.is_empty() else "res://scenes/world.tscn")
signal travelled(to: String) ## saved and on the way to another worldlet
## Just arrived from another worldlet: put you where you came in.
func _arrive(at: String) -> void:
if at.is_empty():
return
var pos := Vector3.INF
var facing := Vector3.FORWARD
match at:
"sea_cave":
if sea_cave != null:
pos = sea_cave.front
facing = (sea_cave.front - sea_cave.global_position).normalized()
"tunnel_bottom":
if tunnel != null and tunnel.dug >= tunnel.STRETCHES:
pos = tunnel.bottom + Vector3(0.8, 0.0, 0.0)
facing = Vector3(1, 0, 0)
if pos == Vector3.INF:
push_warning("[world] nowhere to arrive at '%s': the start instead" % at)
_respawn()
return
player.global_position = pos + Vector3(0, 0.2, 0)
player.velocity = Vector3.ZERO
player.rotation = Vector3(0.0, atan2(-facing.x, -facing.z), 0.0)
if hud != null:
hud.toast("The %s" % Terrain.worldlet_name(worldlet_id()))
const Terrain = preload("res://scripts/world/terrain.gd")
## Off a beach (W2), a current carries anyone swimming too far out back
## towards the shore: the sea's version of the invisible wall. It's faster
## than you can swim, so you can't push through it, and it says so once.
@@ -858,9 +952,10 @@ func placement_valid(def: Dictionary, xf: Transform3D) -> bool:
if BuildPiece.is_build(def):
return build.check(def, xf).is_empty()
var p := xf.origin
if not terrain.inside_boundary(p.x, p.z, 1.5):
var under := underground_at(p) # (candles and torch stands down the tunnel: its floor is the ground)
if not under and not terrain.inside_boundary(p.x, p.z, 1.5):
return false
if p.y < terrain.water_level + 0.15:
if not under and p.y < terrain.water_level + 0.15:
return false
var fp: Array = def.place.get("footprint", [1.0, 1.0])
var hx := float(fp[0]) * 0.5