Compare commits
2
Commits
666b64e748
...
2fd6c5e854
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fd6c5e854 | ||
|
|
c23bd369f1 |
+10
-1
@@ -1,4 +1,4 @@
|
|||||||
export const VERSION = '0.7.0';
|
export const VERSION = '0.7.1';
|
||||||
|
|
||||||
export interface ChangelogEntry {
|
export interface ChangelogEntry {
|
||||||
version: string;
|
version: string;
|
||||||
@@ -7,6 +7,15 @@ export interface ChangelogEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CHANGELOG: ChangelogEntry[] = [
|
export const CHANGELOG: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
version: '0.7.1',
|
||||||
|
date: '2026-08-03',
|
||||||
|
changes: [
|
||||||
|
'Alla modaler (hjälp, sällskapet, bildvisning) går nu faktiskt att stänga genom att klicka utanför dem, inte bara med X-knappen — tidigare fungerade det bara av misstag i mitten av skärmen',
|
||||||
|
'Mjukat till hur mycket en frånvaro kan kosta: de första fem minuterna borta dränerar ingen hemlighet alls, och en enskild frånvaro kan aldrig kosta mer än 35 hemlighet totalt, hur länge den än varar',
|
||||||
|
'Fixat: Malmö universitets kartnål satt vid Hyllie, inte vid campus',
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
version: '0.7.0',
|
version: '0.7.0',
|
||||||
date: '2026-08-01',
|
date: '2026-08-01',
|
||||||
|
|||||||
@@ -246,6 +246,23 @@ describe('applyOfflineProgress', () => {
|
|||||||
expect(after.phase).toBe('lost');
|
expect(after.phase).toBe('lost');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('never drains hemlighet at all within the grace window', () => {
|
||||||
|
let s = createState();
|
||||||
|
s.troende = 50; // would otherwise drain fast
|
||||||
|
s.hemlighet = 50;
|
||||||
|
const after = applyOfflineProgress(s, 4 * 60 * 1000); // 4 minutes, under the 5-minute grace
|
||||||
|
expect(after.hemlighet).toBe(50);
|
||||||
|
expect(after.andakt).toBeGreaterThan(s.andakt); // rewards still accrue during grace
|
||||||
|
});
|
||||||
|
|
||||||
|
it('caps how much hemlighet a single absence can cost, no matter how long', () => {
|
||||||
|
let s = createState();
|
||||||
|
s.troende = 50; // heavy secrecy drain
|
||||||
|
s.hemlighet = 100;
|
||||||
|
const after = applyOfflineProgress(s, 24 * 3600 * 1000); // the full 24h cap
|
||||||
|
expect(after.hemlighet).toBeGreaterThanOrEqual(100 - 35);
|
||||||
|
});
|
||||||
|
|
||||||
it('never mutates the input state', () => {
|
it('never mutates the input state', () => {
|
||||||
const s = createState();
|
const s = createState();
|
||||||
const frozenAndakt = s.andakt;
|
const frozenAndakt = s.andakt;
|
||||||
|
|||||||
+18
-1
@@ -211,6 +211,8 @@ export function tick(state: State): State {
|
|||||||
|
|
||||||
const MAX_OFFLINE_SECONDS = 24 * 3600; // cap the catch-up so a week-long absence doesn't insta-win or insta-lose
|
const MAX_OFFLINE_SECONDS = 24 * 3600; // cap the catch-up so a week-long absence doesn't insta-win or insta-lose
|
||||||
const MIN_OFFLINE_SECONDS = 60; // don't bother with a log line for a quick tab-switch
|
const MIN_OFFLINE_SECONDS = 60; // don't bother with a log line for a quick tab-switch
|
||||||
|
const OFFLINE_GRACE_SECONDS = 5 * 60; // the first 5 minutes away accrue rewards but risk no hemlighet at all
|
||||||
|
const MAX_OFFLINE_HEMLIGHET_DROP = 35; // one absence, however long, can't cost more than this much secrecy
|
||||||
|
|
||||||
// Approximates what `tick()` would have done, second-by-second, without
|
// Approximates what `tick()` would have done, second-by-second, without
|
||||||
// actually running potentially hundreds of thousands of frames synchronously
|
// actually running potentially hundreds of thousands of frames synchronously
|
||||||
@@ -222,6 +224,7 @@ export function applyOfflineProgress(state: State, awayMs: number): State {
|
|||||||
if (awaySec < MIN_OFFLINE_SECONDS) return state;
|
if (awaySec < MIN_OFFLINE_SECONDS) return state;
|
||||||
|
|
||||||
const s = { ...state, areas: { ...state.areas }, upgrades: new Set(state.upgrades), log: [...state.log], usedEventIds: new Set(state.usedEventIds), flags: new Set(state.flags) };
|
const s = { ...state, areas: { ...state.areas }, upgrades: new Set(state.upgrades), log: [...state.log], usedEventIds: new Set(state.usedEventIds), flags: new Set(state.flags) };
|
||||||
|
const startingHemlighet = s.hemlighet;
|
||||||
|
|
||||||
s.andakt += s.andaktRate * s.troende * awaySec;
|
s.andakt += s.andaktRate * s.troende * awaySec;
|
||||||
s.inflytande += INFLYTANDE_BASE_RATE * s.inflytandeRate * Math.pow(s.troende, 1.2) * awaySec;
|
s.inflytande += INFLYTANDE_BASE_RATE * s.inflytandeRate * Math.pow(s.troende, 1.2) * awaySec;
|
||||||
@@ -233,10 +236,16 @@ export function applyOfflineProgress(state: State, awayMs: number): State {
|
|||||||
const kaosReleased = s.kaos + kaosGain >= 100;
|
const kaosReleased = s.kaos + kaosGain >= 100;
|
||||||
s.kaos = Math.min(100, s.kaos + kaosGain);
|
s.kaos = Math.min(100, s.kaos + kaosGain);
|
||||||
|
|
||||||
|
// A quick tab-out shouldn't be punished at all, and even a long one has
|
||||||
|
// a floor on how much it can cost — losing the game entirely to a
|
||||||
|
// stretch you weren't watching, with no chance to react, felt like a
|
||||||
|
// different (worse) kind of loss than one you saw coming during play.
|
||||||
|
const drainSec = Math.max(0, awaySec - OFFLINE_GRACE_SECONDS);
|
||||||
const troendeDrain = s.troende > 4 ? HEMLIGHET_DRAIN_PER_TROENDE * (s.troende - 4) : 0;
|
const troendeDrain = s.troende > 4 ? HEMLIGHET_DRAIN_PER_TROENDE * (s.troende - 4) : 0;
|
||||||
const dreadDrain = DREAD_HEMLIGHET_DRAIN * (s.dread / (s.dread + 150));
|
const dreadDrain = DREAD_HEMLIGHET_DRAIN * (s.dread / (s.dread + 150));
|
||||||
const ritualDrain = s.ritualUnlocked ? RITUAL_HEMLIGHET_DRAIN * (s.ritual / 100) : 0;
|
const ritualDrain = s.ritualUnlocked ? RITUAL_HEMLIGHET_DRAIN * (s.ritual / 100) : 0;
|
||||||
s.hemlighet = Math.max(0, s.hemlighet - (troendeDrain + dreadDrain + ritualDrain) * awaySec);
|
const rawDrop = (troendeDrain + dreadDrain + ritualDrain) * drainSec;
|
||||||
|
s.hemlighet = Math.max(0, s.hemlighet - Math.min(MAX_OFFLINE_HEMLIGHET_DROP, rawDrop));
|
||||||
|
|
||||||
// A little flavor for the time away — non-choice events only, since nobody
|
// A little flavor for the time away — non-choice events only, since nobody
|
||||||
// was there to answer. Roughly one per two hours away, capped low so the
|
// was there to answer. Roughly one per two hours away, capped low so the
|
||||||
@@ -252,6 +261,14 @@ export function applyOfflineProgress(state: State, awayMs: number): State {
|
|||||||
s.kaos = Math.max(0, s.kaos - 80);
|
s.kaos = Math.max(0, s.kaos - 80);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-apply the session cap after events — those can also carry a
|
||||||
|
// hemlighet penalty, and the cap is meant to bound the *total* damage
|
||||||
|
// an absence can do, not just the passive-drain portion of it.
|
||||||
|
const totalDrop = startingHemlighet - s.hemlighet;
|
||||||
|
if (totalDrop > MAX_OFFLINE_HEMLIGHET_DROP) {
|
||||||
|
s.hemlighet = Math.max(0, startingHemlighet - MAX_OFFLINE_HEMLIGHET_DROP);
|
||||||
|
}
|
||||||
|
|
||||||
const awayLabel =
|
const awayLabel =
|
||||||
awaySec < 3600 ? `${Math.round(awaySec / 60)} minuter` :
|
awaySec < 3600 ? `${Math.round(awaySec / 60)} minuter` :
|
||||||
awaySec < MAX_OFFLINE_SECONDS ? `${(awaySec / 3600).toFixed(1)} timmar` :
|
awaySec < MAX_OFFLINE_SECONDS ? `${(awaySec / 3600).toFixed(1)} timmar` :
|
||||||
|
|||||||
@@ -37,63 +37,6 @@ export function initUI(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="help-modal" class="help-modal">
|
|
||||||
<div class="help-modal-inner" id="help-modal-inner">
|
|
||||||
<div class="help-modal-header">
|
|
||||||
<span class="help-modal-title">DJUPET — Hjälp & Ändringslogg</span>
|
|
||||||
<button id="help-close" class="help-close">✕</button>
|
|
||||||
</div>
|
|
||||||
<div class="help-modal-body">
|
|
||||||
<section class="help-section">
|
|
||||||
<div class="help-section-title">Hur man spelar</div>
|
|
||||||
<ul class="help-list">
|
|
||||||
<li><strong>Andakt</strong> ackumuleras passivt. Använd den för att köpa åtgärder i vänsterpanelen.</li>
|
|
||||||
<li><strong>Dread</strong> ökar när ritualen fördjupas. Hög dread låser upp nya platser och händelser.</li>
|
|
||||||
<li><strong>Hemlighet</strong> sjunker när ni blir för uppenbara. Når den noll är det slut.</li>
|
|
||||||
<li><strong>Kaos</strong> byggs upp naturligt och utlöser kaotiska händelser när det når 100.</li>
|
|
||||||
<li><strong>Ritual</strong> låses upp via Skriften. Nå 100 för att fullborda ceremonin.</li>
|
|
||||||
<li>Klicka på <em>platser på kartan</em> för att besöka dem. Varje besök lämnar spår.</li>
|
|
||||||
<li>Händelser inträffar automatiskt. De flesta är dåliga. En del är värre.</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
<section class="help-section">
|
|
||||||
<div class="help-section-title">Om spelet</div>
|
|
||||||
<p class="help-flavor">
|
|
||||||
Djupet är ett idle-spel om en kult i Malmö, Sverige.<br>
|
|
||||||
Inspirerat av Anders Fager, Kult och Call of Cthulhu.<br>
|
|
||||||
Den Sovande är inte vaken ännu. Jobba på det.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
<section class="help-section">
|
|
||||||
<div class="help-section-title">Ändringslogg</div>
|
|
||||||
${CHANGELOG.map(e => `
|
|
||||||
<div class="help-changelog-entry">
|
|
||||||
<div class="help-changelog-version">v${e.version} <span class="help-changelog-date">${e.date}</span></div>
|
|
||||||
<ul class="help-list">${e.changes.map(c => `<li>${c}</li>`).join('')}</ul>
|
|
||||||
</div>
|
|
||||||
`).join('')}
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="characters-modal" class="help-modal">
|
|
||||||
<div class="help-modal-inner" id="characters-modal-inner">
|
|
||||||
<div class="help-modal-header">
|
|
||||||
<span class="help-modal-title">Sällskapet</span>
|
|
||||||
<button id="characters-close" class="help-close">✕</button>
|
|
||||||
</div>
|
|
||||||
<div class="help-modal-body">
|
|
||||||
<div class="archive-toggle" role="group" aria-label="Visningsläge">
|
|
||||||
<button type="button" class="archive-toggle-btn active" data-archive="story">Under spelets gång</button>
|
|
||||||
<button type="button" class="archive-toggle-btn" data-archive="full">Fullständigt arkiv</button>
|
|
||||||
</div>
|
|
||||||
<p class="help-flavor" id="characters-hint" style="margin-bottom:14px;">De som suttit vid bordet. Vissa längre än andra.</p>
|
|
||||||
<div id="characters-grid" class="characters-grid"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel" id="panel-left">
|
<div class="panel" id="panel-left">
|
||||||
<div class="panel-title">Resurser</div>
|
<div class="panel-title">Resurser</div>
|
||||||
|
|
||||||
@@ -168,6 +111,18 @@ export function initUI(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="danger-vignette"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Overlays live as siblings of #app, appended directly to <body>, not
|
||||||
|
// inside it. #app carries a `filter` (for the dread color effect),
|
||||||
|
// and CSS filter on an ancestor creates a new containing block for any
|
||||||
|
// `position: fixed` descendant — so these would clip to #app's own box
|
||||||
|
// (max-width: 1200px, centered) instead of the real viewport. On wider
|
||||||
|
// screens that left a dead margin on each side where "click outside to
|
||||||
|
// close" silently did nothing, because the click never actually landed
|
||||||
|
// inside the fixed element's clipped box.
|
||||||
|
document.body.insertAdjacentHTML('beforeend', `
|
||||||
<div id="overlay">
|
<div id="overlay">
|
||||||
<div class="overlay-scrim">
|
<div class="overlay-scrim">
|
||||||
<div class="overlay-title" id="overlay-title"></div>
|
<div class="overlay-title" id="overlay-title"></div>
|
||||||
@@ -176,7 +131,62 @@ export function initUI(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="danger-vignette"></div>
|
<div id="help-modal" class="help-modal">
|
||||||
|
<div class="help-modal-inner" id="help-modal-inner">
|
||||||
|
<div class="help-modal-header">
|
||||||
|
<span class="help-modal-title">DJUPET — Hjälp & Ändringslogg</span>
|
||||||
|
<button id="help-close" class="help-close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="help-modal-body">
|
||||||
|
<section class="help-section">
|
||||||
|
<div class="help-section-title">Hur man spelar</div>
|
||||||
|
<ul class="help-list">
|
||||||
|
<li><strong>Andakt</strong> ackumuleras passivt. Använd den för att köpa åtgärder i vänsterpanelen.</li>
|
||||||
|
<li><strong>Dread</strong> ökar när ritualen fördjupas. Hög dread låser upp nya platser och händelser.</li>
|
||||||
|
<li><strong>Hemlighet</strong> sjunker när ni blir för uppenbara. Når den noll är det slut.</li>
|
||||||
|
<li><strong>Kaos</strong> byggs upp naturligt och utlöser kaotiska händelser när det når 100.</li>
|
||||||
|
<li><strong>Ritual</strong> låses upp via Skriften. Nå 100 för att fullborda ceremonin.</li>
|
||||||
|
<li>Klicka på <em>platser på kartan</em> för att besöka dem. Varje besök lämnar spår.</li>
|
||||||
|
<li>Händelser inträffar automatiskt. De flesta är dåliga. En del är värre.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section class="help-section">
|
||||||
|
<div class="help-section-title">Om spelet</div>
|
||||||
|
<p class="help-flavor">
|
||||||
|
Djupet är ett idle-spel om en kult i Malmö, Sverige.<br>
|
||||||
|
Inspirerat av Anders Fager, Kult och Call of Cthulhu.<br>
|
||||||
|
Den Sovande är inte vaken ännu. Jobba på det.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section class="help-section">
|
||||||
|
<div class="help-section-title">Ändringslogg</div>
|
||||||
|
${CHANGELOG.map(e => `
|
||||||
|
<div class="help-changelog-entry">
|
||||||
|
<div class="help-changelog-version">v${e.version} <span class="help-changelog-date">${e.date}</span></div>
|
||||||
|
<ul class="help-list">${e.changes.map(c => `<li>${c}</li>`).join('')}</ul>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="characters-modal" class="help-modal">
|
||||||
|
<div class="help-modal-inner" id="characters-modal-inner">
|
||||||
|
<div class="help-modal-header">
|
||||||
|
<span class="help-modal-title">Sällskapet</span>
|
||||||
|
<button id="characters-close" class="help-close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="help-modal-body">
|
||||||
|
<div class="archive-toggle" role="group" aria-label="Visningsläge">
|
||||||
|
<button type="button" class="archive-toggle-btn active" data-archive="story">Under spelets gång</button>
|
||||||
|
<button type="button" class="archive-toggle-btn" data-archive="full">Fullständigt arkiv</button>
|
||||||
|
</div>
|
||||||
|
<p class="help-flavor" id="characters-hint" style="margin-bottom:14px;">De som suttit vid bordet. Vissa längre än andra.</p>
|
||||||
|
<div id="characters-grid" class="characters-grid"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="event-modal" class="help-modal">
|
<div id="event-modal" class="help-modal">
|
||||||
<div class="help-modal-inner" id="event-modal-inner">
|
<div class="help-modal-inner" id="event-modal-inner">
|
||||||
@@ -193,7 +203,7 @@ export function initUI(
|
|||||||
<div id="lightbox-caption" class="lightbox-caption"></div>
|
<div id="lightbox-caption" class="lightbox-caption"></div>
|
||||||
<p id="lightbox-blurb" class="lightbox-blurb"></p>
|
<p id="lightbox-blurb" class="lightbox-blurb"></p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`);
|
||||||
|
|
||||||
document.getElementById('overlay-btn')!.addEventListener('click', onRestart);
|
document.getElementById('overlay-btn')!.addEventListener('click', onRestart);
|
||||||
document.getElementById('start-btn')!.addEventListener('click', onStart);
|
document.getElementById('start-btn')!.addEventListener('click', onStart);
|
||||||
|
|||||||
Reference in New Issue
Block a user