Egor Urvanov

Architecture of “Zhitiyo”: C4 and critical paths

C4 diagrams down to components and four critical paths as sequences: startup, a simulation tick, a player action and a frame.

Zhitiyo is a browser life sim: a neighbourhood of ten households, needs, careers, relationships, Buy and Build modes. No build step and no server: a page, a set of ES modules and three.js from a CDN. Below: C4 diagrams down to components and the four paths everything rests on.

C4: context and containers

Level 1. The system and its surroundings

Playermouse, keyboardZhitiyoa browser gamejsDelivr CDNthree.js 0.169localStoragesave, settingsplaysloads the librarywrites and reads
There is no server of its own: any static host will do.

Level 2. Containers

main.jswiring, loop, autosaveBrainjs/simWorldjs/worldCorebus, statelocalStoragezhitie.saveUIjs/uiAudiojs/audioDatadata/Renderjs/renderCDNthree.jsModelsassets/*.glbtickloadStartLotsave / loadframeframebus, statebussoundcatalogueimport maploadManifest
main.js creates the bus and the state, wires the contexts and runs the loop. Brain, World and UI read the catalogue from data/.

C4 level 3: components of each context

Contexts talk through the event bus and one state object. Every fact has one owner: the Brain runs time and needs, the World knows tiles and walls, the Render draws the picture.

Brain

sim/index.jstick, enqueueautonomy.jsaction choicewalk.jssteps along a pathqueue.jspriority queueactions.jswalk, enter, loop, exitmotives.jsneeds, moodSubsystemscareer, social, familyhazards, npc, hoodWorldfindPath, useSpotsthinkpushItemfirst in queuestartActionstepWalkmotiveTickHOOKS, EVENTSpath
The Brain knows neither DOM nor three.js: it runs in Node, which is how the tests work.
  • Autonomy scores nearby objects by need curves and divides the gain by distance.
  • A player command queues above an autonomous one and pre-empts it.
  • Subsystems attach to actions through step-end hooks; the loop core does not know them.

World

world/index.jspublic APIobjects, wallsfloors, roofcache.jsderived gridsrooms.jsroomAt, roomScorenav.jsA* pathfindingspots.jsuse spotshood.jsneighbourhood, lots, move-inhousegen, furnishcommunitylot.jsloadStartLotdata/templates, catalogueeditsinvalidateroomswalkabilityfindPathgoalsloadLot, moveInbuilds housesstarttemplates
The World keeps the lot in the state and answers: can it be placed, where to walk, which room is this.
  • Walkability and room grids are computed once and reset when the lot is edited.
  • The active lot lives in state.lot; the others are kept as snapshots inside the neighbourhood.

Render

render/index.jsinitRender, framecamera.js4 rotations, 3 zoomsenv.jslight, sky, shadowsassets, manifestglb modelslot.jswalls and floorsroof, fireghostsobjects.jsfurnituresims.jsresidents, animationshood.jsneighbourhood viewrigupdateloadManifestrebuildupdatesyncsyncshowHood
The Render only reads the state and listens to the bus; it changes nothing in it.
  • Objects and residents resync on bus events; walls also by a hash check twice a second.
  • A model not found: a procedural stand-in is drawn.
  • Shaders are pre-warmed at start to avoid hitches later.

UI and audio

ui/index.jsinput, modes, looppanel.jsneeds, timepie.jspie menuqueue, bubblesqueue, bubblescamera.jsmouse, keysbuy.js, build.jsBuy, Buildhood.js, cas.jsneighbourhood, familydialogs, eventsquestions, eventsaudio/index.jssound and musicupdateopenupdateupdateenter, clickopenupdatesfx, frame
The UI gets the world, brain and render from main.js as one object and calls them directly.
  • Audio starts on the first player gesture: the browser allows nothing earlier.
  • Volumes and settings are kept in localStorage.

Four critical paths

1. Startup and world load

main.jsStorageBrainWorldRenderUIload() if ?continuesave or nothingcreateState()seed, addSim ×2loadStartLotspawn pointcreateHoodawait initRendermodels, shader warm-uprender readyinitUIrequestAnimationFrame
A new game seeds the RNG, places the start house and two residents; with a save it skips that.

2. Simulation tick

main.jssim.tickautonomyactionsWorldBustick(dt)speed × minutes; all asleep: speed-up0.5 min sub-stepsthink if no actionpushItem to queuestartActionfindPathpathsim:anim, object:changedneeds every 2 minmoney:changed, notify
Several sub-steps run per frame: needs tick rarely, actions and walking every sub-step.

3. Player action and its effect

PlayerUIRenderBrainBusclick an objectpick(x, y)object, idinteractionsForitems and refusal reasonspie menupick an itemenqueuequeue above autonomyobject:changed, sim:animsync, animation
The tick then runs the action: walk, enter, loop, exit; the Render reacts to events.

4. A frame: render and audio

main.jssim.tickUIAudioRenderStoragetick(dt) in Live modeframe(dt)frame(dt)loops: fire, smoke alarmframe(dt)sync, wall cutaway, rooflight by game clockdraw the framesave() every 30 s
One requestAnimationFrame drives everything: simulation, interface, picture, audio and autosave.

The rules everything rests on

  • One state. Everything worth saving lives in one object that turns into JSON as a whole.
  • The event bus links contexts. An error in one listener does not stop the others.
  • Brain and World without DOM and three.js. They are tested in Node with no browser.
  • The Render decides nothing. It draws what is in the state.
  • Player and autonomy share one queue. A player order always wins.

Weak spots

Where What is wrong
Hooks in one file All action-step handlers sit in one 450-line sim/index.js.
UI knows the Render Family creation takes outfit lists straight from the render manifest file.
World via a global The Brain gets the world as an argument and also keeps it in a module-wide variable.
Save version A save of another version is silently dropped; there are no migrations.
Load weight three.js comes from an external CDN and the glb model set loads at start.

About the game

Game pageZhitiyoA life simulation in the spirit of The Sims 1: an isometric low-poly neighbourhood of ten households
© 2026 Egor Urvanov