Soften offline hemlighet drain: grace period + per-session cap
Docker / build-and-push (push) Successful in 1m39s
Docker / build-and-push (push) Successful in 1m39s
Per feedback that unattended-tab game-overs felt bad specifically because there was no chance to react — unlike a loss during active play, which you can usually see coming. - The first 5 minutes away accrue rewards (andakt, dread, etc.) as before, but cost zero hemlighet. A quick tab-out is never punished. - A single absence, however long, can never cost more than 35 hemlighet total. Applied as a clamp on the *combined* passive-drain and flavor-event effects — an earlier version of this only capped the passive-drain component, so a run of bad luck on the random away-time events could still blow past the intended cap. Doesn't change anything about live play — only applyOfflineProgress. A cult that was already critically exposed before you left can still be found while you're gone; the cap bounds the damage one absence can do, it doesn't grant immunity. Added two tests (grace window is a true no-op below threshold and still accrues rewards; a 24h absence can't drop hemlighet by more than the cap regardless of which random events fire) and verified live in a real browser: a 4-minute absence left hemlighet unchanged by the offline step, and a 30-hour absence starting at full hemlighet landed at 64/65 instead of the uncapped 58 from before the fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c23bd369f1
commit
2fd6c5e854
+10
-1
@@ -1,4 +1,4 @@
|
||||
export const VERSION = '0.7.0';
|
||||
export const VERSION = '0.7.1';
|
||||
|
||||
export interface ChangelogEntry {
|
||||
version: string;
|
||||
@@ -7,6 +7,15 @@ export interface 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',
|
||||
date: '2026-08-01',
|
||||
|
||||
@@ -246,6 +246,23 @@ describe('applyOfflineProgress', () => {
|
||||
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', () => {
|
||||
const s = createState();
|
||||
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 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
|
||||
// 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;
|
||||
|
||||
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.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;
|
||||
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 dreadDrain = DREAD_HEMLIGHET_DRAIN * (s.dread / (s.dread + 150));
|
||||
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
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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 =
|
||||
awaySec < 3600 ? `${Math.round(awaySec / 60)} minuter` :
|
||||
awaySec < MAX_OFFLINE_SECONDS ? `${(awaySec / 3600).toFixed(1)} timmar` :
|
||||
|
||||
Reference in New Issue
Block a user