Content pass: give inflytande a purpose, branch the endings, deepen troende/Möllan/Universitet
Docker / build-and-push (push) Successful in 1m23s
Docker / build-and-push (push) Successful in 1m23s
Addresses the gaps flagged after surveying the existing game against the new LORE-BIBLE.md: - inflytande had exactly one spending sink in the whole game. Added favor_network (unlocks a bigger repeatable "call in a favor" upgrade) and neutralize_investigation, which pays off the previously-dead-end Kommissarie Fors thread from coc_investigator_dream -- she stops investigating, but why she was dreaming about the cult in the first place stays exactly as unexplained as the lore bible says it should. - Lindqvist's and Hassan's event-chain flags (listened/silenced, empowered/sidelined) previously only rerouted flavor text on the way to identical win/lose copy. showOverlay() now picks a variant based on which flags are set, most-specific match first, so a run where both arcs resolved gets its own combined ending instead of falling through to whichever flag happened to be checked first. - troende past nine was flavor-only even though the hard cap was removed in 0.3.0. Added troende_overflow (event) and bigger_venue (upgrade, gated behind nine_chairs_full + troende>=10) so outgrowing the basement has a real mechanical consequence, not just a stat that keeps climbing quietly. - Möllan and Universitet (added in 0.6.0) had zero upgrade or event ties compared to Rosengård/Värnhem/Torso. Added mollan_regular + mollan_favor and universitet_seminar as a first pass -- lighter-touch than full area-gating (which would mean reshaping State.areas across engine/map/upgrades), but real content where there was none. - Kaos events skewed entirely comic relief against standard events' near-total dread. Added four kaos entries in the heavier register (thirteen chimes, the mirror room, permanent lights on the Öresund seabed, the warm chair finally occupied) as a tonal counterweight, lower-weighted so they don't crowd out the existing jokes. Added engine tests for all four new upgrades' gating and cost scaling. 21/21 tests pass, typecheck clean, build clean. Verified all six ending variants (win base/hassan/lindqvist/combo/fors, lose base/hassan/ lindqvist) render correctly against a real build via injected save state. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
a420032594
commit
e449cba288
+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