Expand letters and decision consequences
This commit is contained in:
@@ -124,6 +124,94 @@ export const LETTERS: Letter[] = [
|
|||||||
body: 'Om du läser detta har jag fått gå hem. Det händer sällan här. Bordet behöver någon.', marks: ['PERSONALAKT', 'SVART STÄMPEL', 'SVART STÄMPEL', 'SVART STÄMPEL'],
|
body: 'Om du läser detta har jag fått gå hem. Det händer sällan här. Bordet behöver någon.', marks: ['PERSONALAKT', 'SVART STÄMPEL', 'SVART STÄMPEL', 'SVART STÄMPEL'],
|
||||||
correctRoute: 'archive', explanation: 'Äldre än femtio år och tre svarta stämplar. Arkiv 9 tar hand om företrädaren. Förmodligen.',
|
correctRoute: 'archive', explanation: 'Äldre än femtio år och tre svarta stämplar. Arkiv 9 tar hand om företrädaren. Förmodligen.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'l21', day: 1, sender: 'MKB Fastighets AB', recipient: 'Boende, lgh 1302', address: 'Grönegatan 8, Malmö', postmark: '2026-07-21',
|
||||||
|
body: 'Planerat vattenavbrott torsdag 09:00–15:00. Knackningar i ledningarna under övrig tid ingår inte i felanmälan.', marks: ['SAMHÄLLSINFORMATION'],
|
||||||
|
correctRoute: 'deliver', explanation: 'Fullständig adress och nutida mottagare. Fastighetens rörsystem ligger utanför uppdraget.',
|
||||||
|
correctConsequence: 'Meddelandet gick vidare. Klockan 09:01 på torsdag blev hela huset tyst.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'l22', day: 1, sender: 'Havet', recipient: 'Den som öppnade sist', address: '', postmark: '2026-07-21',
|
||||||
|
body: 'ÖPPNA INTE. Jag har redan kommit in genom kuvertets hörn.', marks: ['SALTFLÄCKAR', 'ÖPPNA EJ'],
|
||||||
|
correctRoute: 'quarantine', explanation: 'Brevet ber uttryckligen att inte öppnas. §3.1 gäller oavsett avsändarens geografiska omfång.',
|
||||||
|
correctConsequence: 'Karantänskåpets nedersta låda började droppa. Golvbrunnen tog hand om det mesta.',
|
||||||
|
wrongConsequences: {
|
||||||
|
deliver: 'Brevbäraren kom tillbaka genomblöt och utan skugga. Händelsen registrerades som arbetsrelaterad.',
|
||||||
|
return: 'Kustbevakningen hittade retursäcken flytande utanför Limhamn. Den var varm.',
|
||||||
|
archive: 'Arkiv 9 stängde för sanering. Din signatur fanns på insidan av samtliga kuvert.',
|
||||||
|
},
|
||||||
|
penalty: { deliver: { composure: 2, suspicion: 2 }, return: { suspicion: 2 }, archive: { composure: 2 } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'l23', day: 2, sender: 'Folkbokföringen', recipient: 'Karl-Gustav Holm (1901–1974)', address: 'Rönneholmsvägen 44', postmark: '2026-07-22',
|
||||||
|
body: 'Bekräftelse av adressändring. Ny folkbokföringsadress: samma plats, cirka två meter längre ned.', marks: ['PERSONBEVIS'],
|
||||||
|
correctRoute: 'archive', explanation: 'Mottagaren är avliden. Formulärets exakta djup förändrar inte §4.3.',
|
||||||
|
correctConsequence: 'Arkiv 9 lade handlingen i en mapp märkt FAST BOENDE. Mappen var redan full.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'l24', day: 2, sender: 'Karl-Gustav Holm (1901–1974)', recipient: 'Folkbokföringen', address: 'Kattsundsgatan 7', postmark: '2026-07-22',
|
||||||
|
body: 'Adressändringen godkänns inte. Det är trångt här nere och grannen spelar radio hela natten.', marks: ['LUKT AV FUKT', 'HANDSKRIVET'],
|
||||||
|
correctRoute: 'quarantine', explanation: 'En avliden person står som avsändare. §4.7 är den specifika och senare föreskriften.',
|
||||||
|
correctConsequence: 'Brevet låstes in. Radion hördes ändå svagt från ventilationsgallret resten av dagen.',
|
||||||
|
wrongConsequences: {
|
||||||
|
deliver: 'Folkbokföringen registrerade Karl-Gustav som levande. Systemet registrerade samtidigt dig som avliden.',
|
||||||
|
return: 'Kuvertet återkom med tillägget NEKAS: MOTTAGAREN EJ HEMMA, skrivet med din handstil.',
|
||||||
|
archive: 'Arkiv 9 vägrade ta emot avsändaren två gånger. Den tredje gången tog de emot budet i stället.',
|
||||||
|
},
|
||||||
|
penalty: { deliver: { composure: 1, suspicion: 2 }, return: { composure: 2 }, archive: { suspicion: 2 } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'l25', day: 3, sender: 'Avdelning Noll', recipient: 'Vaktmästeriet', address: 'Källarplan -6', postmark: '2026-07-23',
|
||||||
|
body: 'Byt lampan i korridoren. Den nuvarande belyser sådant som inte längre är anställt.', marks: ['INTERNPOST', '0', 'BRÅDSKANDE'],
|
||||||
|
correctRoute: 'deliver', explanation: 'Avdelning Nolls internpost skall alltid vidare enligt §5.9, trots det negativa våningsplanet.',
|
||||||
|
correctConsequence: 'Vaktmästaren tog med en stege och kom tillbaka utan den. Lampan fungerar nu.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'l26', day: 3, sender: 'Bostadsrättsföreningen Eken', recipient: 'Innehavaren', address: 'Östra Rönneholmsvägen -1', postmark: '2026-07-23',
|
||||||
|
body: 'Kallelse till extra föreningsstämma angående den nya lägenheten under tvättstugan.', marks: ['REKOMMENDERAT'],
|
||||||
|
correctRoute: 'return', explanation: 'Negativa gatunummer är ogiltiga enligt §5.2. Det gäller även bostadsrättsföreningar med expansionsplaner.',
|
||||||
|
correctConsequence: 'Föreningen strök lägenheten ur röstlängden. Avgiften drogs fortfarande via autogiro.',
|
||||||
|
wrongConsequences: {
|
||||||
|
deliver: 'En ny namnskylt dök upp under hissens nedersta knapp. Föreningen kräver nu att du betalar medlemsavgift.',
|
||||||
|
archive: 'Arkiv 9 registrerade fastigheten. Kommunens karta fick ett hål där kvarteret brukade ligga.',
|
||||||
|
quarantine: 'Karantänskåpet kallades till föreningsstämman och fick två röster.',
|
||||||
|
},
|
||||||
|
penalty: { deliver: { suspicion: 2 }, archive: { composure: 2 }, quarantine: { suspicion: 2 } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'l27', day: 4, sender: 'Arkiv 9', recipient: 'Sorteringen', address: 'Internfack 9', postmark: '2026-07-24',
|
||||||
|
body: 'Den här försändelsen har tre svarta stämplar. Den här försändelsen har fyra svarta stämplar.', marks: ['SVART STÄMPEL', 'SVART STÄMPEL', 'SVART STÄMPEL', 'TEXTEN HAR ÄNDRATS'],
|
||||||
|
correctRoute: 'quarantine', explanation: 'Textförändring enligt §6.6 har uttryckligt företräde framför trestämpelregeln.',
|
||||||
|
correctConsequence: 'Skåpet svalde brevet utan att öppnas. En fjärde svart stämpel syntes kort på din handrygg.',
|
||||||
|
wrongConsequences: {
|
||||||
|
archive: 'Arkiv 9 skickade tillbaka brevet tillsammans med ett fotografi av dig när du fattade beslutet.',
|
||||||
|
},
|
||||||
|
penalty: { archive: { composure: 2, suspicion: 2 } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'l28', day: 4, sender: 'Länsstyrelsen', recipient: 'Arkiv 9', address: 'Internleverans', postmark: '2026-07-24',
|
||||||
|
body: 'Slutrapport rörande tre försvunna brunnar, två felplacerade vittnen och ett godkänt avsteg.', marks: ['SVART STÄMPEL', 'SVART STÄMPEL', 'SVART STÄMPEL'],
|
||||||
|
correctRoute: 'archive', explanation: 'Tre svarta stämplar innebär Arkiv 9. Texten har inte förändrats, hur mycket man än önskar det.',
|
||||||
|
correctConsequence: 'Rapporten arkiverades. En av brunnarna ströks från bilagan och från verkligheten.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'l29', day: 5, sender: 'Skatteverket 2041', recipient: 'Skatteverket 2026', address: 'Kungsgatan 15', postmark: '2041-03-18',
|
||||||
|
body: 'Förhandsbesked: dödsboet efter Staten Sverige saknar behörig firmatecknare.', marks: ['FRAMTIDA', 'TJÄNSTEPOST'],
|
||||||
|
correctRoute: 'return', explanation: 'Framtida post skall återsändas enligt §7.1. Innehållets skattemässiga betydelse är inte ert ansvar.',
|
||||||
|
correctConsequence: 'Returen accepterades femton år i förväg. Ett automatiskt dröjsmålsbrev skrevs ut omedelbart.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'l30', day: 5, sender: 'Föreståndaren, måndag', recipient: 'Du, fredag', address: 'Ditt sorteringsbord', postmark: '2026-07-27',
|
||||||
|
body: 'Du läser det sista brevet nu. När du har stämplat hör du hissen stanna på plan fyra. Ta inte med nyckeln.', marks: ['NÄSTA VECKA', 'PERSONLIGT'],
|
||||||
|
correctRoute: 'quarantine', explanation: 'Framtida post som beskriver den pågående arbetsdagen omfattas av §7.4. Det räcker att beskrivningen är korrekt.',
|
||||||
|
correctConsequence: 'Brevet låstes in. Hissen stannade ändå på plan fyra. Någon därute provade din dörr tre gånger.',
|
||||||
|
wrongConsequences: {
|
||||||
|
deliver: 'Du vidarebefordrade brevet till dig själv. Hissen öppnades och nästa arbetsvecka började genast.',
|
||||||
|
return: 'Föreståndaren tog emot returen i måndags och skrev ditt namn på fredagens frånvarolista.',
|
||||||
|
archive: 'Arkiv 9 registrerade dagen som redan avslutad. Alla klockor i huset hoppade till 17:01.',
|
||||||
|
},
|
||||||
|
penalty: { deliver: { composure: 2, suspicion: 2 }, return: { suspicion: 2 }, archive: { composure: 2 } },
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DAY_BRIEFINGS = [
|
export const DAY_BRIEFINGS = [
|
||||||
|
|||||||
@@ -32,4 +32,21 @@ describe('sorting engine', () => {
|
|||||||
expect(state.day).toBe(2);
|
expect(state.day).toBe(2);
|
||||||
expect(state.composure).toBe(3);
|
expect(state.composure).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('applies a letter-specific high-risk consequence', () => {
|
||||||
|
let state = startGame(createState());
|
||||||
|
const letters = lettersForDay(1);
|
||||||
|
for (const letter of letters.slice(0, 5)) state = routeLetter(state, letter.correctRoute);
|
||||||
|
expect(currentLetter(state)?.id).toBe('l22');
|
||||||
|
|
||||||
|
state = routeLetter(state, 'deliver');
|
||||||
|
expect(state.lastDecision?.correct).toBe(false);
|
||||||
|
expect(state.lastDecision?.composureCost).toBe(2);
|
||||||
|
expect(state.lastDecision?.suspicionCost).toBe(2);
|
||||||
|
expect(state.lastDecision?.consequence).toContain('utan skugga');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has six authored letters in every shift', () => {
|
||||||
|
for (let day = 1; day <= 5; day += 1) expect(lettersForDay(day)).toHaveLength(6);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ export const MAX_COMPOSURE = 3;
|
|||||||
export const MAX_SUSPICION = 5;
|
export const MAX_SUSPICION = 5;
|
||||||
export const LAST_DAY = Math.max(...LETTERS.map((letter) => letter.day));
|
export const LAST_DAY = Math.max(...LETTERS.map((letter) => letter.day));
|
||||||
|
|
||||||
|
const DEFAULT_WRONG_CONSEQUENCES: Record<Route, string> = {
|
||||||
|
deliver: 'Försändelsen lämnade byggnaden. Någon ringde senare, men samtalet innehöll bara ditt eget andetag.',
|
||||||
|
return: 'Returtransporten kom tillbaka tom. Chauffören ville inte säga var han hade varit.',
|
||||||
|
archive: 'Arkiv 9 kvitterade försändelsen och registrerade samtidigt en anmärkning i din personalakt.',
|
||||||
|
quarantine: 'Karantänskåpet tog emot brevet. Något annat därinne slutade knacka.',
|
||||||
|
};
|
||||||
|
|
||||||
export function createState(): GameState {
|
export function createState(): GameState {
|
||||||
return {
|
return {
|
||||||
phase: 'intro', day: 1, letterIndex: 0, accuracy: 100,
|
phase: 'intro', day: 1, letterIndex: 0, accuracy: 100,
|
||||||
@@ -30,11 +37,18 @@ export function routeLetter(state: GameState, route: Route): GameState {
|
|||||||
if (!letter) return state;
|
if (!letter) return state;
|
||||||
|
|
||||||
const correct = letter.correctRoute === route;
|
const correct = letter.correctRoute === route;
|
||||||
const decisions = [...state.decisions, { letterId: letter.id, route, correct }];
|
const configuredPenalty = letter.penalty?.[route];
|
||||||
|
const composureCost = correct ? 0 : configuredPenalty?.composure ?? 1;
|
||||||
|
const suspicionCost = correct ? 0 : configuredPenalty?.suspicion ?? 1;
|
||||||
|
const consequence = correct
|
||||||
|
? letter.correctConsequence ?? 'Försändelsen försvann ned i rätt fack. Ingen protesterade högt nog för att räknas.'
|
||||||
|
: letter.wrongConsequences?.[route] ?? DEFAULT_WRONG_CONSEQUENCES[route];
|
||||||
|
const decision = { letterId: letter.id, route, correct, consequence, composureCost, suspicionCost };
|
||||||
|
const decisions = [...state.decisions, decision];
|
||||||
const correctCount = decisions.filter((decision) => decision.correct).length;
|
const correctCount = decisions.filter((decision) => decision.correct).length;
|
||||||
const accuracy = Math.round((correctCount / decisions.length) * 100);
|
const accuracy = Math.round((correctCount / decisions.length) * 100);
|
||||||
const composure = Math.max(0, state.composure - (correct ? 0 : 1));
|
const composure = Math.max(0, state.composure - composureCost);
|
||||||
const suspicion = Math.min(MAX_SUSPICION, state.suspicion + (correct ? 0 : 1));
|
const suspicion = Math.min(MAX_SUSPICION, state.suspicion + suspicionCost);
|
||||||
const dayLetters = lettersForDay(state.day);
|
const dayLetters = lettersForDay(state.day);
|
||||||
const isLastLetter = state.letterIndex >= dayLetters.length - 1;
|
const isLastLetter = state.letterIndex >= dayLetters.length - 1;
|
||||||
|
|
||||||
@@ -50,7 +64,7 @@ export function routeLetter(state: GameState, route: Route): GameState {
|
|||||||
composure,
|
composure,
|
||||||
suspicion,
|
suspicion,
|
||||||
decisions,
|
decisions,
|
||||||
lastDecision: { letterId: letter.id, route, correct },
|
lastDecision: decision,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,12 +81,12 @@ export function nextDay(state: GameState): GameState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function saveState(state: GameState): void {
|
export function saveState(state: GameState): void {
|
||||||
localStorage.setItem('retur-save-v1', JSON.stringify(state));
|
localStorage.setItem('retur-save-v2', JSON.stringify(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadState(): GameState | null {
|
export function loadState(): GameState | null {
|
||||||
try {
|
try {
|
||||||
const value = localStorage.getItem('retur-save-v1');
|
const value = localStorage.getItem('retur-save-v2');
|
||||||
return value ? JSON.parse(value) as GameState : null;
|
return value ? JSON.parse(value) as GameState : null;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@@ -80,5 +94,5 @@ export function loadState(): GameState | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function clearState(): void {
|
export function clearState(): void {
|
||||||
localStorage.removeItem('retur-save-v1');
|
localStorage.removeItem('retur-save-v2');
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/main.ts
20
src/main.ts
@@ -6,6 +6,7 @@ import type { Route } from './types.ts';
|
|||||||
let state = loadState() ?? createState();
|
let state = loadState() ?? createState();
|
||||||
let selectedRoute: Route | null = null;
|
let selectedRoute: Route | null = null;
|
||||||
let showingResult = false;
|
let showingResult = false;
|
||||||
|
let resultAdvanced = false;
|
||||||
|
|
||||||
const app = document.getElementById('app')!;
|
const app = document.getElementById('app')!;
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ function escapeHtml(value: string): string {
|
|||||||
function render(): void {
|
function render(): void {
|
||||||
document.documentElement.style.setProperty('--suspicion', String(state.suspicion / 5));
|
document.documentElement.style.setProperty('--suspicion', String(state.suspicion / 5));
|
||||||
if (state.phase === 'intro') return renderIntro();
|
if (state.phase === 'intro') return renderIntro();
|
||||||
|
if (showingResult) return renderDesk();
|
||||||
if (state.phase === 'day-end') return renderDayEnd();
|
if (state.phase === 'day-end') return renderDayEnd();
|
||||||
if (state.phase === 'won' || state.phase === 'lost') return renderEnding();
|
if (state.phase === 'won' || state.phase === 'lost') return renderEnding();
|
||||||
renderDesk();
|
renderDesk();
|
||||||
@@ -63,9 +65,13 @@ function renderDesk(): void {
|
|||||||
? `<div class="result ${state.lastDecision.correct ? 'correct' : 'wrong'}">
|
? `<div class="result ${state.lastDecision.correct ? 'correct' : 'wrong'}">
|
||||||
<strong>${state.lastDecision.correct ? 'KORREKT SORTERAD' : 'ANMÄRKNING REGISTRERAD'}</strong>
|
<strong>${state.lastDecision.correct ? 'KORREKT SORTERAD' : 'ANMÄRKNING REGISTRERAD'}</strong>
|
||||||
<p>${escapeHtml(letter.explanation)}</p>
|
<p>${escapeHtml(letter.explanation)}</p>
|
||||||
|
<div class="consequence"><span>FÖLJD</span>${escapeHtml(state.lastDecision.consequence)}</div>
|
||||||
|
${state.lastDecision.correct ? '' : `<div class="penalty">−${state.lastDecision.composureCost} lugn · +${state.lastDecision.suspicionCost} misstanke</div>`}
|
||||||
<button id="continue-button">NÄSTA FÖRSÄNDELSE →</button>
|
<button id="continue-button">NÄSTA FÖRSÄNDELSE →</button>
|
||||||
</div>` : '';
|
</div>` : '';
|
||||||
|
|
||||||
|
const recentDecisions = state.decisions.slice(-3).reverse();
|
||||||
|
|
||||||
shell(`<main class="desk">
|
shell(`<main class="desk">
|
||||||
<aside class="panel status-panel">
|
<aside class="panel status-panel">
|
||||||
<div class="panel-title">Arbetsdag</div>
|
<div class="panel-title">Arbetsdag</div>
|
||||||
@@ -74,6 +80,7 @@ function renderDesk(): void {
|
|||||||
<div class="meter"><span>Lugn</span><b>${'●'.repeat(state.composure)}${'○'.repeat(3 - state.composure)}</b></div>
|
<div class="meter"><span>Lugn</span><b>${'●'.repeat(state.composure)}${'○'.repeat(3 - state.composure)}</b></div>
|
||||||
<div class="meter suspicion"><span>Misstanke</span><b>${state.suspicion} / 5</b><i style="width:${state.suspicion * 20}%"></i></div>
|
<div class="meter suspicion"><span>Misstanke</span><b>${state.suspicion} / 5</b><i style="width:${state.suspicion * 20}%"></i></div>
|
||||||
<div class="meter"><span>Noggrannhet</span><b>${state.accuracy}%</b></div>
|
<div class="meter"><span>Noggrannhet</span><b>${state.accuracy}%</b></div>
|
||||||
|
${recentDecisions.length ? `<div class="panel-title incidents-title">Senaste följder</div><ul class="incident-log">${recentDecisions.map((decision) => `<li class="${decision.correct ? 'ok' : 'bad'}"><b>${decision.letterId.toUpperCase()}</b>${escapeHtml(decision.consequence)}</li>`).join('')}</ul>` : ''}
|
||||||
<div class="panel-title regulations-title">Gällande föreskrifter</div>
|
<div class="panel-title regulations-title">Gällande föreskrifter</div>
|
||||||
<ol class="regulations">${regulations.map((rule) => `<li class="${rule.fromDay === state.day && state.day > 1 ? 'new-rule' : ''}"><b>§${rule.number}</b>${escapeHtml(rule.text)}${rule.note ? `<em>${escapeHtml(rule.note)}</em>` : ''}</li>`).join('')}</ol>
|
<ol class="regulations">${regulations.map((rule) => `<li class="${rule.fromDay === state.day && state.day > 1 ? 'new-rule' : ''}"><b>§${rule.number}</b>${escapeHtml(rule.text)}${rule.note ? `<em>${escapeHtml(rule.note)}</em>` : ''}</li>`).join('')}</ol>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -114,21 +121,20 @@ function stamp(): void {
|
|||||||
const before = currentLetter(state);
|
const before = currentLetter(state);
|
||||||
state = routeLetter(state, selectedRoute);
|
state = routeLetter(state, selectedRoute);
|
||||||
saveState(state);
|
saveState(state);
|
||||||
if (state.phase === 'playing' && before?.id !== currentLetter(state)?.id) {
|
resultAdvanced = before?.id !== currentLetter(state)?.id;
|
||||||
|
if (resultAdvanced) {
|
||||||
// The engine has advanced; temporarily step the view back to show why the previous decision scored as it did.
|
// The engine has advanced; temporarily step the view back to show why the previous decision scored as it did.
|
||||||
state = { ...state, letterIndex: state.letterIndex - 1 };
|
state = { ...state, letterIndex: state.letterIndex - 1 };
|
||||||
showingResult = true;
|
|
||||||
} else if (state.phase === 'day-end' || state.phase === 'won' || state.phase === 'lost') {
|
|
||||||
render();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
showingResult = true;
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
function advanceAfterResult(): void {
|
function advanceAfterResult(): void {
|
||||||
if (!showingResult) return;
|
if (!showingResult) return;
|
||||||
state = { ...state, letterIndex: state.letterIndex + 1 };
|
if (resultAdvanced) state = { ...state, letterIndex: state.letterIndex + 1 };
|
||||||
showingResult = false;
|
showingResult = false;
|
||||||
|
resultAdvanced = false;
|
||||||
selectedRoute = null;
|
selectedRoute = null;
|
||||||
saveState(state);
|
saveState(state);
|
||||||
render();
|
render();
|
||||||
@@ -158,7 +164,7 @@ function renderEnding(): void {
|
|||||||
<dl class="final-stats"><div><dt>Noggrannhet</dt><dd>${state.accuracy}%</dd></div><div><dt>Misstanke</dt><dd>${state.suspicion}/5</dd></div><div><dt>Försändelser</dt><dd>${state.decisions.length}</dd></div></dl>
|
<dl class="final-stats"><div><dt>Noggrannhet</dt><dd>${state.accuracy}%</dd></div><div><dt>Misstanke</dt><dd>${state.suspicion}/5</dd></div><div><dt>Försändelser</dt><dd>${state.decisions.length}</dd></div></dl>
|
||||||
<button class="primary" id="restart-button">NY PERSONALAKT</button>
|
<button class="primary" id="restart-button">NY PERSONALAKT</button>
|
||||||
</div></main>`);
|
</div></main>`);
|
||||||
document.getElementById('restart-button')?.addEventListener('click', () => { clearState(); state = createState(); selectedRoute = null; showingResult = false; render(); });
|
document.getElementById('restart-button')?.addEventListener('click', () => { clearState(); state = createState(); selectedRoute = null; showingResult = false; resultAdvanced = false; render(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindShell(): void {
|
function bindShell(): void {
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ button { font: inherit; }
|
|||||||
.meter.suspicion::after, .meter.suspicion i { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 3px; background: var(--border); }
|
.meter.suspicion::after, .meter.suspicion i { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 3px; background: var(--border); }
|
||||||
.meter.suspicion i { right: auto; background: var(--danger); z-index: 1; transition: width .25s; }
|
.meter.suspicion i { right: auto; background: var(--danger); z-index: 1; transition: width .25s; }
|
||||||
.regulations-title { margin-top: 24px; }
|
.regulations-title { margin-top: 24px; }
|
||||||
|
.incidents-title { margin-top: 24px; }
|
||||||
|
.incident-log { margin: 0; padding: 0; list-style: none; display: grid; gap: 7px; }
|
||||||
|
.incident-log li { padding-left: 10px; border-left: 2px solid var(--safe); color: var(--dim); font-size: 9px; line-height: 1.45; }
|
||||||
|
.incident-log li.bad { border-left-color: var(--danger); }
|
||||||
|
.incident-log b { display: block; color: var(--text); font-size: 8px; }
|
||||||
.regulations { padding: 0; margin: 0; list-style: none; display: grid; gap: 9px; }
|
.regulations { padding: 0; margin: 0; list-style: none; display: grid; gap: 9px; }
|
||||||
.regulations li { padding-left: 36px; color: #aaaacc; font-size: 10px; position: relative; }
|
.regulations li { padding-left: 36px; color: #aaaacc; font-size: 10px; position: relative; }
|
||||||
.regulations b { position: absolute; left: 0; color: var(--accent); }
|
.regulations b { position: absolute; left: 0; color: var(--accent); }
|
||||||
@@ -64,6 +69,9 @@ button { font: inherit; }
|
|||||||
.result strong { font-size: clamp(22px, 4vw, 38px); letter-spacing: .12em; }
|
.result strong { font-size: clamp(22px, 4vw, 38px); letter-spacing: .12em; }
|
||||||
.result.correct strong { color: var(--safe); } .result.wrong strong { color: var(--danger); }
|
.result.correct strong { color: var(--safe); } .result.wrong strong { color: var(--danger); }
|
||||||
.result p { max-width: 540px; color: var(--dim); }
|
.result p { max-width: 540px; color: var(--dim); }
|
||||||
|
.consequence { max-width: 540px; margin: 10px auto 14px; padding: 12px; border: 1px solid var(--border); color: var(--text); font-size: 11px; }
|
||||||
|
.consequence span { display: block; margin-bottom: 4px; color: var(--archive); font-size: 8px; letter-spacing: .12em; }
|
||||||
|
.penalty { margin-bottom: 12px; color: var(--danger); font-size: 10px; text-transform: uppercase; }
|
||||||
.result button { border: 0; background: none; color: var(--text); cursor: pointer; }
|
.result button { border: 0; background: none; color: var(--text); cursor: pointer; }
|
||||||
.intro-screen, .end-screen { min-height: calc(100vh - 62px); display: grid; place-items: center; padding: 30px; background: radial-gradient(circle, #20192c 0, var(--bg) 66%); }
|
.intro-screen, .end-screen { min-height: calc(100vh - 62px); display: grid; place-items: center; padding: 30px; background: radial-gradient(circle, #20192c 0, var(--bg) 66%); }
|
||||||
.intro-screen { grid-template-columns: auto minmax(280px, 540px); gap: clamp(30px, 7vw, 90px); }
|
.intro-screen { grid-template-columns: auto minmax(280px, 540px); gap: clamp(30px, 7vw, 90px); }
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ export interface Letter {
|
|||||||
marks: string[];
|
marks: string[];
|
||||||
correctRoute: Route;
|
correctRoute: Route;
|
||||||
explanation: string;
|
explanation: string;
|
||||||
|
correctConsequence?: string;
|
||||||
|
wrongConsequences?: Partial<Record<Route, string>>;
|
||||||
|
penalty?: Partial<Record<Route, { composure?: number; suspicion?: number }>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Regulation {
|
export interface Regulation {
|
||||||
@@ -25,6 +28,9 @@ export interface Decision {
|
|||||||
letterId: string;
|
letterId: string;
|
||||||
route: Route;
|
route: Route;
|
||||||
correct: boolean;
|
correct: boolean;
|
||||||
|
consequence: string;
|
||||||
|
composureCost: number;
|
||||||
|
suspicionCost: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GameState {
|
export interface GameState {
|
||||||
|
|||||||
Reference in New Issue
Block a user