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
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:
@@ -1,8 +1,9 @@
|
||||
# Circular Dependencies
|
||||
Generated: 2026-06-08T18:36:58.404Z
|
||||
Found: 1 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||
Generated: 2026-06-08T20:08:33.456Z
|
||||
Found: 2 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||
|
||||
1. 1) stores/golem-combat-actions.ts > stores/golem-combat-helpers.ts
|
||||
2. 2) stores/combatStore.ts > stores/combat-descent-actions.ts > stores/attunementStore.ts
|
||||
|
||||
## How to fix
|
||||
1. Identify which import in the chain can be extracted to a shared types/utils file.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"_meta": {
|
||||
"generated": "2026-06-08T18:36:56.442Z",
|
||||
"generated": "2026-06-08T20:08:31.411Z",
|
||||
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
||||
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
||||
},
|
||||
@@ -539,6 +539,7 @@
|
||||
],
|
||||
"stores/attunementStore.ts": [
|
||||
"data/attunements.ts",
|
||||
"stores/combatStore.ts",
|
||||
"types.ts",
|
||||
"utils/safe-persist.ts"
|
||||
],
|
||||
|
||||
@@ -259,8 +259,8 @@ guardian's `unlocksMana` is derived from `resolveMultiUnlockChain(element)`:
|
||||
| 110 | lightning | `fire`, `air`, `lightning` |
|
||||
| 120 | frost | `air`, `water`, `frost` |
|
||||
| 130 | blackflame | `fire`, `earth`, `metal` |
|
||||
| 140 | radiantflames | `light`, `fire` |
|
||||
| 150 | miasma | `air`, `death` |
|
||||
| 140 | radiantflames | `light`, `fire`, `radiantflames` |
|
||||
| 150 | miasma | `air`, `death`, `miasma` |
|
||||
| 160 | shadowglass | `earth`, `dark` |
|
||||
| 170+ | exotic | varies (see guardian-data.ts) |
|
||||
|
||||
@@ -298,8 +298,8 @@ mana to any elemental type. All elemental mana must come from:
|
||||
| 110 | lightning | 22% | 13h |
|
||||
| 120 | frost | 28% | 14h |
|
||||
| 130 | blackflame | 32% | 15h |
|
||||
| 140 | sand+earth+water | 25% | 16h |
|
||||
| 150 | lightning+fire+air | 28% | 17h |
|
||||
| 140 | light+fire+radiantflames | 25% | 16h |
|
||||
| 150 | air+death+miasma | 28% | 17h |
|
||||
| 160 | shadowglass | 33% | 18h |
|
||||
|
||||
### 8.3 Tier 3 — Exotic Elements (Floors 170–240)
|
||||
|
||||
@@ -273,8 +273,8 @@ describe('getGuardianForFloor (unified lookup)', () => {
|
||||
|
||||
it('should return multi-element guardians for tier 3 floors', () => {
|
||||
expect(getGuardianForFloor(130)!.element).toEqual(['metal', 'fire', 'earth']);
|
||||
expect(getGuardianForFloor(140)!.element).toEqual(['sand', 'earth', 'water']);
|
||||
expect(getGuardianForFloor(150)!.element).toEqual(['lightning', 'fire', 'air']);
|
||||
expect(getGuardianForFloor(140)!.element).toEqual(['light', 'fire', 'radiantflames']);
|
||||
expect(getGuardianForFloor(150)!.element).toEqual(['air', 'death', 'miasma']);
|
||||
});
|
||||
|
||||
it('should return exotic guardians for floors 170-200', () => {
|
||||
|
||||
@@ -165,7 +165,7 @@ const TIER2: Record<number, GuardianDef> = {
|
||||
90: mk(90, '', ['metal'], '#BDC3C7', 0.30, 3.5,
|
||||
[
|
||||
{ type: 'elementalDamage', value: 15, desc: '+15% Metal damage' },
|
||||
{ type: 'maxMana', value: 150, desc: '+150 max mana' },
|
||||
{ type: 'critChance', value: 8, desc: '+8% crit chance' },
|
||||
],
|
||||
'Metal spells pierce 20% armor',
|
||||
[{ type: 'armor_pierce', value: 0.2 }],
|
||||
@@ -183,7 +183,7 @@ const TIER2: Record<number, GuardianDef> = {
|
||||
110: mk(110, '', ['lightning'], '#FFEB3B', 0.22, 4.0,
|
||||
[
|
||||
{ type: 'elementalDamage', value: 15, desc: '+15% Lightning damage' },
|
||||
{ type: 'castingSpeed', value: 15, desc: '+15% casting speed' },
|
||||
{ type: 'manaGain', value: 10, desc: '+10% mana gain' },
|
||||
],
|
||||
'Lightning spells chain to 2 additional targets',
|
||||
[{ type: 'chain', value: 2 }],
|
||||
@@ -207,21 +207,21 @@ const TIER2: Record<number, GuardianDef> = {
|
||||
[{ type: 'curse', value: 0.15 }, { type: 'burn', value: 0.15 }, { type: 'armor_pierce', value: 0.2 }],
|
||||
{ shield: 1000, shieldRegen: 25, healthRegen: 5, healthRegenIsPercent: true },
|
||||
),
|
||||
140: mk(140, '', ['sand', 'earth', 'water'], '#FFAA33', 0.25, 4.75,
|
||||
140: mk(140, '', ['light', 'fire', 'radiantflames'], '#FFAA33', 0.25, 4.75,
|
||||
[
|
||||
{ type: 'elementalDamage', value: 15, desc: '+15% Radiant Earth damage' },
|
||||
{ type: 'elementalDamage', value: 15, desc: '+15% Radiant Flames damage' },
|
||||
{ type: 'maxMana', value: 200, desc: '+200 max mana' },
|
||||
],
|
||||
'Radiant Earth spells blind enemies, reducing their accuracy and damage by 15%',
|
||||
'Radiant Flames spells blind enemies, reducing their accuracy and damage by 15%',
|
||||
[{ type: 'blind', value: 0.15 }, { type: 'armor_pierce', value: 0.1 }],
|
||||
{ barrier: 0.12, barrierRegen: 0.03, healthRegen: 6, healthRegenIsPercent: true },
|
||||
),
|
||||
150: mk(150, '', ['lightning', 'fire', 'air'], '#6B8E23', 0.28, 5.0,
|
||||
150: mk(150, '', ['air', 'death', 'miasma'], '#6B8E23', 0.28, 5.0,
|
||||
[
|
||||
{ type: 'elementalDamage', value: 15, desc: '+15% Storm Lightning damage' },
|
||||
{ type: 'elementalDamage', value: 15, desc: '+15% Miasma damage' },
|
||||
{ type: 'castingSpeed', value: 15, desc: '+15% casting speed' },
|
||||
],
|
||||
'Storm Lightning spells corrode armor and spread chain lightning in swarm rooms',
|
||||
'Miasma spells corrode armor and spread toxic clouds in swarm rooms',
|
||||
[{ type: 'chain', value: 2 }, { type: 'cast_speed', value: 0.1 }, { type: 'burn', value: 0.1 }],
|
||||
{ shield: 1100, shieldRegen: 28, barrier: 0.05, barrierRegen: 0.01 },
|
||||
),
|
||||
@@ -273,7 +273,7 @@ const TIER3: Record<number, GuardianDef> = {
|
||||
200: mk(200, '', ['crystal', 'stellar', 'void'], '#E8D5F5', 0.35, 7.0,
|
||||
[
|
||||
{ type: 'elementalDamage', value: 30, desc: '+30% Exotic damage' },
|
||||
{ type: 'maxMana', value: 400, desc: '+400 max mana' },
|
||||
{ type: 'prestigeInsight', value: 50, desc: '+50 prestige insight' },
|
||||
],
|
||||
'Exotic convergence: Crystal/Stellar/Void spells bypass all defenses and shields',
|
||||
[{ type: 'defense_pierce', value: 0.3 }, { type: 'resist_ignore', value: 0.2 }, { type: 'reflect', value: 0.1 }],
|
||||
@@ -291,7 +291,7 @@ const TIER3: Record<number, GuardianDef> = {
|
||||
220: mk(220, '', ['plasma'], '#FF6B9D', 0.28, 8.0,
|
||||
[
|
||||
{ type: 'elementalDamage', value: 25, desc: '+25% Plasma damage' },
|
||||
{ type: 'manaRegen', value: 2.5, desc: '+2.5 mana regen' },
|
||||
{ type: 'studySpeed', value: 15, desc: '+15% study speed' },
|
||||
],
|
||||
'Plasma spells chain to 3 targets with 30% damage each',
|
||||
[{ type: 'chain', value: 3 }, { type: 'burn', value: 0.1 }],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user