From Open World to Torii Portals, and Nine Maps with One Town Each
We built a seamless continent and switched it off nine hours later, turned a hand sketch into a 2.6 x 3 km map and shrank it by two thirds, then rebuilt every other map to match.
Where we started #
The world began small and data-driven. A map is a JSON file in data/maps/: zones (safe sanctuaries and
hunting grounds with level ranges), NPC placements, props, a seeded random scatter, and portals. The first
Sanctuary Vale was 480 x 480 m: a town in the middle, then rings of hunting grounds getting harder toward
the edge, and a portal north to the Shadow Marsh. All maps load into one scene tree, 5000 m apart, so
they never see each other.
Zones were circles and rectangles at first. On day two they got a polygon shape, for one reason that
the design doc states plainly:
Zones are circles, rectangles or polygons (any outline, from the user's map sketches: the user draws, Claude translates into points; 1 sketch square = 25 m).
The human designs maps the way people have always designed maps: on a grid, with a pencil. Converting the sketch into coordinates is the AI's job.
The open-world detour #
On day three the plans got bigger. Seven more themed maps were drafted (Orc Valley, Duskmoor, Sunscar Desert, Frostpeak, Ember Wastes, and the realms Hellmouth Rift and the Shadowlands), each with its own sky, fog, weather, liquids and border mountains. A Python generator drew a world map from a layout spec, and the idea came up to make it seamless. The backlog entry describes it as "regions that build onto each other (like Where Winds Meet), joined by land bridges at passes".
We built it in an afternoon, in three commits:
- Joined world: the Vale and the Marsh meet at a land bridge called Willow Ford. Each map got an organic coastline (a seeded wobbly shape plus land bridges), a sandy rim and sea around it. Walls follow the coast but stay open where a neighbour's land continues. Walking off one map moves you onto the next in place, and sky, fog and light fade over three seconds.
- Fixes: you walk along the shore, joined maps wake together, and you can fight across the border. Before that, Marsh monsters across the ford "stood frozen like a wall", because only the player's own map was awake.
- The open world: all regions joined in a chain, each with named landmarks ("sights") that announce themselves with a banner when you arrive. Among them: the Old Mill, the Sand Ship, the Mammoth Graveyard, the Gate of Teeth.
Terrain arrived in between: rolling hills from seeded noise, baked into a 2.5 m height grid. The grid is flattened toward coasts, rivers, safe zones, portals and land bridges, so joined maps meet at the same height.
It worked. The commit message even notes the frame rate: "~140 fps again with the Vale and Marsh both awake".
Switching it off #
Nine hours after the first joined map, this commit landed:
Back to separate maps linked by torii portals (open world switched off): purple portals along the chain, a blue one from the Vale to Orc Valley
It was the human's call. CLAUDE.md records it with a date: "Open world is OFF (2026-10-09, the user's
call): every map is its own place, linked by torii portals". The code wasn't deleted. data/world.json
keeps an empty "continent", and the crossing, coastline and neighbour-sight code stays dormant for later.
Looking back at what the open world needed, the trade is easy to see. Every map had to fit into its neighbours' coastlines and match their heights at the border. Maps had to wake together, load together, and share visibility across the border.1 Peers saw entities on a neighbour within 90 m. Every map's size and shape was constrained by the chain. With portals, each map is its own place. It can be as big as it needs to be, in any shape, and the next map only loads when you step through a gate.
The switch itself was small (11 files, more lines removed than added), mostly because the earlier rules
held. Actors never cared which map they were on; World.place() was already the only way to move between
maps; and portals already existed for dungeons. Fittingly, the portals became torii gates: a swirl in the
portal's colour inside a gate model from the building generator.
Map 1 from a sketch #
With the size constraint gone, the human drew Map 1 by hand: a walled town with four gates, a coast to the west, a river coming down from the northern mountains, lakes, roads to farmsteads, a harbour village, a mountain border, and two portals in the far corners. Thirty-two minutes after the portal commit, this landed:
Map 1 rebuilt from the user's sketch: 2.6 x 3 km Sanctuary Vale with a walled town and four gates, coast, river, lakes, mountain border, roads, farmsteads, harbor, new landmarks, torii portals to the Marsh and Orc Valley
The diff has 11,156 added lines, almost all of them generated map data.
The generator #
The map isn't hand-written JSON. It comes from tools/mapgen/maps/vale.py, a rerunnable script that
writes data/maps/sanctuary_vale.json, and the JSON says so in its _doc: "Generated by
tools/mapgen/maps/vale.py (edit that, not this file)." The sketch becomes a few named constants and
functions:
TOWN_R = 230.0
NORTH, SOUTH, EAST = -1420.0, 1420.0, 900.0
BLUE_PORTAL = (600.0, -1150.0)
PURPLE_PORTAL = (560.0, 1110.0)
HARBOR = (-980.0, 150.0)
CRYPT = (-150.0, -1050.0)
def west_coast(z):
"""x of the west coast at z (wavy, a cape mid-way, an inlet where the river ends)."""
x = -1130 + 95 * math.sin(z / 380 + 1.0) + 45 * math.sin(z / 140 + 0.4)
x -= 120 * math.exp(-((z - 80) / 160) ** 2) # a cape around the harbor
x += 160 * math.exp(-((z + 1050) / 120) ** 2) # the river mouth bay
return x
From there the script lays out everything else: the coastline polygon, three rows of mountains along the north, east and south edges (with a gap where the river comes through), the river Silverrun and three lakes, a town wall in 12 m segments around a 230 m radius with four gatehouses, roads from the gates to the farmsteads, the harbour and both portals, and the hunting grounds in rings:
hunting("whispering_fields", {"circle": [0, 0, 650]}, [1, 4], 30, 220),
hunting("shadow_woods", {"circle": [0, 0, 1150]}, [5, 9], 45, 400),
hunting("ashen_reach", {"rect": [-1500, -1500, 2500, 3000]}, [9, 14], 35, 300),
Levels 1-4 in the fields, 5-9 in the woods, 9-14 out to the mountains, with the Shadows and packs counted per zone. The scatter is scaled to the area: 15,000 pines and 50,000 ferns in the Shadow Woods alone. The script also keeps the old town square with all its NPCs, school halls and the forge, and it updates the Marsh and Orc Valley portals so you arrive next to the matching torii.
Because the generator is code, iterating on the map means changing a constant and rerunning it. "Move the Old Mill", "make the woods denser" or "three times the packs" are each a one-line change and a fresh JSON.
Checking the result: overviews and the atlas #
How do you check that a 2.6 x 3 km map matches a sketch? You draw it. Every generator run also writes a
top-down overview (tools/mapgen/overview.py): zones with their level ranges, roads, water, landmarks and
portals, all drawn from the JSON the game will load. The overview is what the human compared with the
sketch, and what every later map change was reviewed with.
On day three there was also a world atlas (tools/mapgen/atlas.py) that asked Godot, headless, to export
every map's real geometry and drew all the maps side by side. Its README says why it works that way:
"Rerun after changing maps; it can't drift." It drifted anyway, in a different sense: the atlas expects the
coastlines of the joined world, and after the rework below it no longer runs (a map without a coastline
gives it nothing to draw). The per-map overviews took its place, and fixing the atlas is on the list.
New problems at 2.6 km #
A map more than 30 times the area of the old one broke assumptions everywhere, and the next commits deal with them:
- Grass streams around the player instead of being built for the whole map.
- Peers only receive entities within 160 m, not everything on the map.
- Terrain cells grow with the map size, and heights become exact along roads and coasts.
- Mountains block at their foot, and nothing grows or spawns on their slopes.
- Monsters spawn only near players. A map this size has more than a thousand monsters.
That last point became its own performance story (the next post).
Too big, too empty #
Then the human played it. Their notes from that morning: "monster density or frequency is quite low, I also feel like the map is a bit too big." A big map is only impressive for the first walk across it. After that it's the time between fights.
Because the map is a generator, shrinking it was a change of constants, not a redesign. Twelve hours after the sketch became a map, the Vale shrank to about 1.4 x 1.8 km. The town stayed exactly as it was; the fields, woods and outer ring moved closer. Packs now stand about every 35 m, Shadows are twice as dense, roaming packs move more often, and pack spawns stay out of lakes. The proximity spawn radius dropped from 250 m to 200 m to match.
Every map at Vale scale, one town each #
The other eight maps were still the old small ones, mostly 300 x 300 m with two or three safe zones each. The human's brief for fixing that was one line: "Rework all other maps we have. Keep safe zones to one per map + only other safe zones for portals."
That rule turned out to shape the whole game. With one town per map, the town is where you go back to, so it has to be worth going back to. Everything else is hunting ground, and the portal shrines are small islands of safety you pass through. The lore later gave it a reason: every town is built around a shard of a broken sacred stone, and monsters can't cross its song.
The rework was one of the first big parallel jobs: four agents, two maps each, each writing a rerunnable
generator modelled on the Vale's (tools/mapgen/maps/<map>.py). Each map came out at roughly 1.2 to 1.5 km
across, with mountain borders, roads, packs about 35 m apart, eight to ten named landmarks,2 The desert later got thirteen. and waystones
at the town, the portals and the landmarks. The Vale's own eleven landmarks were rebuilt as proper set
pieces with 17 new building models in the same commit. Because four generators wrote portals
independently, a small script (tools/mapgen/link_portals.py) runs afterwards and points every portal's
arrival at the partner map's matching shrine.3 The coordinator post covers what went wrong with four
agents in one repository.
Exceptions, on purpose #
Two later decisions bent the rule, both from the human:
- The desert got bigger and emptier. "The desert I feel like should be larger and feel somewhat empty. What are rewards we can have there to still make it valuable?" Sunscar is now about 2 x 2 km, with islands of life between empty dunes and about 30% fewer packs. The emptiness pays: elite dune wanderers with better loot, a Sandstorm event with storm elementals and treasure caches, rare spawns (a sand wyrm that burrows, a raider warlord), lore tablets, a caravan merchant who keeps a wall-clock schedule, and Sunstone, the material the top three upgrade steps need.4 A proposed digging-with-a-shovel feature was dropped: "not so much of a fan of the shovel thing".
- Some maps lost their town. "Some maps don't have to have a town." The Ember Wastes, the Hellmouth Rift and the Shadowlands now keep only a gate camp inside the portal shrine, and their old houses stand empty in the wilds. It reads right: these are the places where the land itself is the enemy.
Getting around nine big maps #
Bigger maps with one town each need fast travel, or every quest turns into a walk. There are two layers:
- Waystones: stone steles at the town, the portals and the landmarks. Walking up to one attunes it (its rune lights up for good); at any attuned stone you can travel to any other you've attuned, for a fee that grows with the target map's level. The server checks every trip like any other request.
- Gate Keepers: the human asked for "a teleport NPC per larger safe zone that allows me to travel to places I've been before and talked to the other teleport NPC". They're NPCs on the same waystone network, so talking to one attunes it.
Each map plays a little differently #
The last layer, added on day six, is a small set of map mechanics (data in data/mechanics.json, run
on the server): the desert's heat (stand in shade or water, or carry a water skin), Frostpeak's cold
(campfires and shelter warm you; avalanches come down marked slopes after a warning), the Ember Wastes'
shifting lava and diving drakes, demon portals in the Hellmouth Rift that you close by channelling,
rituals in Duskmoor, and the Shadowlands' shade, which builds up away from shard light. The rule, in the
data file's own words: "Keep it modest: flavour and reward, not frustration."
Takeaways #
- Separate places are simpler than a seamless world. The open world worked, but it tied every map to its neighbours. Portals give each map its freedom back. Keep the seamless code dormant if you might want it later.
- Let the human draw and the machine measure. A sketch at 1 square = 25 m plus a generator script is a surprisingly direct path from paper to a playable map.
- Big is not the same as good. Density of things to do matters more than square kilometres. A generator makes "smaller and denser" a change of constants.
- One strong rule shapes a lot. "One town per map" decided where NPCs live, what the lore says, where waystones go and which maps feel safe.
- Render what you generated, and keep the renderer running. A tool that checks your maps is only useful while it still runs against today's maps.
What's next #
A map with tens of thousands of plants and hundreds of animated monsters doesn't run well by default, and later a GPU heap ran out on a map that had rendered fine. Next: performance.