Compare commits
2
Commits
2fd6c5e854
...
e449cba288
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e449cba288 | ||
|
|
a420032594 |
+239
@@ -0,0 +1,239 @@
|
|||||||
|
# Djupet — Lore Bible
|
||||||
|
|
||||||
|
No such document existed before this one — everything below is reconstructed
|
||||||
|
from what's actually in `src/events.ts`, `src/upgrades.ts`, `src/characters.ts`,
|
||||||
|
and `src/map.ts`, plus a few threads that were seeded in event text and never
|
||||||
|
paid off. This file is the canon going forward: new events, upgrades,
|
||||||
|
characters, and endings should agree with it rather than contradict it. Where
|
||||||
|
something below is marked **(open thread)**, it's a hook that exists in
|
||||||
|
shipped content but hasn't been resolved yet — a deliberate invitation, not
|
||||||
|
an oversight.
|
||||||
|
|
||||||
|
## Premise, one line
|
||||||
|
|
||||||
|
A Malmö cult meets in a Rosengård basement under cover of a Nordic
|
||||||
|
crime-fiction reading circle, working toward completing a ritual tied to
|
||||||
|
something asleep beneath Öresund — and the game is about managing how much
|
||||||
|
of that anyone else gets to notice.
|
||||||
|
|
||||||
|
## Tone
|
||||||
|
|
||||||
|
Three registers, deliberately blended, and every new piece of content should
|
||||||
|
know which one (or which mix) it's writing in:
|
||||||
|
|
||||||
|
- **Anders Fager** — vardagssvenska possession-by-mundane-detail. Skatteverket,
|
||||||
|
ICA, SJ, Pressbyrån, Systembolaget. The horror arrives disguised as a
|
||||||
|
Tuesday.
|
||||||
|
- **Kult** — reality-as-prison, the flesh under the surface of things. Rooms
|
||||||
|
that aren't on the blueprints, walls that are warm, doors that stop
|
||||||
|
existing.
|
||||||
|
- **Call of Cthulhu** — investigators, sanity, the Mythos with a Skåne accent.
|
||||||
|
Things half-glimpsed, things half-translated, things that are worse once
|
||||||
|
understood than before.
|
||||||
|
|
||||||
|
The house style is **deadpan understatement**. Nobody screams. Things are
|
||||||
|
noted, dryly, and then the cult moves on. A sentence structure that recurs
|
||||||
|
constantly in existing text: state something uncanny in one short sentence,
|
||||||
|
then immediately undercut or normalize it in the next ("X. Ingen pratar om
|
||||||
|
det." / "X. Det är Malmö."). New content should reach for that rhythm before
|
||||||
|
reaching for anything more explicit.
|
||||||
|
|
||||||
|
## The entity: Den Sovande
|
||||||
|
|
||||||
|
**Never given a proper name beyond the epithet** ("The Sleeping One") — this
|
||||||
|
is deliberate and should stay deliberate. It sleeps in or beneath Öresund.
|
||||||
|
The ritual's endpoint is not clearly "waking" it so much as *completing* it —
|
||||||
|
event and upgrade text consistently frames the goal as finishing something
|
||||||
|
unfinished, not rousing something dormant. "Ofärdig" (unfinished) is the word
|
||||||
|
used in the abandon-game log line, and it's the right word to keep reaching
|
||||||
|
for.
|
||||||
|
|
||||||
|
Established physical/architectural connections:
|
||||||
|
- **Turning Torso** is not a metaphor for a twisting spine — per
|
||||||
|
`torso_beacon`'s upgrade text, that's literal: "Det är korrekt. Det är inte
|
||||||
|
en metafor." The building has always been listening. The architect is
|
||||||
|
fictionalized as **Calatrava-Santiago**.
|
||||||
|
- **Värnhemstorget** — a chalk circle that regrows overnight no matter how
|
||||||
|
often the city washes it off. Doesn't advance the ritual as directly as
|
||||||
|
Torso, but every visit and every varnhem-gated event ties dread/ritual
|
||||||
|
together here.
|
||||||
|
- **Öresund itself** — water level anomalies, a "sunken city" glimpsed on the
|
||||||
|
bottom in the deepest kaos-tier event (`k_oresund_rises`) that is
|
||||||
|
explicitly **not** an old city — the buildings are modern, one has
|
||||||
|
lighting. This is the single most unresolved, most promising lore thread
|
||||||
|
in the game **(open thread)**: what is the sunken city, and is it Den
|
||||||
|
Sovande's, a reflection of Malmö itself, or something arriving rather than
|
||||||
|
waking?
|
||||||
|
|
||||||
|
## The artifact: Skriften (The Scripture)
|
||||||
|
|
||||||
|
Found immured behind 1923 tile in a wall on Davidshallsgatan. Properties
|
||||||
|
established across several events and the `the_manuscript` upgrade:
|
||||||
|
- Self-writing — new lines appear overnight, "inte på något känt språk...
|
||||||
|
inte på något okänt språk heller egentligen."
|
||||||
|
- Moves if you look away (`torso_windows`' cousin-text; also implied by the
|
||||||
|
manuscript upgrade's "Texten rör sig om man tittar bort").
|
||||||
|
- Was translated by a linguistics grad student who wasn't trying to (she was
|
||||||
|
chasing "ett ovanligt baltiskt dialektdrag" for unrelated academic work) —
|
||||||
|
she later asks to join the cult (`k_researcher_joins`) specifically because
|
||||||
|
she "just wants to see how it ends." She is the **Doktoranden** character.
|
||||||
|
- The last page wrote itself the night the grand ritual prep upgrade
|
||||||
|
triggers, and per that upgrade's text, it's "ett recept" — a recipe. This
|
||||||
|
is worth remembering: the endgame isn't a spell in the abstract, it's
|
||||||
|
*instructions*, plural steps, something with a procedure. Good hook for
|
||||||
|
more mid-ritual content (see Depth ideas below).
|
||||||
|
|
||||||
|
## Place index
|
||||||
|
|
||||||
|
Real Malmö geography throughout — never invent a fictional location when a
|
||||||
|
real one will do; that's load-bearing for the tone.
|
||||||
|
|
||||||
|
**Core ritual sites** (area-gated, upgrade-unlocked):
|
||||||
|
- **Rosengård** — the founding basement. Nominal cover: "en läsecirkel för
|
||||||
|
nordisk kriminallitteratur." Nine chairs; one is always a little too warm.
|
||||||
|
- **Värnhemstorget** — the chalk circle.
|
||||||
|
- **Turning Torso** — the beacon/altar.
|
||||||
|
|
||||||
|
**Free-roam locations** (visitable from turn one, currently flavor-only, no
|
||||||
|
upgrade or event-chain ties of their own — see Depth ideas):
|
||||||
|
- **Öresundsbron** — the bridge to Denmark; the Danes conspicuously don't
|
||||||
|
notice anything, which several events flag as more unsettling than if they
|
||||||
|
did.
|
||||||
|
- **Ribersborgsstranden** — kallbadhus, and ducks that face Turning Torso in
|
||||||
|
an "almost worshipful" state. Recurring motif, used at least three times
|
||||||
|
across events/kaos (`ducks_torso`, `k_duck_army`, location flavor text).
|
||||||
|
The ducks know more than the cult does — never contradict that.
|
||||||
|
**(open thread — the ducks have never been explained even in-joke terms)**
|
||||||
|
- **Davidshallsgatan** — where Skriften was found. A neighbor sings something
|
||||||
|
"almost, but not quite, a psalm" through the wall.
|
||||||
|
- **Stortorget** — the old town square; clocks around it rarely agree.
|
||||||
|
- **Möllevångstorget ("Möllan")** — market, fifty kitchens' worth of smell at
|
||||||
|
once, three speakers never playing quite the same song. Added later
|
||||||
|
(v0.6.0) alongside Universitet and still has no upgrade/NPC tie of its own.
|
||||||
|
- **Malmö universitet (Niagara building)** — library, lecture halls, a room
|
||||||
|
for rarely-requested collections that always has whatever the cult's code
|
||||||
|
needs. This is where the Doktoranden's translation happened.
|
||||||
|
|
||||||
|
**Flavor-only mentions** (referenced in event text, never visitable, don't
|
||||||
|
need to become visitable — they're texture, not real estate): Malmö C,
|
||||||
|
Kockumskranen, Folkets Park, Triangeln, Limhamns kalkbrott, Hyllie,
|
||||||
|
Augustenborg, Lund Observatory/university, Västra Hamnen, S:t Johannes
|
||||||
|
kyrka, Gustav Adolfs Torg.
|
||||||
|
|
||||||
|
## Character index
|
||||||
|
|
||||||
|
**Founders** (always known):
|
||||||
|
- **Broder Lindqvist** — found the basement. Dreams of the sea every night,
|
||||||
|
increasingly literally as the ritual progresses. Draws the same map,
|
||||||
|
always the same angle toward Öresund. Sleeping less and less; says he no
|
||||||
|
longer needs to. His 3-step event chain (`choice_lindqvist_dream` →
|
||||||
|
`lindqvist_chain_2a/2b` → `lindqvist_chain_3`) ends with him walking into
|
||||||
|
Öresund at night and coming back changed, dry, no longer talking about
|
||||||
|
dreams — regardless of which branch (listened/silenced) the player took to
|
||||||
|
get there. **The two branches currently only reroute flavor text on the
|
||||||
|
way to the same outcome — see branching-endings work below.**
|
||||||
|
- **Syster Hassan** — handles paperwork, writes 47-page replies to
|
||||||
|
Skatteverket and wins. Practical and unshakeable until she starts
|
||||||
|
proposing changes to the ritual's order with a confidence nobody recalls
|
||||||
|
her having before. Her 3-step chain (`hassan_ambition` → `hassan_chain_2a/2b`
|
||||||
|
→ `hassan_chain_3`) ends with her either performing a solo ritual at
|
||||||
|
Turning Torso (empowered branch) or being told no (sidelined branch) — same
|
||||||
|
structural shape as Lindqvist's arc, same current problem: doesn't touch
|
||||||
|
the ending.
|
||||||
|
|
||||||
|
**Met along the way** (unlocked by specific events, portraits exist for all):
|
||||||
|
- **Göran** — added to the cult's Discord by mistake, mistook the chat for a
|
||||||
|
board game about the sea, gave "constructive" feedback anyway. Later
|
||||||
|
promoted to "Väktare av det Tredje Sigillet" (Guardian of the Third Seal),
|
||||||
|
which he interprets as treasurer. Roughly correct.
|
||||||
|
- **Bengt-Åke från Husie** — buys a nameless newspaper from Pressbyrån daily.
|
||||||
|
Turns out to already belong to a rival sect (active since 1987), whose own
|
||||||
|
lineage goes back to the 1840s. The coordination meeting between the two
|
||||||
|
groups "went well" — **(open thread — this rival-but-friendly lineage has
|
||||||
|
never been used again; ripe for a recurring faction rather than a one-off
|
||||||
|
joke)**.
|
||||||
|
- **Rolf från Limhamn** — attended for three weeks, then stopped. Shoes found
|
||||||
|
outside, neatly placed, toes toward the sea. A non-smoker's tobacco left
|
||||||
|
beside them. Never resolved, never explained, never brought back — this
|
||||||
|
reads as intentionally left open rather than abandoned; treat him as
|
||||||
|
"taken," not "written out."
|
||||||
|
- **Systern Anneli-Karin** — talks in her sleep in an unrecognized language;
|
||||||
|
played backward, still incomprehensible, "just differently incomprehensible."
|
||||||
|
- **Den gamle sjömannen** — retired sailor, unidentifiable tattoos, locked
|
||||||
|
himself in the Turning Torso tower and sang; rescue services couldn't get
|
||||||
|
in; the singing genuinely helped the ritual.
|
||||||
|
- **Doktoranden** — the linguistics grad student who unwittingly translated
|
||||||
|
Skriften, later asked to join specifically to see how it ends.
|
||||||
|
- **Journalisten från Sydsvenskan** — wrote an award-winning puff piece about
|
||||||
|
"Malmö's growing alternative spirituality" (the jury can't recall why they
|
||||||
|
gave it the prize); came back later for a follow-up, found more than she
|
||||||
|
was looking for; the follow-up was never published; her notes were kept by
|
||||||
|
the cult instead.
|
||||||
|
|
||||||
|
**Named but never made full characters** — usable as-is for new event text,
|
||||||
|
or promotable to full characters if a future content pass wants them:
|
||||||
|
**Kommissarie Annika Fors** (a Grova Brott detective who dream-attends
|
||||||
|
meetings — the cult has set out a chair for her that she sits in without
|
||||||
|
knowing it; this is the single clearest seeded-antagonist thread in the game
|
||||||
|
and currently dead-ends after one event, `coc_investigator_dream`
|
||||||
|
**(open thread)**), the unnamed **Hässleholm rival cult** (tried to summon a
|
||||||
|
different entity, summoned IKEA customer service instead — currently played
|
||||||
|
as a pure joke in `k_rival_cult`, but Bengt-Åke's backstory proves the game
|
||||||
|
already has appetite for rival-sect lore played straight).
|
||||||
|
|
||||||
|
## Recurring motifs — reuse these before inventing new ones
|
||||||
|
|
||||||
|
- **Ducks at Ribersborg**, facing Turning Torso.
|
||||||
|
- **The number 13** — 13-minute intervals, 13-meter circles, 13 chimes at
|
||||||
|
midnight, a train departing every 13 minutes forever.
|
||||||
|
- **Circles and rings** — the Värnhemstorget chalk circle, fish swimming in
|
||||||
|
a shrinking circle, pigeons flying an unclosing circle.
|
||||||
|
- **Doubling / mirroring** — a second illusory Turning Torso, a mirror
|
||||||
|
reflecting a different-era version of the same room, two sets of measurements
|
||||||
|
that never quite agree with each other.
|
||||||
|
- **A chorus/choir**, heard underground or underwater, never seen.
|
||||||
|
- **Time glitches** — missing hours nobody misses (which is what's
|
||||||
|
unsettling about it, not the hour itself), trains stopped in tunnels that
|
||||||
|
aren't on any map, clocks agreeing with each other but not with reality.
|
||||||
|
- **Bureaucracy as a delivery mechanism for dread** — Skatteverket,
|
||||||
|
Länsstyrelsen, a rådman, a fastighetsmäklare, the Statistiska centralbyrån
|
||||||
|
survey call that gets uncomfortably specific. The mundane administrative
|
||||||
|
state is *already* a little bit compromised, and needs surprisingly little
|
||||||
|
push to become an active participant. `inflytande`'s whole design premise
|
||||||
|
hangs off this — favors flow both ways.
|
||||||
|
|
||||||
|
## The number nine
|
||||||
|
|
||||||
|
Nine chairs from the very first log line ("Nio stolar. En av dem är alltid
|
||||||
|
lite för varm."). The mechanical troende cap was removed in v0.3.0, but the
|
||||||
|
*flavor* still treats nine as symbolically complete
|
||||||
|
(`nine_chairs_full`, `troende_schism`'s "Nio troende är fler än vi någonsin
|
||||||
|
haft"). Do not resolve this tension by explaining it away — the correct move
|
||||||
|
is to keep nine as the meaningful, "designed" number, and treat anything
|
||||||
|
past it as the cult outgrowing its own architecture. The warm chair is
|
||||||
|
occupied only once flavor-confirmed (`nine_chairs_full`: "Ingen pratar om vem
|
||||||
|
som sitter i den"), and nobody has ever been told who's in it. **Keep it
|
||||||
|
that way.**
|
||||||
|
|
||||||
|
## What is deliberately never explained
|
||||||
|
|
||||||
|
A short list, because knowing what to leave alone matters as much as knowing
|
||||||
|
what to build out:
|
||||||
|
- Who or what sits in the ninth (warm) chair.
|
||||||
|
- What happened to Rolf.
|
||||||
|
- What the sunken city at the bottom of Öresund actually is.
|
||||||
|
- Why the ducks know.
|
||||||
|
- Den Sovande's actual name, shape, or intentions beyond "unfinished."
|
||||||
|
|
||||||
|
## Using this document
|
||||||
|
|
||||||
|
New events, upgrades, and characters should:
|
||||||
|
1. Pick one of the three registers (or a blend) deliberately, the way the
|
||||||
|
existing source comments already tag event blocks by register.
|
||||||
|
2. Reuse a real Malmö place before inventing a new one, and reuse a
|
||||||
|
recurring motif before inventing a new image.
|
||||||
|
3. Prefer resolving or advancing an **(open thread)** listed above over
|
||||||
|
starting an entirely new dangling one — the game already has more setup
|
||||||
|
than payoff.
|
||||||
|
4. Keep the deadpan two-sentence rhythm: state the uncanny thing, undercut
|
||||||
|
or normalize it immediately after.
|
||||||
+13
-1
@@ -1,4 +1,4 @@
|
|||||||
export const VERSION = '0.7.1';
|
export const VERSION = '0.8.0';
|
||||||
|
|
||||||
export interface ChangelogEntry {
|
export interface ChangelogEntry {
|
||||||
version: string;
|
version: string;
|
||||||
@@ -7,6 +7,18 @@ export interface ChangelogEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CHANGELOG: ChangelogEntry[] = [
|
export const CHANGELOG: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
version: '0.8.0',
|
||||||
|
date: '2026-08-08',
|
||||||
|
changes: [
|
||||||
|
'Nytt: LORE-BIBLE.md dokumenterar allt som redan var etablerat i spelet, inklusive trådar som aldrig knutits ihop',
|
||||||
|
'Inflytande har äntligen fler användningsområden: ett nätverk av tjänster, en repeterbar "kalla in en tjänst"-uppgradering, och ett sätt att neutralisera Kommissarie Fors utredning permanent',
|
||||||
|
'Fler än nio troende har nu en egen konsekvens: källaren blir för trång, och en ny uppgradering flyttar er till en större lokal',
|
||||||
|
'Möllevångstorget och Malmö universitet har fått egna händelser och en uppgradering, istället för att bara vara besöksbara utan vidare koppling',
|
||||||
|
'Fyra nya kaos-händelser i ett tyngre register — inte allt i kaos-poolen behöver vara ett skämt',
|
||||||
|
'Segrar och nederlag har nu olika slutord beroende på om Lindqvist lyssnade, Hassan fick ansvar, eller utredningen neutraliserades — tidigare påverkade de valen aldrig själva slutet',
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
version: '0.7.1',
|
version: '0.7.1',
|
||||||
date: '2026-08-03',
|
date: '2026-08-03',
|
||||||
|
|||||||
@@ -189,6 +189,83 @@ describe('exert_influence', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('favor_network / call_in_favor', () => {
|
||||||
|
it('call_in_favor is unavailable until favor_network is bought, then restores hemlighet with rising cost per use', () => {
|
||||||
|
let s = createState();
|
||||||
|
s.troende = 3;
|
||||||
|
s.inflytande = 1000;
|
||||||
|
s.hemlighet = 50;
|
||||||
|
|
||||||
|
const blocked = buyUpgrade(s, 'call_in_favor');
|
||||||
|
expect(blocked.hemlighet).toBe(50); // no favor_network yet — refused
|
||||||
|
|
||||||
|
s = buyUpgrade(s, 'favor_network');
|
||||||
|
expect(s.upgrades.has('favor_network')).toBe(true);
|
||||||
|
|
||||||
|
const callInFavor = UPGRADES.find(u => u.id === 'call_in_favor')!;
|
||||||
|
let lastCost = 0;
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
s.hemlighet = 50; // reset below the cap each round so the restore is actually observable
|
||||||
|
const cost = callInFavor.cost(s).inflytande!;
|
||||||
|
expect(cost).toBeGreaterThan(lastCost);
|
||||||
|
lastCost = cost;
|
||||||
|
s = buyUpgrade(s, 'call_in_favor');
|
||||||
|
expect(s.hemlighet).toBeGreaterThan(50);
|
||||||
|
expect(s.favorsCalled).toBe(i + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('neutralize_investigation', () => {
|
||||||
|
it('requires favor_network and having actually met Kommissarie Fors, then sets fors_neutralized', () => {
|
||||||
|
let s = createState();
|
||||||
|
s.troende = 3;
|
||||||
|
s.inflytande = 1000;
|
||||||
|
s.andakt = 1000;
|
||||||
|
|
||||||
|
const tooEarly = buyUpgrade(s, 'neutralize_investigation');
|
||||||
|
expect(tooEarly.flags.has('fors_neutralized')).toBe(false);
|
||||||
|
|
||||||
|
s = buyUpgrade(s, 'favor_network');
|
||||||
|
s.usedEventIds.add('coc_investigator_dream');
|
||||||
|
s = buyUpgrade(s, 'neutralize_investigation');
|
||||||
|
expect(s.flags.has('fors_neutralized')).toBe(true);
|
||||||
|
expect(s.upgrades.has('neutralize_investigation')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('bigger_venue (troende past nine)', () => {
|
||||||
|
it('is only available once nine_chairs_full is bought and troende reaches ten', () => {
|
||||||
|
let s = createState();
|
||||||
|
s.troende = 10;
|
||||||
|
s.andakt = 10000;
|
||||||
|
s.dread = 10000;
|
||||||
|
|
||||||
|
const tooEarly = buyUpgrade(s, 'bigger_venue');
|
||||||
|
expect(tooEarly.upgrades.has('bigger_venue')).toBe(false);
|
||||||
|
|
||||||
|
s.upgrades.add('nine_chairs_full');
|
||||||
|
const rateBefore = s.andaktRate;
|
||||||
|
s = buyUpgrade(s, 'bigger_venue');
|
||||||
|
expect(s.upgrades.has('bigger_venue')).toBe(true);
|
||||||
|
expect(s.andaktRate).toBeGreaterThan(rateBefore);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mollan_favor', () => {
|
||||||
|
it('requires the mollan_regular event to have actually fired first', () => {
|
||||||
|
let s = createState();
|
||||||
|
s.andakt = 10000;
|
||||||
|
|
||||||
|
const tooEarly = buyUpgrade(s, 'mollan_favor');
|
||||||
|
expect(tooEarly.upgrades.has('mollan_favor')).toBe(false);
|
||||||
|
|
||||||
|
s.usedEventIds.add('mollan_regular');
|
||||||
|
s = buyUpgrade(s, 'mollan_favor');
|
||||||
|
expect(s.upgrades.has('mollan_favor')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('buyUpgrade immutability', () => {
|
describe('buyUpgrade immutability', () => {
|
||||||
it('does not mutate the input state', () => {
|
it('does not mutate the input state', () => {
|
||||||
const s = createState();
|
const s = createState();
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface State {
|
|||||||
ritual: number; // 0-100, win at 100
|
ritual: number; // 0-100, win at 100
|
||||||
ritualRate: number; // per second
|
ritualRate: number; // per second
|
||||||
ritualUnlocked: boolean;
|
ritualUnlocked: boolean;
|
||||||
|
favorsCalled: number; // times "Kalla in en tjänst" bought — drives its own cost scaling
|
||||||
areas: { rosengard: boolean; varnhem: boolean; torso: boolean };
|
areas: { rosengard: boolean; varnhem: boolean; torso: boolean };
|
||||||
upgrades: Set<string>;
|
upgrades: Set<string>;
|
||||||
log: LogEntry[];
|
log: LogEntry[];
|
||||||
@@ -68,6 +69,7 @@ export function createState(): State {
|
|||||||
ritual: 0,
|
ritual: 0,
|
||||||
ritualRate: 0,
|
ritualRate: 0,
|
||||||
ritualUnlocked: false,
|
ritualUnlocked: false,
|
||||||
|
favorsCalled: 0,
|
||||||
areas: { rosengard: false, varnhem: false, torso: false },
|
areas: { rosengard: false, varnhem: false, torso: false },
|
||||||
upgrades: new Set(),
|
upgrades: new Set(),
|
||||||
log: [{ tick: 0, text: 'Mötet började i en fuktig källare i Rosengård. Nio stolar. En av dem är alltid lite för varm.', kind: 'system' }],
|
log: [{ tick: 0, text: 'Mötet började i en fuktig källare i Rosengård. Nio stolar. En av dem är alltid lite för varm.', kind: 'system' }],
|
||||||
@@ -483,6 +485,7 @@ export function loadState(): State | null {
|
|||||||
usedEventIds: new Set<string>(d.usedEventIds ?? []),
|
usedEventIds: new Set<string>(d.usedEventIds ?? []),
|
||||||
flags: new Set<string>(d.flags ?? []),
|
flags: new Set<string>(d.flags ?? []),
|
||||||
logSeq: d.logSeq ?? 0,
|
logSeq: d.logSeq ?? 0,
|
||||||
|
favorsCalled: d.favorsCalled ?? 0,
|
||||||
// Saves from before offline progress existed won't have this — treat
|
// Saves from before offline progress existed won't have this — treat
|
||||||
// them as "just now" so an old save doesn't suddenly claim you were
|
// them as "just now" so an old save doesn't suddenly claim you were
|
||||||
// away since the Unix epoch.
|
// away since the Unix epoch.
|
||||||
|
|||||||
@@ -458,6 +458,40 @@ export const STANDARD_EVENTS: GameEvent[] = [
|
|||||||
requires: { minDread: 30 },
|
requires: { minDread: 30 },
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── Kommissarie Fors, escalating ──
|
||||||
|
{
|
||||||
|
id: 'fors_closing_in',
|
||||||
|
text: 'Kommissarie Fors har börjat fråga kollegor om källaren i Rosengård, vaken den här gången. Hon minns nu vad hon drömt. Hon skriver rapporter om det, officiellt, i vanlig text, med sitt eget namn under.',
|
||||||
|
effect: { hemlighet: -14, dread: 10 },
|
||||||
|
weight: 5,
|
||||||
|
requires: { completedEventId: 'coc_investigator_dream', minDread: 45 },
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Nio blir fler ──
|
||||||
|
{
|
||||||
|
id: 'troende_overflow',
|
||||||
|
text: 'Källaren rymmer inte fler stolar. Ni har börjat stå längs väggarna. Ingen klagar. Ingen erbjuder heller sin plats till den som kom sist.',
|
||||||
|
effect: { kaos: 8, hemlighet: -6 },
|
||||||
|
weight: 6,
|
||||||
|
requires: { minTroende: 10 },
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Möllan och Universitet, fördjupade ──
|
||||||
|
{
|
||||||
|
id: 'mollan_regular',
|
||||||
|
text: 'Marknadshandlaren på Möllan har börjat spara undan varor åt er innan ni ens kommer dit. Hon kallar det stamkundsrabatt. Ingen av er minns att ni presenterat er för henne.',
|
||||||
|
effect: { andakt: 10, kaos: 4 },
|
||||||
|
weight: 7,
|
||||||
|
requires: { minTroende: 3 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'universitet_seminar',
|
||||||
|
text: 'En seminarieserie om "marina folktraditioner i Öresundsregionen" har startat på universitetet, utan att någon minns vem som initierade den. Föreläsaren citerar källor som inte existerar utanför Skriften.',
|
||||||
|
effect: { dread: 12, ritual: 2 },
|
||||||
|
weight: 6,
|
||||||
|
requires: { minRitual: 5 },
|
||||||
|
},
|
||||||
|
|
||||||
// ── Choice events ───────────────────────────────────────────────────────────
|
// ── Choice events ───────────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
id: 'choice_lindqvist_dream',
|
id: 'choice_lindqvist_dream',
|
||||||
@@ -1070,4 +1104,34 @@ export const KAOS_EVENTS: GameEvent[] = [
|
|||||||
weight: 4,
|
weight: 4,
|
||||||
requires: { minDread: 90 },
|
requires: { minDread: 90 },
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── Tyngre kaos-register: inte allt behöver vara ett skämt ──
|
||||||
|
{
|
||||||
|
id: 'k_thirteen_chimes',
|
||||||
|
text: 'Klockan i S:t Petri slår tretton slag mitt på dagen. Ingen på torget verkar höra det utom er. Ni räknar tillsammans efteråt, var för sig, utan att jämföra. Alla fick samma tal.',
|
||||||
|
effect: { kaos: -50, dread: 35 },
|
||||||
|
weight: 6,
|
||||||
|
requires: { minDread: 40 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'k_mirror_room_return',
|
||||||
|
text: 'Spegeln i förrummet visar återigen det gamla rummet, men den här gången känner ni igen en av personerna som sitter där. Det är en av er. Ingen säger vem. Ingen frågar heller.',
|
||||||
|
effect: { kaos: -55, dread: 45 },
|
||||||
|
weight: 5,
|
||||||
|
requires: { minDread: 60 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'k_sunken_lights',
|
||||||
|
text: 'Ett ljus syns nu permanent på botten av Öresund, på samma plats varje kväll. Det är inte fartygstrafik. Dykare skickas aldrig ner för att kolla. Ingen har föreslagit det. Ingen behöver föreslå det.',
|
||||||
|
effect: { kaos: -60, dread: 50, ritual: 4 },
|
||||||
|
weight: 4,
|
||||||
|
requires: { minDread: 90 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'k_warm_chair_used',
|
||||||
|
text: 'Den varma stolen i källaren var upptagen i kväll, för första gången någonsin. Ingen av er såg vem som satt där. Alla av er såg att stolen var upptagen.',
|
||||||
|
effect: { kaos: -65, dread: 55, hemlighet: -10 },
|
||||||
|
weight: 3,
|
||||||
|
requires: { minTroende: 8, minDread: 70 },
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -527,6 +527,53 @@ function renderCharacterGrid(state: State | null): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Both NPC arcs (Lindqvist listened/silenced, Hassan empowered/sidelined)
|
||||||
|
// previously only rerouted flavor text on the way to the same win/lose
|
||||||
|
// copy — the choices never actually reached the ending. Picking the most
|
||||||
|
// specific match first (both arcs resolved the same direction) means a
|
||||||
|
// player who leaned into both gets a variant that acknowledges both,
|
||||||
|
// rather than whichever flag happened to be checked first.
|
||||||
|
function pickVariant(flags: Set<string>, variants: { when: string[]; text: string }[], fallback: string): string {
|
||||||
|
const scored = variants
|
||||||
|
.filter(v => v.when.every(f => flags.has(f)))
|
||||||
|
.sort((a, b) => b.when.length - a.when.length);
|
||||||
|
return scored[0]?.text ?? fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WIN_BASE = 'Det är gjort. Turning Torso pekar rakt ned. Havet är stilla — alltför stilla. Malmö fortsätter som vanligt. Det är det värsta.';
|
||||||
|
|
||||||
|
const WIN_VARIANTS = [
|
||||||
|
{
|
||||||
|
when: ['hassan_empowered', 'lindqvist_listened'],
|
||||||
|
text: 'Det är gjort. Hassan står i cirkelns mitt och talar orden i en ordning ni aldrig lärde henne. Lindqvist står vid vattnet, torr, och nickar i takt med henne utan att höra. Turning Torso pekar rakt ned. Ni undrar, för första gången, om ni fortfarande behövs.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
when: ['hassan_empowered'],
|
||||||
|
text: 'Det är gjort, men det var Hassans röst som avslutade det, inte er. Cirkeln följde henne hela vägen. Turning Torso pekar rakt ned. Havet är stilla. Ni klappar henne på axeln. Hon ler och tittar förbi er, mot vattnet.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
when: ['lindqvist_listened'],
|
||||||
|
text: 'Det är gjort. Lindqvist stod vid vattnet när det hände, precis som han sagt att han skulle. Han kom tillbaka torr och log på ett sätt ni inte känner igen längre. Turning Torso pekar rakt ned. Havet är stilla — alltför stilla för att vara bara hav.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
when: ['fors_neutralized'],
|
||||||
|
text: 'Det är gjort. Ingen kom för att stoppa er — ni såg till det i god tid. Turning Torso pekar rakt ned. Havet är stilla. Kommissarie Fors sover, äntligen, utan drömmar. Ni undrar om det är en tjänst ni gjorde henne, eller tog ifrån henne.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const LOST_BASE_FALLBACK = 'Utredarna kom.';
|
||||||
|
|
||||||
|
const LOST_VARIANTS = [
|
||||||
|
{
|
||||||
|
when: ['hassan_empowered'],
|
||||||
|
text: 'Utredarna slog till vid gryningen. Hassan var den enda som inte sprang. Hon satt kvar i cirkeln och log mot dem tills de förde bort henne. Källaren är tom nu. Mattan ligger kvar. Under mattan finns fortfarande bordet.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
when: ['lindqvist_listened'],
|
||||||
|
text: 'Utredarna slog till vid gryningen. Lindqvist var inte där — han hade gått ner till vattnet igen, som han gjort så många nätter. Ingen vet om han någonsin fick veta att det var över. Källaren är tom. Mattan ligger kvar. Under mattan finns fortfarande bordet.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
function showOverlay(state: State): void {
|
function showOverlay(state: State): void {
|
||||||
const ov = document.getElementById('overlay')!;
|
const ov = document.getElementById('overlay')!;
|
||||||
ov.classList.add('visible');
|
ov.classList.add('visible');
|
||||||
@@ -536,7 +583,7 @@ function showOverlay(state: State): void {
|
|||||||
ov.style.backgroundImage = "url('/img/win.webp')";
|
ov.style.backgroundImage = "url('/img/win.webp')";
|
||||||
title.textContent = 'FULLBORDAT';
|
title.textContent = 'FULLBORDAT';
|
||||||
title.className = 'overlay-title won';
|
title.className = 'overlay-title won';
|
||||||
body.textContent = 'Det är gjort. Turning Torso pekar rakt ned. Havet är stilla — alltför stilla. Malmö fortsätter som vanligt. Det är det värsta.';
|
body.textContent = pickVariant(state.flags, WIN_VARIANTS, WIN_BASE);
|
||||||
} else if (state.phase === 'abandoned') {
|
} else if (state.phase === 'abandoned') {
|
||||||
ov.style.backgroundImage = "url('/img/fail.webp')";
|
ov.style.backgroundImage = "url('/img/fail.webp')";
|
||||||
title.textContent = 'ÖVERGIVET';
|
title.textContent = 'ÖVERGIVET';
|
||||||
@@ -546,7 +593,7 @@ function showOverlay(state: State): void {
|
|||||||
ov.style.backgroundImage = "url('/img/fail.webp')";
|
ov.style.backgroundImage = "url('/img/fail.webp')";
|
||||||
title.textContent = 'AVSLÖJAD';
|
title.textContent = 'AVSLÖJAD';
|
||||||
title.className = 'overlay-title lost';
|
title.className = 'overlay-title lost';
|
||||||
body.textContent = state.lostReason ?? 'Utredarna kom.';
|
body.textContent = pickVariant(state.flags, LOST_VARIANTS, state.lostReason ?? LOST_BASE_FALLBACK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,56 @@ export const UPGRADES: Upgrade[] = [
|
|||||||
available: s => s.upgrades.has('grand_ritual_prep'),
|
available: s => s.upgrades.has('grand_ritual_prep'),
|
||||||
once: false,
|
once: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'favor_network',
|
||||||
|
name: 'Nätverket av tjänster',
|
||||||
|
desc: 'Kontakterna ni samlat på er är fler än ni insett. En tjänst ger en annan. Nätverket börjar arbeta för sin egen skull nu, inte bara för er räkning. +inflytande/s, och en ny, större sorts tjänst blir möjlig att kalla in.',
|
||||||
|
cost: () => ({ inflytande: 60 }),
|
||||||
|
effect: s => { s.inflytandeRate += 0.5; },
|
||||||
|
available: s => s.troende >= 3 && !s.upgrades.has('favor_network'),
|
||||||
|
once: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'call_in_favor',
|
||||||
|
name: 'Kalla in en tjänst',
|
||||||
|
desc: 'Ring den som är skyldig er mest. Det är sällan gratis att bli påmind om vad man är skyldig — men det fungerar. Stor återhämtning av hemlighet, mot lite ökad kaos. Dyrare för varje gång ni ber om samma sak.',
|
||||||
|
cost: s => ({ inflytande: Math.round(50 * Math.pow(1.4, s.favorsCalled)) }),
|
||||||
|
effect: s => {
|
||||||
|
s.hemlighet = Math.min(100, s.hemlighet + 20);
|
||||||
|
s.kaos = Math.min(100, s.kaos + 6);
|
||||||
|
s.dread = Math.max(0, s.dread - 5);
|
||||||
|
s.favorsCalled += 1;
|
||||||
|
},
|
||||||
|
available: s => s.upgrades.has('favor_network'),
|
||||||
|
once: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'neutralize_investigation',
|
||||||
|
name: 'Neutralisera utredningen',
|
||||||
|
desc: 'Kommissarie Fors har en chef. Chefen har en tjänst att göra gentjänst för. Utredningen läggs ner, officiellt av resursskäl. Fors fortsätter drömma om er ändå — hon skriver bara inte ner det längre.',
|
||||||
|
cost: () => ({ inflytande: 150, andakt: 300 }),
|
||||||
|
effect: s => { s.hemlighet = Math.min(100, s.hemlighet + 25); s.flags.add('fors_neutralized'); },
|
||||||
|
available: s => s.upgrades.has('favor_network') && s.usedEventIds.has('coc_investigator_dream') && !s.upgrades.has('neutralize_investigation'),
|
||||||
|
once: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bigger_venue',
|
||||||
|
name: 'Källaren är för liten',
|
||||||
|
desc: 'Ni flyttar till en större lokal, officiellt "för läsecirkeln." Fler rum, fler dörrar, fler saker att förklara för grannarna. Den varma stolen flyttar med. Den är fortfarande varm.',
|
||||||
|
cost: () => ({ andakt: 1200, dread: 150 }),
|
||||||
|
effect: s => { s.andaktRate += 2; s.kaosRate += 0.25; },
|
||||||
|
available: s => s.troende >= 10 && s.upgrades.has('nine_chairs_full') && !s.upgrades.has('bigger_venue'),
|
||||||
|
once: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mollan_favor',
|
||||||
|
name: 'Stamkund på Möllan',
|
||||||
|
desc: 'Handlaren ger er mer än rabatt nu. Hon ber inte om förklaringar, bara om att få vara med "när det händer." Ni har inte sagt ja. Ni har inte sagt nej heller.',
|
||||||
|
cost: () => ({ andakt: 250 }),
|
||||||
|
effect: s => { s.andaktRate += 0.4; },
|
||||||
|
available: s => s.usedEventIds.has('mollan_regular') && !s.upgrades.has('mollan_favor'),
|
||||||
|
once: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'whisper_guidance',
|
id: 'whisper_guidance',
|
||||||
name: 'Viskningen leder er',
|
name: 'Viskningen leder er',
|
||||||
|
|||||||
Reference in New Issue
Block a user