Files
Emberwild/docs/WORLDLETS_DESIGN.md

361 lines
20 KiB
Markdown

# Worldlets design
Status: **W0 built**; the rest is planned. The journey between worldlets is
§10.
*As built in W0:*
- `data/worldlets/valley.json`: the Valley as data, its terrain as five
building blocks in order (`hills`, `rim`, `sea_cliff`, `lake`,
`ocean_edge`) with every number that used to be hard-coded.
- `terrain.gd` runs a worldlet's blocks (`use_worldlet()`); each block is
the old code's arithmetic, unchanged. `tests/test_worldlets.gd` checks
four seeds give exactly the same height grid as before (SHA-256 of the
grid, taken from the old code).
- The worldlet is chosen like the seed: a server's (it sends its real one),
else the save's `meta.json` "worldlet", else the Valley (worlds from
before W0). New worlds and servers record it.
- The WorldInfo API on the world: `worldlet_id()`, `water_bodies()`,
`climate()`, `crossings()` (the planned ways on), beside `water_at()`.
Systems still read the terrain's numbers directly; they move over to
these as each new worldlet needs them to differ (W1-W3).
## 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:
```json
{
"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:
1. **Terrain pipeline.** `terrain.gd` becomes a runner that stacks the
chosen building blocks into the height grid. The grid, collision,
`sample_height` and the boundary wall stay exactly as they are.
2. **A `WorldInfo` API** 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.
3. **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`.
4. **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.
5. **Saves.** `meta.json` gains `"worldlet": "valley"`. Older saves default
to Valley, so nothing breaks.
6. **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.json` under `"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
asks `water_at` what 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 of `terrain.water_level`.
- `data/fish.json` holds 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
1. **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.)
2. **Worldlet size:** keep ~320 m, or let some be bigger?
3. **Difficulty per worldlet:** should harsher worldlets (Highlands) bring
wolves sooner, or keep the same grace period everywhere?
4. **Which second worldlet first:** Cove (sea fishing, the beach you
mentioned) is my suggestion.
## 10. The journey: tunnelling out (proposal, Nov 2026)
A goal for the game, from Paul: once you have a good enough pick, you tunnel
through the mountain rim into the next, slightly harder worldlet, and now and
then go back to earlier ones for what only they have. This is option B
(linked worldlets) from §2, reached by digging rather than a path.
### The shape of it
- **A chain of worldlets**, each a little harder: Valley (today's island) →
Cove (sea, salt, mackerel) → Highlands (cold, deep snow, bigger packs) →
Marsh (bog iron, sickness, mists) → the Ember, the volcanic heart the game
is named for: the end of the journey.
- **The goal**, something to tell players on day 1: Old Tam talks of the
land beyond the mountains, where the fire in the earth still burns. Reach
the Ember and light the old beacon there. (Whether there's an ending or
it's open-ended is a question below.)
- **Gates:** each tunnel needs the best tool from the worldlet you're in.
The antler pickaxe (from the Old Stag) opens the Valley's tunnel; the
Highlands need a pick made from something found in the Cove, and so on.
Each worldlet has its own boss or trial, like the Old Stag.
### A different crossing each time (Paul, Nov 2026)
Players have to work out how to get from each worldlet to the next, and
it's a different way every time: tunnelling, climbing, burrowing, sailing.
Each crossing is that worldlet's puzzle, and what it needs comes from what
you've learned and gathered there:
| From → to | Crossing | Needs (a first idea) |
|---|---|---|
| Valley → Cove | **Tunnel** through the rim at a seam | the antler pickaxe, timber props, light |
| Cove → Highlands | **Sail** out of the bay and round the headland | a boat (planks, nails, a hide sail, rope), calm weather, food that keeps |
| Highlands → Marsh | **Climb** a frozen cliff | climbing picks and rope, warmth to last the climb (bear cloak, pemmican) |
| Marsh → Ember | **Burrow** under the old lava crust | a digging tool from bog iron, and a way through the heat and gas |
- **The hint, not the answer:** each worldlet shows its way out without
explaining it: a seam in the rock with old pick marks, a wrecked boat on
the beach, rope frayed on a cliff ledge. Old Tam (and his tips) nudge,
and the guide can keep it vague.
- **Coming back** uses the same crossing in reverse (the tunnel stays dug,
the boat stays moored, the rope stays fixed), so supply runs get easier
once you've made the way.
- **New systems this needs, one per crossing:** digging into a rim seam
(tunnel and burrow share most of it), a boat you build and steer on the
water, and climbing a marked face. Each is a phase of its own (J1 tunnel;
later the sail, the climb, the burrow).
### Tunnelling (the first crossing)
- **Where:** each worldlet's rim has one or two **seams**: cracked, darker
rock faces with a few loose stones at the foot, findable by exploring
(and Old Tam's tips). The rim stays a wall everywhere else.
- **How:** hit a seam with a good enough pick, many times. Each stretch dug
needs **timber props** (wood) or it falls back in, and it's dark in there:
candles, torch stands or a lantern. The tunnel is a short, built-for-it
cave (a few bends, maybe a side chamber with ore or a hazard), so it
stays hand-made rather than a hole through the terrain mesh, which the
heightmap can't do.
- **Through:** the far end opens onto the next worldlet. Walking out loads
it (a short fade, "The Cove"), with your bag, cloak and stats as they are.
### The first tunnel: down the sea cliff to the Cove
The Valley's sea side ends at a sheer cliff, its top about level with the
valley and the sea some 25 m below. The Cove is down there, so the first
tunnel goes **down**, inside the cliff, from the clifftop to the shore.
- **The seam:** a cracked outcrop on the clifftop strip near the edge (the
sea_cliff block marks where), with old pick marks and loose stones at its
foot. It can't be dug until you have the antler pickaxe (tier 3); a lesser
pick just chips it ("Too hard: you need a better pick").
- **Digging down:** the tunnel is about six stretches, each a few metres on
and a few down: a sloping passage that turns back on itself like a
stair, so the 25 m drop fits inside the cliff. A stretch takes many pick
blows (stamina, and wear on the pick) to open.
- **Props:** each new stretch needs a **timber prop** (2 wood) set at its
mouth before you can dig the next. Leave one out and the face won't take
another blow ("It's loose: prop it first"); nothing caves in on you, so
it's a cost, not a trap. That's about 12 wood all the way down.
- **Light:** the tunnel has no daylight at all. Every stretch is pitch dark
until you bring light: a torch in your hand works, but your hands are full
with the pick, so the way to do it is to set **candles and torch stands**
as you go (tallow candles from bear fat, torch stands from wood). Placed
lights stay, so the way is lit for the trip back and for friends.
- **The bottom:** the last stretch breaks out into a sea cave, its mouth
opening on the Cove's shingle beach. Walking out of the mouth is the
crossing: a fade, then the Cove, arriving at the cave's Cove end. The way
back starts there and climbs the same tunnel.
- **What's saved:** the tunnel belongs to the Valley's save: how far it's
dug, the props, the lights. On a server it's shared: anyone can dig on
from where someone stopped, and the lights are everyone's.
- **How it's built:** the tunnel is its own small space (a "between" scene
next to the worldlet), made from a kit of rock pieces (straight, turn,
slope down, the cave mouth) laid out from the seed, with box-shaped
colliders. The heightmap can't have holes or overhangs, so the passage is
never cut into the terrain itself: you walk into the seam and you're in
the tunnel scene.
### Why go back
- **Each worldlet has things the others lack.** Clay and salt only in the
Valley and Cove; little wood in the Highlands; no bears outside the
Valley (so bear fat for candles and pemmican is a supply run); a fish or
herb only one place has. Food that keeps (pemmican, salted and smoked
meat, clay jars) becomes what you carry between them.
- **Your bases stay** where you built them: each worldlet keeps its own
save file, so your Valley hut, chests and jars are waiting.
- **Time:** a worldlet you're not in stands still (as a server does with
nobody on), so a base you left doesn't starve or get raided.
### Building it (phases)
| Phase | Delivers | Done when |
|---|---|---|
| **W0-W2** | the worldlet groundwork above (terrain pipeline, worldlet in the save, several water bodies) | as §8 |
| **J1** | seams in the rim, digging with props and light, a tunnel scene, travel between two worldlets in single-player, per-worldlet save files | dig through from the Valley, walk to the Cove, walk back to your hut |
| **W3** | the Cove (§5) as the second worldlet | its suites pass, and its tunnel back works |
| **J2** | gates and supply needs: tool tiers per tunnel, resources only some worldlets have | a test bot can't open the second tunnel without the Cove's pick |
| **J3** | multiplayer travel (see below) | two players in different worldlets on one server, or the rule we choose |
| **J4** | Highlands, Marsh, the Ember and its ending | the journey can be finished |
### Multiplayer: the hard part
A server today runs one island. Options:
- **Everyone together:** travel needs every player at the tunnel (like the
sleep rule). Simplest; one worldlet runs at a time.
- **Spread out:** the server runs each worldlet that has someone in it.
The most freedom, and the most work and CPU.
- **Party leader:** whoever goes through brings the others with a vote.
### Decisions (Paul, Nov 2026)
- **Extendable, not finished:** the Ember has a last boss, and after it you
keep playing. The chain is built so more worldlets (and crossings) can be
added after the Ember later: each worldlet names the next, nothing assumes
the Ember is the end.
- **The crossings:** tunnel, sail, climb, burrow, in that order.
- **Harder by biome, not just by numbers:** each worldlet has its own,
stronger animals and its own hazards. The Marsh, for one: mosquitoes whose
bites can make you sick, and bog, mist and the like. Others in the same
spirit per biome (to design with each worldlet).
- **Multiplayer: spread out.** Each player goes where they like; the server
runs every worldlet someone is in, and the rest stand still. Built in J3,
after single-player travel works. What that means for the code:
- one server process holds several worldlet scenes, each with its own
clock, weather, creatures and save file;
- every player belongs to one worldlet at a time, and the server only
sends them that worldlet's world, creatures, sounds and chat-free events
(chat and the player list stay server-wide);
- a crossing moves a player from one worldlet to another (save their
place, load or wake the other worldlet, send them its world);
- sleep: "everyone in bed" counts the players in that worldlet only;
- the Old Stag and other bosses scale with the players in their worldlet.
- **Boats: one each, no passengers.** A boat belongs to whoever built it
(like a bed): only the owner, and their friends under the server's locks
rule, can sail it, and nobody else can dismantle it. It stays moored where
it was last left (the Cove end or the Highlands end), so nobody is
stranded by someone else taking the only boat. Lose or wreck it and you
build another, which is harder where wood is scarce (the Highlands), so
plan the trip back. Each player has to earn their own crossing by boat.
- **The other crossings are shared:** a dug tunnel, a fixed climbing rope
and a dug burrow are changes to the world, there for everyone once one
player has made them (like a door or a bridge).
### Server cost, measured (0.3.1)
One worldlet, a dedicated server on the M5 Pro, never pausing: about
**12% of one CPU core and 270 MB of memory**, with nobody on. Players add
creatures, weather and pickups around them. So with *spread out*, the worst
case of 8 players in all 5 worldlets is about five of today's servers in
one process: roughly 1.4 GB and a little over half a core before players,
and worldlets with nobody in them stand still and cost next to nothing.
Comfortable on the home Mac; a small cloud server (2 GB) would be tight.
### (Answered) questions
1. **The ending:** a real ending at the Ember (light the beacon, a last
boss, the credits) or open-ended (the journey is the goal, keep living)?
2. **The crossings:** do the four above fit (tunnel, sail, climb, burrow),
and in that order? Should a worldlet have more than one way out (a hard
one and a long one)? And for the tunnel: only at marked seams, or
anywhere in the rim (more freedom, harder to make good tunnels)?
3. **Multiplayer:** everyone together, spread out, or a party vote?
4. **How much harder** each worldlet: stronger animals and weather only, or
new rules too (thirst in the Ember, sickness in the Marsh)?
5. **Order:** Cove second (sea fishing, gentle) as planned, or straight
into the Highlands for a harder step?