Lumiland: How I Built a World of Light-Bears That Lives On Its Own in the Browser | Antonio Pedoto
Inside Lumiland: a 3D god-game in Godot 4 and WebGL2, local AI with no LLM, an infinite chunked world, and a self-balancing ecology. The technical story, with c
Field Detail --- --- Name Lumiland Genre God-game / 3D isometric life-sim (the legacy of late-'90s/2000s RTS games, with modern tech) Engine Godot 4.3 — GDScript Platform Web (WebAssembly + WebGL2); runs on mobile too URL https://antoniopedoto.it/lumiland/ Protagonists the Lumi — light-bears, recognizable by their distinctive "tuft of light" Technical core Headless simulation separated from rendering · JSON-serializable state · local, deterministic cognitive AI (no LLM) · infinite chunked world · self-balancing ecology with an S-curve Hook — a world that lives even when you're not watching
Close the browser tab. Go to sleep. The next morning you reopen Lumiland and find a note you never wrote: "While you were away, seven cubs were born in Valdoro. A patrol set off northward and founded Boscoluce. Old Honey, the first one you named, is gone: his children mourned him for two days."
It's not a cutscene. It's not a pre-baked block of text that fires on login. It actually happened — or rather, it happened in the sense that things happen inside a simulation: the world kept turning, calculated how many real hours had passed, aged the Lumi, paired off couples, migrated colonies, let the old ones die. And then it told you about it.
This is Lumiland's promise, and it's also its biggest technical challenge. A god-game where you're a deity who influences rather than commands needs creatures with a life of their own so robust they don't need you. They have to eat, fall in love, quarrel, learn, build, and die even when no one is watching. And all of this has to run inside a browser, in WebAssembly, on a phone, with no server thinking on their behalf.
How do you do it? The answer lies in a pile of counterintuitive decisions: cleanly separating the simulation from the graphics, giving up the fashionable language models in favor of a tiny, deterministic cognitive mind, generating an infinite planet 64 meters at a time, and — the most fun part — fighting for months against an ecology that swung between mass extinction and demographic explosion. Let's dive in. What Lumiland is, and why it's interesting
Lumiland is a god-game in 3D isometric view. If you grew up with the late-'90s management/RTS games — that genre where you watched a teeming world from above and could nudge it gently in a direction — you'll recognize the language immediately: an oblique camera, a little population doing its thing, and you in the role of a higher presence. The difference is that underneath there's no longer a period engine, but Godot 4 running in WebGL2.
The protagonists are the Lumi: light-bears with an emissive "tuft of light" on their heads, their trademark. Each one is an individual: it has a genome, a temperament, a mood, a memory, bonds with the others. They're born, grow, mate, have litters, found villages, invent things, age, and die. You are their god.
And here's the sentence that drove every design choice: the god influences, it does not command. You can select a group of Lumi and move them — even with your finger, on touch — but it isn't an absolute order: it's a nudge. They have free will. If you move a hungry Lumo away from food, maybe it listens for a moment and then goes back to looking for something to eat, because hunger matters more than your will. This changes everything. In a classic RTS the unit is an extension of your mouse; here it's an autonomous entity that, at most, goes along with you.
Why is this interesting technically? Because "autonomous creatures that live on their own" is exactly the kind of feature that, done badly, wrecks your performance, fills your code with coupled state, and makes it impossible to test anything. Lumiland is, in large part, the story of how not to get wrecked by that ambition. And it starts with a clean separation. The architecture — the simulation doesn't know graphics exist
The first decision, the one that made all the others possible, is trivial to state and disciplining to do: the simulation is completely separated from the rendering.
The project has clean folders. sim/ holds the world, the bears, the genetics, the society, the persistence: it's the model, and it knows nothing about cameras, meshes, or shaders. ai/ has the Director (the staging of events) and the Mind (the cognition of individuals). comm/ handles dialogue and voices. Then there are render/, core/, ui/, and tests/. The golden rule is just one: there is no game logic in the renderer. The renderer reads the simulation's state and draws it. Full stop. It doesn't decide who mates, it doesn't move anyone, it doesn't even know what "hunger" means: it only knows that Lumo number 42 is at position (x, z), has a certain mood, and should be drawn with a certain glowing tuft.
It sounds like architectural fussiness. It is, instead, the thing that holds everything else up, for three concrete reasons.
First: the state is JSON-serializable. Every object in the simulation knows how to turn itself into a dictionary (todict) and rebuild from one (fromdict). The entire world — bears, genomes, colonies, calendar, evolutionary era — is pure data. No scene nodes, no GPU references. This means saving a game is serializing a dictionary; loading it is the reverse. And it means that the famous "the world keeps going while you're away" doesn't require firing up the 3D engine: you work on the data.
Second: you can actually test. When logic doesn't depend on rendering, you can run it headless — no window, no GPU. Lumiland has a suite of GDScript tests that exercise the genetics, the simulation, navigation, persistence, the minds, evolution, and the colonies. They run the world forward for game-years and check invariants: does the population avoid extinction? Do saves come back identical after a JSON round-trip? Does the navmesh cover the expected area? All of this runs in seconds in CI, with no screen.
Third: the rendering still gets verified, but on its own terms. To check that the graphics aren't broken, no real GPU in CI: software screenshots with Xvfb + Mesa llvmpipe (a virtual X display and a CPU rasterizer). Ugly and slow, but enough to catch a visual regression — a missing mesh, a black material — without depending on graphics hardware.
The moral is that the separation isn't purism: it's what makes the simulation portable. The same logic you draw on a desktop with a graphics card can run in a headless test, in an offline catch-up, or inside a browser on a phone. It's always the same data model. Hold on to this idea — "the world is data, the graphics are a read" — because it comes back everywhere. Running a living 3D world in the browser — the limits and the tricks
Now the uncomfortable part. Running Godot 4 in the browser is possible, but the browser is no gaming PC: it's a WebAssembly sandbox that speaks WebGL2. And WebGL2, in Godot, means a specific renderer with rules all its own.
Lumiland uses two renderers depending on where it runs. On desktop, where there's a real GPU, project.godot selects Forward+, the rich pipeline with full PBR lighting. On web and mobile it switches to glcompatibility**, the GLES3/WebGL2 equivalent: the most portable target there is, the one that genuinely runs everywhere. Same simulation, same game, two different renderings.
The problem is that glcompatibility takes away things you'd grown fond of. The most painful: no glow / bloom post-process. On Forward+ the Lumi's "tuft of light" would be an emissive material that post-processing blooms into a soft halo. On compatibility that pass doesn't exist. And a light*-bear with no light is just a bear.
The trick is as old as real-time graphics: if you can't have real bloom, fake it with an additive billboard. Around the tuft there's a QuadMesh with additive blending, always turned toward the camera (bearview.gd literally calls it fake bloom, everywhere*). By adding its color to whatever's behind it, it gives the illusion of a glow that radiates out — on any GPU, even the weakest. It's a hand-drawn halo mimicking a post-processing effect we can't afford. It works because the eye doesn't want the physics of light: it wants to see something that glows.
The rest of the look is chosen to be pretty without costing too much: ACES tonemap, soft directional shadows, and a day-night cycle that, in one go, drives both the color and the angle of the sun and the sky. Nothing esoteric, everything sustainable in real time inside a browser tab.
There's another hidden trim. On glcompatibility, GPUParticles are not supported**. So rain and snow — which you want as particles, a thousand flakes coming down — can't run on the GPU. Solution: CPUParticles3D. The particles are computed by the CPU. It's an explicit compromise (fewer flakes, computed on the processor), but it's the only way to have particle weather on WebGL2. And since the weather is consistent with the graphics — the cloud cover dims the sun, the sky turns gray — even a few particles, over a sky that's darkened, sell the idea of rain perfectly.
Finally, the deploy, where the devil is in the details. You export to HTML5/WASM. The assets (the project's .pck, the engine's .wasm, the JS loader) are pre-compressed with gzip — Godot's .wasm is not light, and shipping it raw means seconds of waiting on first load. On the nginx side, two unintuitive precautions are needed: MIME application/wasm on the .wasm file, otherwise the browser won't stream-instantiate it. Cache-Control: no-cache. This one always surprises people. Godot exports with fixed filenames (index.wasm, index.pck…): there's no hash in the name. If you leave aggressive caching on, after an update users stay stuck on the old version because the browser reuses the cached file with the same name. With no-cache the browser always re-checks, and the update lands.
Three lines of configuration that, if wrong, make you believe you've published a new build while the whole world still sees yesterday's. The infinite world — chunk streaming, borderless fog, a navmesh that chases the camera
A god-game wants space. It wants you to move the camera past the hill and find more world, not an invisible wall or a "end of map" sign. Lumiland takes this desire literally: the world is infinite. And an infinite world can't be held entirely in memory — you have to generate it in pieces, around where you're looking, and throw away what you don't need.
The terrain is generated in chunks 64 meters on a side, derived from noise (FastNoiseLite). Around the camera a 7×7 grid of chunks is kept (radius 3). Every frame it checks which chunk the camera's focus has landed in: if it's the same as before, nothing happens; if it changed, the new chunks entering the radius are generated and the ones leaving it are unloaded. That's all — but it's that "that's all" that makes the world boundless at constant cost:
The key is the guard at the top: the heavy work (building the chunk meshes) happens only when the camera crosses a boundary, not every frame. For 63 meters of scrolling, nothing happens; on the 64th, a handful of chunks are born and as many die. And since every chunk is identified by a pair of integers (Vector2i), there's no privileged origin and no limit: you can travel forever in any direction and the system keeps indexing with integer coordinates that never run out.
The same philosophy governs the fog of war, and it's my favorite part. No map of fixed size: the fog is a dictionary of cells indexed by integer coordinates. You explore an area, that cell clears. There's no edge because there's no fixed-size array: there's only the set of cells you've uncovered, growing indefinitely as you walk. The minimap auto-adapts accordingly: instead of representing a fixed rectangle, it computes the bounds of what you've explored and resizes itself, growing with your kingdom. At first it's a postage stamp around the home colony; after a few game-years of exploration it's an atlas.
Water is solved with an elegant, zero-cost trick: instead of tessellating infinite oceans, it's one large plane that follows the camera. Wherever you look, the water is already down there, because it moves with you. You never generate it and never unload it: it's a single surface that travels with you.
And then there's navigation, which was the subtlest snag. The Lumi have to move over terrain that doesn't exist until you generate it. The "orthodox" solution would be to bake the NavigationMesh with Recast at runtime, as the chunks appear. The problem: runtime Recast baking is not reliable on all platforms — and "all platforms" includes browsers and phones, which are exactly where Lumiland has to run. Entrusting navigation to a component that sometimes, on certain devices, simply doesn't produce a mesh, means having Lumi frozen in place on some devices and not others. Unacceptable.
So: no Recast. The navmesh is built procedurally by hand — a grid of navigable polygons generated in code, deterministic and identical everywhere, that re-centers following the camera exactly the way the terrain does. I give up the precision of a bake that would fit the polygons to the exact shape of the terrain; in exchange I get navigation that works the same on desktop, browser, and mobile, with no device-specific surprises. In a project that lives in the browser, predictability is worth more than refinement. Giving the Lumi a mind — local cognitive AI, and why not an LLM
Now the core. What makes a Lumo "alive"? The temptation, in 2026, is obvious: hook every creature up to a language model, have it generate thoughts and one-liners, and you'll have characters that talk like people. Lumiland makes the opposite choice, and made it deliberately. No client-side LLM.
The reasons are concrete, not ideological. First, latency: a world with dozens of Lumi chatting in real time can't stop to wait for a network response on every line. Second, cost: every conversation would be a paid call, multiplied by every pair that meets, by every player, forever. Third, and decisive for a game that runs in the browser: exposed keys. Any secret you put in a WASM client is, effectively, public — anyone who opens the dev tools can read it. Putting an API key in Lumiland would mean giving it away. And running a language model inside WASM, with no external calls, is impractical in weight and performance.
So every Lumo has a local, deterministic cognitive mind. Small, readable, networkless. Four components, each with a precise role.
Temperament is the emotional baseline, and it comes from the genome. A sociable, brave, clever Lumo tends to start out more serene; a very gluttonous one is a touch moodier. It's a constant of the personality — who you are "by nature."
Mood is a valence between −1 and +1: how you feel right now. It's not static, it reacts to events through an appraisal model — the same idea from the psychology of emotion: an event is "appraised" and shifts the emotional state. Eating raises it; rain taken without shelter lowers it; the birth of a child is joy; the death of a loved one is grief, and it strikes relatives and friends; learning something does good; a quarrel hurts. Crucially: mood decays toward temperament. Emotions pass. After a bad day you slowly drift back toward your baseline self.
Did you notice the second effect of feel? Besides moving the mood, it leaves a trace in memory. That's the third component: every Lumo remembers its recent events (here it keeps five). This memory isn't there to fill a diary: it's there to make the Lumi talk about things that just happened. Whoever just had a cub has "birth" on their mind; whoever lost someone has "grief."
The fourth component is bonds — an opinion toward others, which strengthens or frays with every interaction and gets "pruned" to keep only the most intense relationships. A's opinion of B isn't just the personal bond: kinship is added in (a parent, a child, a partner carry weight) along with belonging to the same colony. From bonds that accumulate over time emerge close friendships and rivalries, with no one scripting them by hand.
The point not to miss is this: none of this is random, and none of it is scripted. It's a tiny dynamical system — four variables influencing each other — that produces believable behavior deterministically. The same seed produces the same story, which is exactly what you need to test a world and to restart it identically from a save. An LLM would give you variety at the cost of unpredictability, latency, and network dependence. This mind gives you coherence, speed, and zero cost — and it all runs inside the browser. What do we do with it? We make it talk. Emergent conversations — from the topic to the procedural voices
When two Lumi meet, something happens. But what they say isn't drawn at random from a list: the mind chooses a topic based on state, mood, and mutual opinion. The available topics span the spectrum of a social life: greeting, love, play, teaching, bickering, gossip, weather, philosophical reflection, birth, grief.
The choice is a cascade of priorities, readable as a decision tree:
Read it as a scene. If one just taught something to the other, they talk about that. If they're partners, they talk about love. If they can't stand each other (opinion below threshold), they bicker. If one has a recent birth in memory, they talk about it joyfully; if grief, they process it together. If it's raining, they complain about the weather. Otherwise, two acquaintances greet each other. The clever, serene minds, on top of that, every now and then indulge in a philosophical reflection on the world. The dialogue emerges from the intersection of who they are, how they feel, and what they think of each other — context-aware in the literal sense: it depends on context.
Once the topic is chosen, the text is produced by parametric Italian templates. Not fixed sentences: skeletons that get filled with the real data of the moment — the names of the two Lumi, the mood-word of the speaker ("radiant," "serene," "pensive," "restless," "downhearted"), and the correct relationship term ("their partner," "a child," "a dear friend," "a rival"). The same pair, on different days and in different moods, says different things. It's combinatorics, not neural generation, but for a light-bear it's more than enough to seem alive.
And the voices? Here Lumiland does something I love for its economy and character: it doesn't record audio, it synthesizes it. Every voice is a procedural melody — sine waves on a pentatonic scale, generated in an AudioStreamWAV on the fly. Pentatonic because whatever note you pick sounds consonant: impossible to play out of tune. The result is a warbling language, fit for creatures of light, never unpleasant. Above the individual voices grows an ambient choir/pad that swells with the population: the more Lumi there are, the more the world "sings." A full colony has its own sonic breath that a village of four does not — and you didn't have to record a single sample.
Visually, when two Lumi converse a clickable speech-bubble icon appears above them. Tap it and you read the transcript of what they're saying. It's the bridge between the silent simulation and you: the chatter exists as data (the chosen topic, the parameters, the audio), and the bubble makes it readable on demand, without cluttering the screen with text. Want to eavesdrop? You tap. Don't want to? You just see bears talking to each other, and that's perfectly fine. Genetics and evolution — genome, eras, buildings that grow
Beneath the personality lies biology. Every Lumo has a genome: a set of traits — curiosity, sociability, courage, intelligence, gluttony, size, color, and others. They're not cosmetic labels: they're levers that move behavior. Sociability raises the odds of mating and bonding; courage changes how one behaves; intelligence opens the door to philosophical conversations and influences learning; even gluttony shifts the temperament toward moodiness. Traits affect behavior, dialogue, longevity, and fertility.
Reproduction is a function, breed(), that mixes the parents' genomes to produce the child's — with mutations and recombination. And every so often, rarely, a "legend" is born: a Lumo with an off-the-charts combination of traits. They're the individuals that give a game a narrative backbone: the exceptional founder, the progenitor the colony will remember. None of this is hard-coded as a "character": it's the genome that, every now and then, pulls out an ace.
Above the biology of individuals sits the evolution of society, paced in eras: Awakening → First Huts → Village → Culture → Civilization of Light. They're not levels you pick: they're thresholds computed from accumulated progress and announced as milestones when you cross them. Seeing "Your people have entered the Village era" appear after a few game-years of organic growth gives exactly that god-game satisfaction of watching a civilization rise on its own.
And the eras aren't just labels: they change what the Lumi can do. Construction never stops. Houses and fields level up over time; each new era unlocks new buildings — a storehouse, a totem of light that grants "Faith." Free Lumi, without you telling them to, build, improve, cultivate. You come back after a few real days and the village of huts you left has become something else: better roofs, new fields, a totem that wasn't there before. Progress is visible as the years pass, and it's their progress. You, at most, gave a nudge in the right direction — because, once again, you influence, you don't command.
There's a hidden cliffhanger in all of this, though. A civilization that grows needs a population that grows. And growing a population of autonomous individuals, so that it neither dies nor explodes, turned out to be the thorniest problem of the entire project. Time to talk about it. The ecology that wasn't supposed to die — extinction versus explosion
Let me tell you the most interesting bug I ran into, because it's the kind of bug that teaches you how a living system actually works.
Early version: four founders, RNG randomized every game, and the Lumi reproducing when they meet. Sounds reasonable. You run ten games and get ten completely different stories — which, at first, even seems like a feature. Then you look closer and realize the variance isn't "flavor": it's out of control. Some games end in extinction; others in explosion to hundreds of Lumi. And the two failures have opposite causes, which is the most insidious thing: every time I pushed to fix one, I made the other worse.
Extinction arrived in two ways, both instructive. The first: the four founders were of similar age, so a cohort of peers died all at once — an entire generation winking out in the same span of time, and if the children weren't yet numerous enough, that was the end. The second, even crueler: a founder who starves to death chasing distant food. With just four individuals, losing one to a bad pathfinding decision could zero out reproduction — maybe it was the only male, or the only female. Four is a cruel number: it forgives no mistakes.
The explosion was the mirror-image problem: nudge fertility a little too high to avoid extinction, and suddenly the population has no brake. It grows exponentially until the browser starts to choke. Two balancing bugs, opposite, meeting in an equilibrium impossible to hit with a single knob.
The solution wasn't a fix: it was a set of combined forces that, put together, produce an S-curve — slow growth at first, rapid in the middle, flattening toward a ceiling. The S-curve is the shape real populations grow with, and getting it here took five ingredients.
First, litters. Every mating produces not one child but a clutch: 2 to 5 cubs from the same pair. This gives robustness from the few founders onward — a single successful mating actually repopulates, instead of adding one unit at a time that bad luck can erase.
Second, variable individual longevity. Here there's a small professional lesson: the variance in lifespan was documented in the code comments but had never actually been implemented. The Lumi died on too-synchronized schedules — that's why the cohorts winked out together. Giving each one a different longevity spreads the deaths over time: no more generations collapsing in lockstep. (Reminder for everyone: a comment describing a behavior is not that behavior. Trust the code, not the good intentions written above it.)
Third, cohesive colonies. Instead of letting the Lumi scatter across an infinite world — where two potential partners might never meet — they're made to gravitate around their settlement. Staying close, they meet constantly, and so they reproduce constantly. Social cohesion is, in effect, a demographic mechanism: love only works if people are in the same place.
Fourth and fifth, the two brakes and the two safety nets. A soft carrying capacity: as the ceiling approaches, births slow down on their own. And an anti-extinction push: when there are very few, fertility spikes because the species has to restart. And if one of the two sexes is missing outright — the condition you can't come back from — there's the last net: the immigration of a wandering Lumo, unrelated, who joins the colony. The code governing these two extremes is surprisingly small:
Look at fertility: below the rescue threshold it returns a high, blunt multiplier — restart, whatever the cost. Above it, it returns 1 − pop/ceiling with a floor of 0.05: the closer you get to carrying capacity, the closer fertility gets to zero, but it never quite vanishes. It's literally the slope that gives the S its shape: strong push at the bottom, rising brake at the top. litter_size doubles down: generous clutches when there are few, a single cub when at the ceiling. The two functions together produce an ecology that self-corrects from both extremes.
The result, verified over simulations of five game-years: no extinctions, no explosions, no lag. The population breathes — it rises, stabilizes, oscillates a little the way a living ecology does — but it doesn't run away or die. And here comes the note of technical honesty, because it matters: the population really does oscillate, it doesn't lock onto a number. That's not a bug, it's the nature of the system; but it's also why a soft ceiling exists around a hundred living Lumi (the SOFTCAP here is 95). That limit isn't arbitrary: beyond a hundred-odd creatures simultaneously alive and simulated*, fluidity in the browser starts to suffer. I chose an ecology that sings within a sustainable boundary, rather than a boundless world that bogs down the phone. It's a trade-off, and it's right to call it by its name. Independent colonies — migration, names, the identity of villages
A cohesive population solves demographics, but it creates another need: if everyone stays clustered around a single settlement, after a while it's crowded and the infinite world stays empty. Lumiland solves this by budding colonies.
Every settlement is a colony with a name of its own. The first, the home colony, is called Prima Luce (First Light). Then come Valdoro, Lumevia, Stellaria, Auroria, Fonteviva, Boscoluce… a repertoire of luminous names that give each village a nameplate, not a number. When a colony becomes too crowded, a patrol sets off toward a new site — and here's the detail that makes the difference between a migration and a death sentence: the patrol has a guaranteed male + female pair. A new colony founded by individuals of the same sex alone would be stillborn. By guaranteeing both sexes in the migrating group, every new village is born demographically viable. It's the lesson of the four founders applied every time it expands: never let the founding depend on luck.
Those born in a colony inherit its membership, and membership weighs on relationships — Lumi of the same colony start with a bit of extra mutual liking, because opinion includes "we're of the same people." So colonies aren't just dots on a map: they're social identities. They have a name, a history, members who recognize each other as fellow citizens.
And you see these identities in three places in the interface. On the minimap, where every colony has its label. In the individual Lumo's card, which lists "Colony of X" as if it were their hometown of record. And in the epic moment: at the birth of a new settlement a founding popup appears — "A people is born" — announcing the christening. Watching Prima Luce become five, six, seven villages scattered across a world that thins out from their frontier is, in the end, the god-game I wanted: a civilization expanding on its own, and you witnessing its geography grow. Time, weather, and UX — calendar, speed, popups, autosave, offline progress
A world that lives in time needs time to be readable. Lumiland keeps a real calendar — days, months (with their names), and years — so that events have a date and a story has a chronology. "It was born in the autumn of the third year" means something, because there's an autumn and there's a third year.
Above the calendar runs the day-night cycle, and above that the weather, consistent with the graphics — which is what makes it convincing. It's not a rain overlay pasted onto an unchanged sky: when bad weather arrives, the cloud cover dims the sun and turns the sky gray; with snow the sky shifts toward a cold white. Only then do the particles fall (the CPUParticles3D we mentioned). The lighting and the atmosphere change together, and the eye believes it because everything shifts in the same direction at once. The weather isn't an effect: it's a state of the world that touches light, sky, and particles in one go.
And because a living world at real speed can be slow to watch, there's a time-speed control: pause, 1×, 2×, 4×, 8×. Want to follow a conversation moment by moment? 1×. Want to watch a colony grow and bud over the course of a session? 8× and you watch the years scroll by. It's the remote a god-game must have: time is yours.
Then comes the chapter that, on the web, separates a prototype from a product: persistence. There's a local registration/profile for each player. And there's a robust autosave, and it's robust for a very practical, very web reason: on a browser, closing the tab doesn't always give the game time to save. You can't rely on the classic "save on exit," because the exit can be brutal. So it saves periodically and on focus loss — the moment you switch tabs or minimize the window, the world is put safe. Defense in depth against how people actually use a browser: in fits and starts, with twenty tabs open, closing without thinking about it.
And here we are back at the beginning — at the morning-after note. That "the world keeps going while you're away" is the offline catch-up. When you return, the game calculates how much real time has passed since the last save and advances the simulation accordingly. And it can do so without firing up the graphics, precisely thanks to the choice in Chapter 3: the simulation is data, it runs headless. While you were away there was no GPU drawing anything — there was only the model grinding through game-days. That recovery feeds a persistent diary/chronicle that tells you what happened: births, deaths, migrations, foundings. The morning note isn't a piece of narrative fiction pasted on top: it's the faithful transcript of a simulation that really ran without you.
The rest of the experience follows the same philosophy of tact. The game's messages are popups organized in tabs, so they don't bury you; the info panels are movable and dockable, you arrange them as you like. And the deity's gesture — selecting groups of Lumi and moving them, even with your fingers on touch — is there, it works, but it stays a suggestion. You influence more than you command. It's the same sentence from the first chapter, but now you know how much work goes underneath to make it true: it takes creatures whose lives are solid enough to be able, gently, to tell you no. Lessons learned, honest limits, and what comes next
Building Lumiland left me with a few convictions I'll carry into the next project.
Separation always pays. "The simulation doesn't know graphics exist" sounds like discipline for its own sake until you realize it's what makes headless tests, offline catch-up, and portability to browser and mobile possible. A strict architectural rule at the right moment buys freedom for months.
Sometimes the technologically "humble" choice is the right one. No client-side LLM; no runtime navmesh bake; fake bloom with an additive billboard instead of post-processing; synthesized voices in place of audio files. Each is a conscious renunciation of something more powerful or more refined in exchange for predictability, zero cost, and running on any device. In a game that has to run in the browser of some random phone, runs everywhere beats is impressive on one machine.
Living systems are balanced with several weak forces, not one strong knob. The ecology didn't stabilize when I found the magic number: it stabilized when five modest mechanisms — litters, variable longevity, cohesion, soft ceiling, anti-extinction net — started pushing each other toward an S-curve. And the bug of the "documented but never implemented" longevity is the most honest reminder of the craft: read the code, not the comments.
And the limits, said without dancing around them. There's no LLM at all: the conversations are rich and context-aware, but they remain combinatorial — a light-bear will never surprise you with a truly new thought, and that's fine for what it is. The population oscillates like a living ecology: don't expect a steady number, expect a breath. And there's a practical ceiling: around 100–120 living Lumi simulated at once, fluidity in the browser begins to drop, which is why the carrying capacity is what it is. The world is infinite; the amount of concurrent life inside it is not.
And after that? The foundations are designed to grow exactly in the spots where I had to stop. The eras end today at "Civilization of Light," but the evolution scale is open. Construction keeps leveling up: there's room for new buildings and new social roles as the eras lengthen. The Lumi's minds are four variables influencing each other — and that model can be enriched while staying deterministic and local, without betraying the anti-LLM choice that keeps everything light. And since the state is just JSON-serializable data, every future — more colonies, more history, more world — doesn't have to fight the architecture: it slides right into it.
For now, though, one thing is enough for me. I close the tab. I know that tomorrow morning, somewhere between Prima Luce and Boscoluce, someone will have been born, someone else will have passed away, and a little people of light will have gone on living — without me, and for that very reason truly alive.
---
Follow Me on Social Media
Stay up to date on my tech and gamedev projects: LinkedIn — Professional articles and behind-the-scenes Facebook — Community and updates
Try Lumiland in your browser: antoniopedoto.it/lumiland