Add Inflytande resource, expand events, and balance the endgame
Docker / build-and-push (push) Successful in 42s

Adds an influence currency that scales with follower count and a new
"exert influence" action to trade it for secrecy, uncaps recruiting
with scaling cost, adds a late-game Andakt sink, and makes hemlighet
drain scale with dread/ritual progress so the endgame carries real
risk. Includes a Vitest suite that simulates play to check balance,
which caught the influence-accrual rate being too slow to be useful
early game.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-07-08 17:28:32 +02:00
co-authored by Claude Sonnet 5
parent fd457ff312
commit 0efbe9d55b
8 changed files with 875 additions and 30 deletions
+33 -11
View File
@@ -4,7 +4,7 @@ export interface Upgrade {
id: string;
name: string;
desc: string;
cost: Partial<{ andakt: number; dread: number }>;
cost: (s: State) => Partial<{ andakt: number; dread: number; inflytande: number }>;
effect: (s: State) => void;
available: (s: State) => boolean;
once: boolean;
@@ -14,17 +14,26 @@ export const UPGRADES: Upgrade[] = [
{
id: 'recruit_neighbor',
name: 'Rekrytera en troende',
desc: '+1 Troende. De undrade varför källarlamporna flimrade. Nu vet de.',
cost: { andakt: 20 },
desc: '+1 Troende. De undrade varför källarlamporna flimrade. Nu vet de. Fler munnar att mätta — priset stiger för varje ny medlem.',
cost: s => ({ andakt: Math.round(20 * Math.pow(1.35, s.troende)) }),
effect: s => { s.troende += 1; },
available: s => s.troende < 12,
available: () => true,
once: false,
},
{
id: 'exert_influence',
name: 'Utöva inflytande',
desc: 'Ring några kontakter. En tjänsteman blir plötsligt mycket samarbetsvillig. En journalist glömmer varför hon ringde. +hemlighet, men ryktet sprids ändå lite. +kaos',
cost: () => ({ inflytande: 30 }),
effect: s => { s.hemlighet = Math.min(100, s.hemlighet + 8); s.kaos = Math.min(100, s.kaos + 3); },
available: s => s.troende >= 2,
once: false,
},
{
id: 'first_meeting',
name: 'Källarmöte i Rosengård',
desc: 'Broder Lindqvist vet en källarlokal. Den ska egentligen vara ett förråd. Hyresvärden vet inte om den. Det är okänt varför han inte vet om den. +1 andakt/s.',
cost: { andakt: 30 },
cost: () => ({ andakt: 30 }),
effect: s => { s.andaktRate += 1; s.areas.rosengard = true; },
available: s => s.troende >= 2 && !s.upgrades.has('first_meeting'),
once: true,
@@ -33,7 +42,7 @@ export const UPGRADES: Upgrade[] = [
id: 'the_manuscript',
name: 'Översätt Skriften',
desc: 'Dokumentet hittades inmurad i en vägg vid Davidshallsgatan, bakom kakel från 1923. Texten rör sig om man tittar bort. Lingvisten vi anlitade satt tyst i tre timmar och gick sedan hem. Han skickade översättningen nästa morgon utan kommentar. Vi läser den inte högt.',
cost: { andakt: 80 },
cost: () => ({ andakt: 80 }),
effect: s => { s.dreadRate += 0.5; s.ritualUnlocked = true; },
available: s => s.troende >= 3 && s.upgrades.has('first_meeting') && !s.upgrades.has('the_manuscript'),
once: true,
@@ -42,7 +51,7 @@ export const UPGRADES: Upgrade[] = [
id: 'blood_pact',
name: 'Paktens Förnyelse',
desc: 'Det är en gammal pakt. Någon tecknade den för länge sedan. Vi vet inte vem. Vi förnyar den ändå — det verkar artigare. Skriften vet om det innan vi är klara.',
cost: { andakt: 150, dread: 30 },
cost: () => ({ andakt: 150, dread: 30 }),
effect: s => { s.andaktRate += 1.5; s.hemlighet = Math.max(0, s.hemlighet - 10); },
available: s => s.upgrades.has('the_manuscript') && !s.upgrades.has('blood_pact'),
once: true,
@@ -51,7 +60,7 @@ export const UPGRADES: Upgrade[] = [
id: 'idol_under_bridge',
name: 'Idolen under bron',
desc: 'Vi fann den redan nergrävd under Øresundsbron. Vi behövde bara gräva ner den djupare. Materialet är okänt. Det är varmt mot handen. Danskarna la märke till ingenting. Det är värre.',
cost: { andakt: 120, dread: 20 },
cost: () => ({ andakt: 120, dread: 20 }),
effect: s => { s.dreadRate += 1; s.andaktRate += 0.5; },
available: s => s.upgrades.has('the_manuscript') && !s.upgrades.has('idol_under_bridge'),
once: true,
@@ -60,7 +69,7 @@ export const UPGRADES: Upgrade[] = [
id: 'varnhem_circle',
name: 'Cirkeln på Värnhemstorget',
desc: 'Rita cirkeln. Använd krita — det är viktigt att det är krita, vi vet inte varför men vi känner det. Kommunen tvättar bort den varje morgon. Den är tillbaka varje kväll. Nu är den tillbaka lite större.',
cost: { andakt: 200, dread: 40 },
cost: () => ({ andakt: 200, dread: 40 }),
effect: s => { s.areas.varnhem = true; s.dreadRate += 1.5; s.kaosRate += 0.2; },
available: s => s.upgrades.has('idol_under_bridge') && s.dread >= 50 && !s.upgrades.has('varnhem_circle'),
once: true,
@@ -69,7 +78,7 @@ export const UPGRADES: Upgrade[] = [
id: 'torso_beacon',
name: 'Altaret vid Turning Torso',
desc: 'Calatrava-Santiago ritade byggnaden som en vridande ryggrad. Det är korrekt. Det är inte en metafor. Byggnaden har alltid lyssnat. Nu ber vi den svara. Den gör det. Svaret är ett ljud under hörselgränsen som vi känner i tänderna.',
cost: { andakt: 400, dread: 100 },
cost: () => ({ andakt: 400, dread: 100 }),
effect: s => { s.areas.torso = true; s.dreadRate += 3; s.ritualRate += 0.5; },
available: s => s.upgrades.has('varnhem_circle') && s.dread >= 100 && !s.upgrades.has('torso_beacon'),
once: true,
@@ -78,9 +87,22 @@ export const UPGRADES: Upgrade[] = [
id: 'grand_ritual_prep',
name: 'Förberedelser för Den Stora Ritualen',
desc: 'Allt har lett hit. Turning Torso pekar. Värnhemstorget svarar. Öresund drar sig tillbaka som om det vet vad som kommer. Skriften är nu fullständig. Den sista sidan skrevs av sig själv i natt. Det är ett recept.',
cost: { andakt: 600, dread: 200 },
cost: () => ({ andakt: 600, dread: 200 }),
effect: s => { s.ritualRate += 2; s.kaosRate += 0.5; },
available: s => s.upgrades.has('torso_beacon') && s.ritual >= 50 && !s.upgrades.has('grand_ritual_prep'),
once: true,
},
{
id: 'deepen_ritual',
name: 'Fördjupa ritualen',
desc: 'Ännu ett lager av mening under det förra. Andakt är inget värt om det bara samlas i en burk. Varje fördjupning kostar mer än den förra — Den Sovande blir kräsnare ju närmre ni kommer.',
cost: s => ({ andakt: Math.round(300 * Math.pow(1.6, ritualDeepenings(s))), dread: 50 }),
effect: s => { s.ritualRate += 0.3; },
available: s => s.upgrades.has('grand_ritual_prep'),
once: false,
},
];
function ritualDeepenings(s: State): number {
return Math.round((s.ritualRate - 2.5) / 0.3);
}