fix(pact-system): resolve 5 spec-vs-code discrepancies (DISC-4,6,8,11,12)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s

- DISC-11: Fix floor 140/150 element composition in guardian-data.ts
  Floor 140 now uses [light, fire, radiantflames] (was [sand, earth, water])
  Floor 150 now uses [air, death, miasma] (was [lightning, fire, air])
- DISC-12: Reconcile spec §7.1 vs §8.2 tables in pact-system-spec.md
  Updated §8.2 to match §7.1 authoritative element mappings
- DISC-4: Deduplicate pact ritual completion logic
  Refactored pact-ritual.ts pipeline to delegate completion to
  prestigeStore.completePactRitual() instead of duplicating state writes
- DISC-6: Add 4 missing boon types to guardians
  critChance (floor 90), manaGain (floor 110), prestigeInsight (floor 200),
  studySpeed (floor 220) — all 12 spec boon types now used
- DISC-8: Add comment clarifying signedPactDetails persistence
  Code already correct (not reset by startNewLoop/resetPrestigeForNewLoop)
- Updated spire-utils.test.ts to match corrected floor 140/150 elements
This commit is contained in:
2026-06-08 22:50:03 +02:00
parent 573130cdb1
commit cba3090d7e
8 changed files with 43 additions and 65 deletions
+4 -2
View File
@@ -209,9 +209,11 @@ export const useGameStore = create<GameCoordinatorStore>()(
rawMana = Math.max(0, Math.min(rawMana + netRawRegen * HOURS_PER_TICK, maxMana));
let totalManaGathered = ctx.mana.totalManaGathered + Math.max(0, actualRegen);
const pactResult = processPactRitual(ctx.prestige.pactRitualFloor, ctx.prestige.pactRitualProgress, ctx.prestige.signedPacts, ctx.prestige.defeatedGuardians, ctx.prestige.prestigeUpgrades.pactAffinity || 0, disciplineEffects.bonuses.pactAffinityBonus || 0, ctx.prestige.signedPactDetails, day, hour);
const pactResult = processPactRitual(ctx.prestige.pactRitualFloor, ctx.prestige.pactRitualProgress, ctx.prestige.prestigeUpgrades.pactAffinity || 0, disciplineEffects.bonuses.pactAffinityBonus || 0);
if (pactResult.writes) writes.prestige = { ...(writes.prestige || {}), ...pactResult.writes };
pactResult.logs.forEach(l => addLog(l));
if (pactResult.completed) {
usePrestigeStore.getState().completePactRitual(addLog);
}
const dr = useDisciplineStore.getState().processTick({ rawMana, elements });
rawMana = dr.rawMana; elements = dr.elements;
+16 -44
View File
@@ -1,76 +1,48 @@
// ─── Pact Ritual Pipeline Phase ───────────────────────────────────────────────
// Processes pact ritual signing during the game tick.
// Progress advancement only — completion is delegated to prestigeStore.completePactRitual().
import { useManaStore } from '../manaStore';
import { getGuardianForFloor } from '../../data/guardian-encounters';
import { HOURS_PER_TICK } from '../../constants';
import type { PrestigeState } from '../prestigeStore';
export interface PactRitualResult {
/** Null when no ritual is in progress. */
writes: {
signedPacts?: number[];
defeatedGuardians?: number[];
signedPactDetails?: PrestigeState['signedPactDetails'];
pactRitualFloor: number | null;
pactRitualProgress: number;
} | null;
logs: string[];
/** True when the ritual reached completion threshold this tick. */
completed: boolean;
}
/**
* Process pact ritual progression. Advances progress and completes signing
* when enough enough hours have accumulated.
* Process pact ritual progression. Advances progress each tick.
* Returns `completed: true` when enough hours have accumulated;
* the caller (gameStore tick) must then invoke
* `usePrestigeStore.getState().completePactRitual(addLog)` to
* finalise signing — this avoids duplicating the completion logic.
*/
export function processPactRitual(
pactRitualFloor: number | null,
pactRitualProgress: number,
signedPacts: number[],
defeatedGuardians: number[],
pactAffinityUpgrade: number,
pactAffinityBonus: number,
signedPactDetails: PrestigeState['signedPactDetails'],
currentDay: number,
currentHour: number,
): PactRitualResult {
if (pactRitualFloor === null) return { writes: null, logs: [] };
const logs: string[] = [];
if (pactRitualFloor === null) return { writes: null, completed: false };
const guardian = getGuardianForFloor(pactRitualFloor);
if (!guardian) return { writes: null, logs: [] };
if (!guardian) return { writes: null, completed: false };
const pactAffinity = Math.min(0.9, pactAffinityUpgrade * 0.1 + pactAffinityBonus);
const requiredTime = guardian.pactTime * (1 - pactAffinity);
if (pactRitualProgress + HOURS_PER_TICK >= requiredTime) {
logs.push(`📜 Pact signed with ${guardian.name}! You have gained their boons.`);
const manaStore = useManaStore.getState();
for (const manaType of guardian.unlocksMana || []) {
const result = manaStore.unlockElement(manaType, 0);
if (result.success) {
logs.push(`${manaType.charAt(0).toUpperCase() + manaType.slice(1)} mana unlocked!`);
}
}
return {
writes: {
signedPacts: [...signedPacts, pactRitualFloor],
defeatedGuardians: defeatedGuardians.filter(f => f !== pactRitualFloor),
signedPactDetails: {
...signedPactDetails,
[pactRitualFloor]: {
floor: pactRitualFloor,
guardianId: guardian.name || `floor-${pactRitualFloor}`,
signedAt: { day: currentDay, hour: currentHour },
skillLevels: {},
},
},
pactRitualFloor: null,
pactRitualProgress: 0,
},
logs,
};
// Signal completion — state writes happen inside completePactRitual()
return { writes: null, completed: true };
}
return {
writes: { pactRitualFloor, pactRitualProgress: pactRitualProgress + HOURS_PER_TICK, signedPacts, defeatedGuardians },
logs,
writes: { pactRitualFloor, pactRitualProgress: pactRitualProgress + HOURS_PER_TICK },
completed: false,
};
}
+2
View File
@@ -246,6 +246,8 @@ export const usePrestigeStore = create<PrestigeStore>()(
pactRitualFloor: null,
pactRitualProgress: 0,
loopInsight: 0,
// NOTE: signedPactDetails is intentionally NOT reset here.
// Per spec §5.1, it persists across loops for historical tracking.
});
},