W0: terrain from worldlet building blocks (Valley in data/worldlets, identical heights), WorldInfo API, worldlet in saves and servers; the cliff tunnel plan

This commit is contained in:
Paul Bailey
2026-11-06 15:51:43 +00:00
parent f4df2bc99d
commit db4a659236
10 changed files with 329 additions and 19 deletions

View File

@@ -56,7 +56,7 @@ func create_world(world_name: String, seed: int, options: Dictionary = {}) -> St
var now := Time.get_unix_time_from_system()
var meta := {
"name": world_name, "seed": seed, "created": now, "last_played": now,
"play_time": 0.0, "version": VERSION,
"play_time": 0.0, "version": VERSION, "worldlet": "valley",
}
for k in options:
meta[k] = options[k]
@@ -65,7 +65,7 @@ func create_world(world_name: String, seed: int, options: Dictionary = {}) -> St
## The world slot called exactly `id` (a server's world), made with `seed` if
## it doesn't exist yet. An existing world keeps its own seed.
func open_or_create(id: String, seed: int) -> String:
func open_or_create(id: String, seed: int, worldlet: String = "valley") -> String:
if not read_meta(id).is_empty():
var had := int(read_meta(id).get("seed", seed))
if had != seed:
@@ -75,7 +75,7 @@ func open_or_create(id: String, seed: int) -> String:
var now := Time.get_unix_time_from_system()
_write_json(_dir(id).path_join("meta.json"), {
"name": id, "seed": seed, "created": now, "last_played": now,
"play_time": 0.0, "version": VERSION,
"play_time": 0.0, "version": VERSION, "worldlet": worldlet,
})
return id

View File

@@ -638,7 +638,7 @@ func _c2s_proof(sig: PackedByteArray) -> void:
print("[server] %s joined (id %s%s, %d/%d online)" % [name, id, ", admin" if p.admin else "",
players.size(), int(config.v("max_players"))])
# (The world's own seed: an existing world keeps the seed it was made with.)
var world := {"seed": int(_terrain.noise_seed), "worldlet": String(config.v("worldlet")), "spawn": start, "yaw": yaw,
var world := {"seed": int(_terrain.noise_seed), "worldlet": String(_terrain.worldlet_id), "spawn": start, "yaw": yaw,
"island_spawn": _spawn, "day": _world.day_night.day, "time": _world.day_night.time, "server_time": server_time,
"weather": _world.weather.override}
_s2c_welcome.rpc_id(peer, {"you": peer, "server": config.public_info(), "players": players, "world": world})

View File

@@ -49,7 +49,7 @@ func open_server_world(config: RefCounted) -> Node3D:
var id := String(config.v("world")).validate_filename()
if id.is_empty():
id = "server-world"
Saves.current_id = Saves.open_or_create(id, int(config.v("seed")))
Saves.current_id = Saves.open_or_create(id, int(config.v("seed")), String(config.v("worldlet")))
backup("start-up") # (the state it's about to load, before anyone changes it)
_backup_left = maxf(float(config.v("backup_hours")), 0.25) * 3600.0
var w: Node3D = load("res://scenes/world.tscn").instantiate()

View File

@@ -75,8 +75,15 @@ func _ready() -> void:
var saves := get_node_or_null("/root/Saves")
if net != null and net.state == "connected" and net.world_info.has("seed"):
noise_seed = int(net.world_info.seed)
worldlet_id = String(net.world_info.get("worldlet", worldlet_id))
elif saves != null and saves.has_world():
noise_seed = saves.world_seed()
# (worlds from before worldlets have none: they're the Valley)
worldlet_id = String(saves.read_meta(saves.current_id).get("worldlet", "valley"))
var why := use_worldlet(worldlet_id)
if not why.is_empty():
push_error("[terrain] %s: using the Valley" % why)
use_worldlet("valley")
_build()
# --- height function --------------------------------------------------------
@@ -135,14 +142,114 @@ func _sea_w(x: float, z: float) -> float:
func _wall_threshold(x: float, z: float) -> float:
return lerpf(_wall_d(), _cliff_d() - 1.0, _sea_w(x, z))
# --- worldlets (docs/WORLDLETS_DESIGN.md, W0) ---------------------------------------
# A worldlet's terrain is a list of building blocks applied in order
# (data/worldlets/<id>.json "terrain"). Each block takes the ground so far and
# returns it changed; "hills" starts it. A worldlet can only combine these.
const BLOCKS := ["hills", "rim", "sea_cliff", "lake", "ocean_edge"]
var worldlet_id := "valley"
var worldlet: Dictionary = {} ## the definition, as loaded
var _blocks: Array = [] ## its terrain blocks, in order
var _valley := 0.0 ## the hills before anything else (the sea cliff eases from it)
var _detail_k := 1.4
## The definition of worldlet `id` (data/worldlets/<id>.json), or {} if none.
static func load_worldlet(id: String) -> Dictionary:
var p := "res://data/worldlets/%s.json" % id
if not FileAccess.file_exists(p):
return {}
var d: Variant = JSON.parse_string(FileAccess.get_file_as_string(p))
return d if d is Dictionary else {}
## Every worldlet there is: [{id, name, description}].
static func worldlets() -> Array:
var out: Array = []
for f in DirAccess.get_files_at("res://data/worldlets"):
if f.ends_with(".json"):
var d := load_worldlet(f.get_basename())
if not d.is_empty():
out.append({"id": String(d.id), "name": String(d.get("name", d.id)), "description": String(d.get("description", ""))})
return out
## Takes on worldlet `id`: its size and every block's numbers. Returns ""
## on success, else what's wrong (an unknown worldlet or block keeps the old
## settings, so the game still has an island).
func use_worldlet(id: String) -> String:
var d := load_worldlet(id)
if d.is_empty():
return "no worldlet '%s'" % id
for b in d.get("terrain", []):
if not (b is Dictionary) or not String(b.get("type", "")) in BLOCKS:
return "worldlet '%s': unknown terrain block %s" % [id, str(b)]
worldlet_id = id
worldlet = d
size = int(d.get("size", size))
resolution = int(d.get("resolution", resolution))
_blocks = d.get("terrain", [])
for b in _blocks:
match String(b.type):
"hills":
height_scale = float(b.get("height_scale", height_scale))
_detail_k = float(b.get("detail", _detail_k))
"rim":
play_radius = float(b.get("play_radius", play_radius))
rim_width = float(b.get("rim_width", rim_width))
mountain_height = float(b.get("mountain_height", mountain_height))
rim_warp = float(b.get("rim_warp", rim_warp))
wall_frac = float(b.get("wall_frac", wall_frac))
"sea_cliff":
sea_angle_deg = float(b.get("angle_deg", sea_angle_deg))
sea_half_width_deg = float(b.get("half_width_deg", sea_half_width_deg))
sea_fade_deg = float(b.get("fade_deg", sea_fade_deg))
sea_level = float(b.get("sea_level", sea_level))
cliff_top = float(b.get("cliff_top", cliff_top))
cliff_offset = float(b.get("cliff_offset", cliff_offset))
cliff_drop_width = float(b.get("drop_width", cliff_drop_width))
"lake":
var c: Array = b.get("center", [lake_center.x, lake_center.y])
lake_center = Vector2(float(c[0]), float(c[1]))
lake_radius = float(b.get("radius", lake_radius))
water_level = float(b.get("water_level", water_level))
snow_height = float(d.get("climate", {}).get("snowline", snow_height))
return ""
func _has_block(t: String) -> bool:
for b in _blocks:
if String(b.type) == t:
return true
return false
func _height_fn(x: float, z: float) -> float:
_ensure_noise()
var valley := _noise.get_noise_2d(x, z) * height_scale
valley += _detail.get_noise_2d(x, z) * 1.4
var d := _rim_d(x, z)
if _blocks.is_empty():
use_worldlet(worldlet_id)
var land := 0.0
for b in _blocks:
match String(b.type):
"hills":
land = _blk_hills(x, z)
"rim":
land = _blk_rim(x, z, land)
"sea_cliff":
land = _blk_sea_cliff(x, z, land)
"lake":
land = _blk_lake(x, z, land)
"ocean_edge":
land = _blk_ocean_edge(x, z, land)
return land
# Mountain rim.
var land := valley
## Rolling hills: the ground everything else is built on.
func _blk_hills(x: float, z: float) -> float:
var valley := _noise.get_noise_2d(x, z) * height_scale
valley += _detail.get_noise_2d(x, z) * _detail_k
_valley = valley
return valley
## The mountain rim: rises from play_radius, stepped into ledges.
func _blk_rim(x: float, z: float, land: float) -> float:
var valley := _valley
var d := _rim_d(x, z)
var t := smoothstep(play_radius, play_radius + rim_width, d)
if t > 0.0:
var ridge := 1.0 - absf(_ridge.get_noise_2d(x, z))
@@ -153,7 +260,12 @@ func _height_fn(x: float, z: float) -> float:
var stepped := floorf(m / band) * band + smoothstep(0.35, 0.65, fposmod(m, band) / band) * band
m = lerpf(m, stepped, 0.45 * t)
land = valley * (1.0 - t * 0.6) + m
return land
## The sea side of the rim: the valley eases to a cliff top, then drops sheer.
func _blk_sea_cliff(x: float, z: float, land: float) -> float:
var valley := _valley
var d := _rim_d(x, z)
# Sea side: the valley eases to a cliff top, then drops sheer to the seabed.
# The edge sits 1 m past the invisible wall at every angle, including the
# headlands where the wall moves outward. (With the edge fixed at _cliff_d(),
@@ -167,14 +279,17 @@ func _height_fn(x: float, z: float) -> float:
var seabed := sea_level - 7.0 - minf(maxf(d - edge, 0.0), 60.0) * 0.08 + _detail.get_noise_2d(x, z) * 2.0
coast = lerpf(coast, seabed, smoothstep(edge, edge + cliff_drop_width, d))
land = lerpf(land, coast, sw)
return land
# Lake basin: blend the ground down to a bed below the waterline.
## A lake basin: the ground blends down to a bed below the waterline.
func _blk_lake(x: float, z: float, land: float) -> float:
var ld := Vector2(x - lake_center.x, z - lake_center.y).length()
ld += _detail.get_noise_2d(x * 0.5, z * 0.5) * 5.0
land = lerpf(land, water_level - 3.2, smoothstep(lake_radius, lake_radius * 0.35, ld))
return lerpf(land, water_level - 3.2, smoothstep(lake_radius, lake_radius * 0.35, ld))
# Ocean all round the chunk edge, so the world is an island and the edge of
# the mesh is always hidden under deep water.
## Ocean all round the chunk edge, so the world is an island and the edge of
## the mesh is always hidden under deep water.
func _blk_ocean_edge(x: float, z: float, land: float) -> float:
var e := smoothstep(size * 0.5 - 28.0, size * 0.5 - 4.0, sqrt(x * x + z * z))
return lerpf(land, sea_level - 10.0 + _detail.get_noise_2d(x, z) * 2.0, e)

View File

@@ -895,6 +895,44 @@ func water_at(x: float, z: float) -> Dictionary:
return {"type": "sea", "surface": terrain.sea_level, "depth": terrain.sea_level - ground}
return {}
# --- the worldlet (docs/WORLDLETS_DESIGN.md: the WorldInfo API) --------------------
# What a system outside the terrain should ask about this place, rather than
# reading the terrain's numbers: which worldlet it is, its water, its climate,
# and its ways out. (W0 adds these; systems move over to them as each worldlet
# needs them to be different.)
## This worldlet's id ("valley") and its whole definition.
func worldlet_id() -> String:
return terrain.worldlet_id
func worldlet() -> Dictionary:
return terrain.worldlet
## Its bodies of water: [{"type", "surface", "center" (x, z; the sea's is a
## point on its coast), "radius" (a lake's; the sea's is 0)}].
func water_bodies() -> Array:
var out: Array = []
for b in terrain._blocks:
match String(b.type):
"lake":
out.append({"type": "lake", "surface": terrain.water_level,
"center": terrain.lake_center, "radius": terrain.lake_radius})
"sea_cliff":
out.append({"type": "sea", "surface": terrain.sea_level,
"center": terrain.coast_point(terrain.sea_angle_deg, 20.0), "radius": 0.0})
return out
## Its climate: {"snowline": m, ...} (more as worldlets differ).
func climate() -> Dictionary:
var c: Dictionary = terrain.worldlet.get("climate", {}).duplicate()
c["snowline"] = terrain.snow_height
return c
## Its ways on to other worldlets (the journey): [{"to", "by", "where"}].
## Planned in the definition; travel comes with J1.
func crossings() -> Array:
return terrain.worldlet.get("next", [])
# --- multiplayer -----------------------------------------------------------------
## Joined a server: start where it says, follow its clock, show the others.