8.3 KiB
Worldlets design
Status: proposal. Nothing here is built yet, apart from the first piece
of groundwork (world.water_at() and data/fish.json, see §6).
1. What a worldlet is
A worldlet is a small, complete world type: a ~320 m playable area with its own shape, water, climate, wildlife and features, generated from a seed like today's island. The current map becomes the first worldlet, "Valley": a mountain ring, a lake and a sea cliff. Others can differ in kind, not just in seed:
| Worldlet | Shape | Water | Twist |
|---|---|---|---|
| Valley (today) | mountain ring + sea cliff | lake | wolves' den in the rim |
| Cove | low hills down to a sandy bay | sea + harbour, a stream | sea fishing, a pier, tides later |
| Highlands | plateau, cliffs, a tarn | small cold lake | snow most days, harsher nights |
| Marsh | flat, reedy, pools | many shallow pools | fog, insects, bog iron later |
| Tutorial | small (~160 m), calm, fixed seed | one pond | guided steps, a fenced rabbit meadow, named spots (docs/TUTORIAL_DESIGN.md) |
A worldlet is data plus a few generator building blocks, not a new scene with its own code. That keeps the game's systems shared (building, survival, creatures, saving) and makes worldlets addable by addons later (§7).
2. The big decision: one worldlet per save, or linked worldlets?
A. One worldlet per save. New World asks for a name, a seed and a worldlet type. Simple, and it fits everything already built.
B. Linked worldlets in one save. Several worldlets form one world, and you travel between them (a mountain pass, a cave tunnel, a boat). Each keeps its own state. More exciting, but much more work, especially in M2: a server would have to simulate several places at once, or keep all players together.
Recommendation: build A first, with a save layout that allows B later.
Each worldlet's state goes in its own file from day one
(worlds/<id>/worldlets/<worldlet>.json), and the player's own state moves
to worlds/<id>/player.json. Then B is "add travel and a second file",
not "migrate every save".
3. Definition format
data/worldlets/<id>.json. Every number that's an @export on
terrain.gd today moves here, plus the lists that are hard-coded:
{
"id": "cove",
"name": "Cove",
"description": "Low hills running down to a sheltered bay.",
"size": 320,
"terrain": {
"base": {"height_scale": 7.0, "octaves": 4},
"boundary": [
{"type": "mountains", "from_deg": 60, "to_deg": 300, "height": 50},
{"type": "beach", "from_deg": 300, "to_deg": 60, "slope": 0.08}
],
"features": [
{"type": "cove", "angle_deg": 0, "width": 60},
{"type": "stream", "from": "mountains", "to": "cove"}
]
},
"water": {"sea_level": -1.5, "lakes": []},
"climate": {"cold_day_chance": 0.1, "snowline": 45, "day_length": 1200},
"wildlife": {
"budget": {"rabbit": 5, "deer": 3, "boar": 2},
"wolves": {"den": "rim", "den_day": 3, "hunt_day": 5}
},
"places": [{"type": "pier", "at": "cove"}],
"spawn": {"near": "cove", "facing": "sea"}
}
Generator building blocks (GDScript, shared by all worldlets): base hills, mountain rim, sea cliff, beach slope, cove/bay, lake basin, stream bed, plateau. A worldlet only picks and parameterises them, so it can't do anything a block can't.
4. What changes in the code
Most game code already asks the terrain generic questions:
sample_height, sample_normal, inside_boundary and find_spawn. The
work is in the places that know about this island's single lake and
cliff. Today:
| Assumption | Used in | Becomes |
|---|---|---|
one lake at water_level |
player (swimming), creature, spawner, den, build_system, props, lake_fish, world | world.water_at(x, z) (done for fishing) |
lake_center / lake_radius |
lake_fish, world (views) | the water bodies list |
sea_angle_deg, coast_point |
props (sea stacks, cliff-top strip), world (views) | the boundary segment list |
snow_height |
props, spawner, weather | climate.snowline |
play_radius |
props, den | boundary queries |
The steps:
- Terrain pipeline.
terrain.gdbecomes a runner that stacks the chosen building blocks into the height grid. The grid, collision,sample_heightand the boundary wall stay exactly as they are. - A
WorldInfoAPI on the world node, for everything outside terrain:water_at,water_bodies(),boundary_at(angle),features(type),climate. No system reads terrain exports directly any more. - Water bodies. Each lake, pond and the sea is a body with a surface,
shape and type. The water mesh is built per body. Swimming, the "too
deep" check, animal steering and fishing all go through
water_at. - Props and places by feature. Scenery reads densities from the definition. The den, the sea stacks and (in Cove) the pier are "places" positioned by features, not hard-coded angles.
- Saves.
meta.jsongains"worldlet": "valley". Older saves default to Valley, so nothing breaks. - New World screen. Pick a worldlet type (with a short description and a preview render), then name and seed as now.
5. The Cove and sea fishing
- Beach boundary. Sand slopes gently into shallow water. You can wade and swim out until the water is deep, and then a current carries you back (the beach equivalent of the invisible wall, with a toast).
- Harbour. A sheltered bay with a generated wooden pier. The pier is built from the building pieces, so it's "placed by the world" and can't be dismantled.
- Sea fish in
data/fish.jsonunder"sea": mackerel (common), cod (bigger, deeper water), and maybe crab pots later. Casting from the pier reaches deep water. The fishing code needs no changes: it already askswater_atwhat it landed in and looks up that water's fish. - Later: tides (sea level shifting slowly), a raft or rowing boat, and driftwood on the beach.
6. Done already (groundwork)
world.water_at(x, z)returns{type, surface, depth}for the lake and the sea, and fishing uses it instead ofterrain.water_level.data/fish.jsonholds the fish per water type, bite timings and dawn/dusk weights.ItemDB.define_water()registers a water's fish, so an addon or a worldlet can add "sea" without code.
7. How it fits the roadmap
- Before M2 (recommended): steps 1-6 for Valley alone, with no visible change, then the Cove. Doing the refactor before multiplayer means M2 replicates "worldlet id + seed + edits", not one hard-coded island.
- M2: the server loads one worldlet per save (option A). Clients regenerate it from id + seed, which is the same determinism we rely on now.
- M3 (addons):
define_worldlet()takes the same JSON. Addons can only combine building blocks, so a worldlet addon can't run arbitrary code in the generator. - Option B (linked worldlets) fits after M2, once we know how the server handles players in different places.
8. Phasing and tests
| Phase | Delivers | Done when |
|---|---|---|
| W0 | terrain pipeline + WorldInfo API; Valley defined in JSON | seed 1337 generates the same height grid as today (a hash test), and all 9 suites still pass |
| W1 | worldlet picked on New World and saved in meta; old saves = Valley | load an old save and a new Valley save; both play |
| W2 | water bodies (several per world), beach boundary block, swim-back current | a test worldlet with two ponds and a beach passes the boundary and terrain-edge tests |
| W3 | Cove worldlet: bay, stream, pier place, sea fish | you catch a mackerel off the pier |
| W4 | (optional) Highlands / Marsh | each passes the shared suites |
| W5 | (after M2, optional) linked worldlets and travel | walk through a pass from Valley into Cove and back |
Every existing suite runs per worldlet (boundary, terrain edges, survival, building, wildlife, fishing), so a new worldlet can't ship with a hole in its edge or animals that spawn in the sea.
9. Questions for you
- A or B? One worldlet per save to start, or do you want travel between them early? (Recommendation: A, with the save layout ready for B.)
- Worldlet size: keep ~320 m, or let some be bigger?
- Difficulty per worldlet: should harsher worldlets (Highlands) bring wolves sooner, or keep the same grace period everywhere?
- Which second worldlet first: Cove (sea fishing, the beach you mentioned) is my suggestion.