The last post covered the cyberdeck's gameplay loop: contracts, mission graphs, RPS combat, chain storylines, three-path endgame. With those bones in place I spent the next few days fleshing out the world around them. The OPERATOR isn't just running missions anymore; they're moving through a city that has its own clock, its own moods, and a memory longer than theirs. Here's what shipped.
STREET grew a city map
The STREET tab used to be a flat list of every shop the OPERATOR had discovered. You clicked a number, paid a flat 50€$ cab fare, and walked into the catalogue. It did the job, but it felt like a vending machine. All the locations existed in some indeterminate space and any one was as close as any other.
Night City has six districts now. Watson is industrial east. Pacifica is the derelict south where the anarchists run the radio. City Center is corporate plaza, surveillance everywhere. Heywood is mixed residential, Westbrook is nightlife and money, Santo Domingo is the foundries. Each shop is anchored to a specific district, and traveling between districts is the thing that costs you. Once you're in a district, walking to its locations is free.
The apartment view (idle state) shows your discovered districts:
DISTRICTS
1. Watson calm 50€$ · industrial east side, dockworkers and tech junkies
2. Pacifica tense 60€$ · derelict south, anarchist cells run the radio ★ active beat
3. Heywood calm 50€$ · mixed residential, cred and family loyalty matter
4. Santo Domingo tense 60€$ · industrial belt, foundries and old machine shops
Each district has a mood scalar in [-1.0, 1.0] that drifts in response to faction events, megacorp falls, and the OPERATOR's own actions. Mood translates to a tier (hostile / tense / calm / festive) and the tier feeds two things you actually care about: cab fare and ambush probability.
CAB_FARE_MULT = {'hostile': 1.5, 'tense': 1.2, 'calm': 1.0, 'festive': 0.9}
AMBUSH_PROB_MULT = {'hostile': 1.5, 'tense': 1.2, 'calm': 1.0, 'festive': 0.7}
A hostile district is a 50% surcharge on cab fare and 50% more likely to produce a corp ambush at high heat. A festive district is the opposite. The math is small, a bumped-up fare to Pacifica is 75€$ instead of 50€$, but the texture is real, and crucially the player can see it before they commit to the trip.
Mission narrative beats can now anchor to a specific district. A node in the mission graph just declares "district": "pacifica" and the engine refuses to engage it from anywhere else:
No narrative beats available in this district.
Anchored beats also surface a ★ active beat badge on the district row in the cab list, so the OPERATOR knows where they need to go. This was the change I sat on the longest before doing it. The earlier flat-shop-list version was fine, but it was missing place. Missions all felt like they happened "around here, somewhere," and at no point did a player ever cross town to do something. Now they do, and the pacing is different. You read the brief, you see "Pacifica," you grab a cab. The cab fare feels like crossing a threshold instead of paying a tax.
The world runs without you
The other major shift is that the city now does things on its own.
There's a CalendarClock that tracks hour-of-day in fictional time. Most shops have hours; if you cab to a daemon dealer at 3 AM the door is locked and the screen tells you when they reopen. Trusted-tier vendors are the exception; they'll meet you at the back door because you're not a stranger anymore:
You walk up to Bayside Market. closed - opens 09:00.
or, if the OPERATOR has trust:
Bit pulls the side door open. "You should be sleeping. Come on in."
Fixers have schedules too. DM Hammer at 4 AM and you'll get a curt one-liner about calling back during waking hours. The schedule is per-fixer and the prompt addendum just informs the LLM about the time-of-day context; the model handles the tone naturally.
District moods drift. A successful mission for a street-aligned fixer in Pacifica nudges Pacifica's mood up. A megacorp falling drops the mood across districts where they had heavy presence. The drift drivers are explicit functions: faction events, contract resolutions, calendar anchors, and a cap clamps the scalar so a runaway driver can't pin a district at extreme mood forever.
The BBS reacts too. Posts are now generated against the world state at the time of generation. A burst of activity in Pacifica produces an uptick of Pacifica-flavored posts. A corp falling produces an uptick of corp-fall posts on every relevant board for the next cycle. The state-aware generation reuses the existing post-generator, we just hand it more context.
def _build_post_context(self) -> Dict:
return {
'district_moods': {d['district_id']: d['tier']
for d in self.district_registry.list_districts()},
'fallen_megacorps': [c['corp_id']
for c in self.megacorp_registry.list_fallen()],
'recent_chain_completions': self.contract_resolver.recent_chains(),
'rival_state': self.rival_roster.summary(),
}
Pulling this together: the city ticks even when the OPERATOR is asleep. You quit, come back the next day, and the moods have drifted, the BBS has new chatter, the fixer who DMed you yesterday at 11 PM has gone quiet because their hours rolled over.
Fixers reach out first
The DM tab used to be one-directional: the OPERATOR sends a message, the fixer responds. Nobody would ever reach out to the OPERATOR first.
Now they do. The proactive-contact system sweeps the fixer roster on a low-frequency cadence and, for fixers in a sufficiently warm trust tier, occasionally has them send the OPERATOR an unprompted DM. The DM is generated against the relationship state: what jobs the OPERATOR ran for them recently, how it went, whether there's a directed contract waiting in the inbox. A frequency floor of five real minutes per fixer keeps the inbox from feeling like a notification torrent.
FREQUENCY_FLOOR_SEC = 300.0 # one DM per fixer per 5 real minutes, max
The BBS tab gets a [1 new DM] pip when a fixer sends something unprompted. The OPERATOR sees who texted before opening the conversation, which mirrors how it should feel: someone reaches out, you decide if you have time. Hammer texting at 2 AM with "you up?" hits very differently than the OPERATOR cold-DMing Hammer at the same hour.
I'd been resistant to this for a while because it sounded annoying. The thing that made it work is the trust gate. A first-tier fixer the OPERATOR has barely interacted with will not DM them out of the blue. It takes shared history. By the time you're getting unprompted texts from a fixer, the relationship has earned that intimacy.
Old enemies come back
The other lateral cut at "world that runs without you" is the vendetta system. The OPERATOR makes enemies. Specifically: high-trust fixers get rivals; failing the wrong contract or burning the wrong handshake earns you a recurring antagonist.
Each faction has a vendetta template: a roster of named rivals with their own backstory, escalation tags, and behavior hooks. When the OPERATOR triggers a faction-level vendetta, the system rolls one up off the template and stamps it with the OPERATOR's specific offense. From then on, when an ambush triggers (heat-driven, in STREET), there's a 40% chance the ambusher is the rival rather than a generic goon. Each appearance escalates: more HP, more damage, sharper dialogue. The dialogue references the original offense.
Hawthorne steps out of the alley. He's not surprised to see you.
"I got burned after you dumped me from that Arasaka subnet. Time to return the favor."
Killing the rival ends the arc. Letting them flee escalates the next encounter.
Time, retuned
The last thing I want to mention is a rare negative change: I deleted a system.
The original cyberdeck had a FICTIONAL_TIME_SCALE of 12, meaning one real minute equals twelve fictional minutes. Cooldowns and intervals were authored in fictional hours. A 24-fictional-hour cooldown was two real hours. A "delivery in three hours" was fifteen real minutes.
It was a clever knob. It also turned out to be impossible to think about. Every time I authored a cooldown or a calendar event I had to do the conversion in my head, and every time the player encountered one they had no way to know what scale was at play. "Comes back in 2 hours": real or fictional? A scale-of-12 universe doesn't tell you.
I dropped it. Fictional time is now real time + 60 years; just a year shift on the timestamp, no rate scaling. Every cooldown got retuned in real-world minutes:
HEAT_DECAY_INTERVAL_SEC = 150 # was 1800 (fictional)
HEALTH_REGEN_INTERVAL_SEC = 60 # was 3600 (fictional)
COOLDOWN_SEC = 90 * 60 # core ability: 90 real minutes
The fictional date stamps still feel right. A BBS post from 2086 is still a BBS post from 2086, and the cooldowns are now numbers you can predict. "Heat decays every 2.5 minutes" lands. "Heat decays every half a fictional hour at 12x scale" doesn't.
The lesson generalized. The fiction is a design tool, not a clock. Years can shift; the game's tempo should run at the player's tempo.
What's next
A lot of the systems above are wired but underutilized. The district mood graph is doing the right things mathematically but only a handful of drivers are pushing it; the BBS reacts to state but only on a few axes; the proactive contact pool only carries a few fixer voices. The next stretch is content. More vendetta templates, more state-aware BBS post types, more proactive-contact moments per fixer. The framework is the part that fights you. Pouring content into a framework that already works is the part that's actually fun.
I also want to do one more pass on schemas, content validation, and the first-run tutorial (the polish pass) and then I'm going to stop adding things and start playing it. The whole project has been "build the next system" mode for a while. Everything is wired together well on paper, but now it's time for some real playtesting.