feat: unify guardian system — merge static GUARDIANS with extended procedural guardians in Pacts tab

- guardian-encounters.ts: add getGuardianForFloor() and getAllGuardianFloors()
  unified lookup functions that merge static GUARDIANS (floors 10-100) with
  extended system (compound 110, exotic 120-140, combo 150+)
- GuardianPactsTab.tsx: use unified system, update tiers to cover all floors
  (Early 10-40, Mid 50-80, Late 90-100, Compound 110, Exotic 120-140,
  Transcendent 150+)
- guardian-pacts-components.tsx: handle combo guardians with dual-element
  display (symbols + names + '✦ Combo' badge)
- docs/circular-deps.txt, docs/dependency-graph.json: auto-generated updates
- craftingStore.ts: extract initial equipment instances to crafting-initial-state.ts
This commit is contained in:
2026-05-23 13:46:17 +02:00
parent 5bc05ded6f
commit feca7549ad
7 changed files with 171 additions and 78 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
# Circular Dependencies # Circular Dependencies
Generated: 2026-05-22T07:19:25.482Z Generated: 2026-05-22T16:18:31.266Z
Found: 4 circular chain(s) — these MUST be fixed before modifying involved files. Found: 4 circular chain(s) — these MUST be fixed before modifying involved files.
1. Processed 128 files (1.6s) (3 warnings) 1. Processed 129 files (1.5s) (3 warnings)
2. 1) stores/gameStore.ts > stores/gameActions.ts 2. 1) stores/gameStore.ts > stores/gameActions.ts
3. 2) stores/gameStore.ts > stores/gameLoopActions.ts 3. 2) stores/gameStore.ts > stores/gameLoopActions.ts
4. 3) stores/gameStore.ts > stores/tick-pipeline.ts 4. 3) stores/gameStore.ts > stores/tick-pipeline.ts
+8 -2
View File
@@ -1,6 +1,6 @@
{ {
"_meta": { "_meta": {
"generated": "2026-05-22T07:19:23.720Z", "generated": "2026-05-22T16:18:29.615Z",
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.", "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." "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."
}, },
@@ -426,13 +426,18 @@
"utils/room-utils.ts", "utils/room-utils.ts",
"utils/safe-persist.ts" "utils/safe-persist.ts"
], ],
"stores/crafting-initial-state.ts": [
"crafting-utils.ts"
],
"stores/craftingStore.ts": [ "stores/craftingStore.ts": [
"crafting-actions/application-actions.ts", "crafting-actions/application-actions.ts",
"crafting-actions/equipment-actions.ts",
"crafting-actions/preparation-actions.ts", "crafting-actions/preparation-actions.ts",
"crafting-design.ts", "crafting-design.ts",
"crafting-equipment.ts", "crafting-equipment.ts",
"crafting-utils.ts", "crafting-utils.ts",
"stores/combatStore.ts", "stores/combatStore.ts",
"stores/crafting-initial-state.ts",
"stores/craftingStore.types.ts", "stores/craftingStore.types.ts",
"stores/manaStore.ts", "stores/manaStore.ts",
"stores/uiStore.ts", "stores/uiStore.ts",
@@ -442,7 +447,8 @@
"utils/safe-persist.ts" "utils/safe-persist.ts"
], ],
"stores/craftingStore.types.ts": [ "stores/craftingStore.types.ts": [
"types.ts" "types.ts",
"types/equipmentSlot.ts"
], ],
"stores/discipline-slice.ts": [ "stores/discipline-slice.ts": [
"data/disciplines/base.ts", "data/disciplines/base.ts",
+25 -9
View File
@@ -5,7 +5,8 @@ import { useShallow } from 'zustand/react/shallow';
import { usePrestigeStore } from '@/lib/game/stores/prestigeStore'; import { usePrestigeStore } from '@/lib/game/stores/prestigeStore';
import { useManaStore } from '@/lib/game/stores/manaStore'; import { useManaStore } from '@/lib/game/stores/manaStore';
import { useUIStore } from '@/lib/game/stores/uiStore'; import { useUIStore } from '@/lib/game/stores/uiStore';
import { GUARDIANS } from '@/lib/game/constants'; import { getGuardianForFloor, getAllGuardianFloors } from '@/lib/game/data/guardian-encounters';
import type { GuardianDef } from '@/lib/game/types';
import { DebugName } from '@/components/game/debug/debug-context'; import { DebugName } from '@/components/game/debug/debug-context';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { import {
@@ -32,13 +33,19 @@ interface FloorTier {
function groupFloorsByTier(floors: number[]): FloorTier[] { function groupFloorsByTier(floors: number[]): FloorTier[] {
const tiers: FloorTier[] = [ const tiers: FloorTier[] = [
{ label: 'Early Spire (1040)', floors: [] }, { label: 'Early Spire (1040)', floors: [] },
{ label: 'Mid Spire (5060)', floors: [] }, { label: 'Mid Spire (5080)', floors: [] },
{ label: 'Late Spire (80100)', floors: [] }, { label: 'Late Spire (90100)', floors: [] },
{ label: 'Compound (110)', floors: [] },
{ label: 'Exotic (120140)', floors: [] },
{ label: 'Transcendent (150+)', floors: [] },
]; ];
for (const f of floors) { for (const f of floors) {
if (f <= 40) tiers[0].floors.push(f); if (f <= 40) tiers[0].floors.push(f);
else if (f <= 60) tiers[1].floors.push(f); else if (f <= 80) tiers[1].floors.push(f);
else tiers[2].floors.push(f); else if (f <= 100) tiers[2].floors.push(f);
else if (f <= 110) tiers[3].floors.push(f);
else if (f <= 140) tiers[4].floors.push(f);
else tiers[5].floors.push(f);
} }
return tiers.filter(t => t.floors.length > 0); return tiers.filter(t => t.floors.length > 0);
} }
@@ -73,10 +80,19 @@ export const GuardianPactsTab: React.FC = () => {
}, []); }, []);
const guardianFloors = useMemo( const guardianFloors = useMemo(
() => Object.keys(GUARDIANS).map(Number).sort((a, b) => a - b), () => getAllGuardianFloors(),
[], [],
); );
const guardianMap = useMemo(() => {
const map: Record<number, GuardianDef> = {};
for (const floor of guardianFloors) {
const g = getGuardianForFloor(floor);
if (g) map[floor] = g;
}
return map;
}, [guardianFloors]);
const tiers = useMemo(() => groupFloorsByTier(guardianFloors), [guardianFloors]); const tiers = useMemo(() => groupFloorsByTier(guardianFloors), [guardianFloors]);
const filteredFloors = useMemo(() => { const filteredFloors = useMemo(() => {
@@ -86,7 +102,7 @@ export const GuardianPactsTab: React.FC = () => {
}, [activeTier, guardianFloors, tiers]); }, [activeTier, guardianFloors, tiers]);
const handleStartRitual = useCallback((floor: number) => { const handleStartRitual = useCallback((floor: number) => {
const guardian = GUARDIANS[floor]; const guardian = getGuardianForFloor(floor);
if (!guardian) return; if (!guardian) return;
const result = startPactRitual(floor, rawMana); const result = startPactRitual(floor, rawMana);
@@ -100,7 +116,7 @@ export const GuardianPactsTab: React.FC = () => {
const cumulativeBoons = useMemo(() => { const cumulativeBoons = useMemo(() => {
const boonMap: Record<string, number> = {}; const boonMap: Record<string, number> = {};
for (const floor of signedPacts) { for (const floor of signedPacts) {
const guardian = GUARDIANS[floor]; const guardian = getGuardianForFloor(floor);
if (!guardian) continue; if (!guardian) continue;
for (const boon of guardian.boons) { for (const boon of guardian.boons) {
boonMap[boon.type] = (boonMap[boon.type] || 0) + boon.value; boonMap[boon.type] = (boonMap[boon.type] || 0) + boon.value;
@@ -137,7 +153,7 @@ export const GuardianPactsTab: React.FC = () => {
<ScrollArea className="h-[500px] rounded border border-gray-700 p-3"> <ScrollArea className="h-[500px] rounded border border-gray-700 p-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{filteredFloors.map((floor) => { {filteredFloors.map((floor) => {
const guardian = GUARDIANS[floor]; const guardian = guardianMap[floor];
if (!guardian) return null; if (!guardian) return null;
return ( return (
<GuardianCard <GuardianCard
@@ -12,11 +12,32 @@ import clsx from 'clsx';
export type GuardianStatus = 'undefeated' | 'defeated' | 'signed'; export type GuardianStatus = 'undefeated' | 'defeated' | 'signed';
interface FloorTier { export interface FloorTier {
label: string; label: string;
floors: number[]; floors: number[];
} }
// ─── Element Display Helper ──────────────────────────────────────────────────
interface ElementDisplay {
sym: string;
name: string;
color: string;
}
function getElementDisplays(element: string): ElementDisplay[] {
// Combo guardians have elements like "fire+water"
const parts = element.split('+');
return parts.map((el) => {
const def = ELEMENTS[el];
return {
sym: def?.sym ?? '?',
name: def?.name ?? el,
color: def?.color ?? '#888',
};
});
}
// ─── Guardian Card ─────────────────────────────────────────────────────────── // ─── Guardian Card ───────────────────────────────────────────────────────────
interface GuardianCardProps { interface GuardianCardProps {
@@ -40,9 +61,9 @@ export const GuardianCard: React.FC<GuardianCardProps> = React.memo(({
ritualProgress, ritualProgress,
onStartRitual, onStartRitual,
}) => { }) => {
const elemDef = ELEMENTS[guardian.element]; const elemDisplays = getElementDisplays(guardian.element);
const elemColor = elemDef?.color ?? '#888'; const primaryColor = elemDisplays[0]?.color ?? '#888';
const elemSym = elemDef?.sym ?? ''; const isCombo = elemDisplays.length > 1;
const statusConfig: Record<GuardianStatus, { label: string; color: string; bg: string }> = { const statusConfig: Record<GuardianStatus, { label: string; color: string; bg: string }> = {
undefeated: { label: 'Undefeated', color: 'text-gray-400', bg: 'bg-gray-800/50' }, undefeated: { label: 'Undefeated', color: 'text-gray-400', bg: 'bg-gray-800/50' },
@@ -54,6 +75,11 @@ export const GuardianCard: React.FC<GuardianCardProps> = React.memo(({
const ritualTime = guardian.pactTime; const ritualTime = guardian.pactTime;
const ritualComplete = ritualProgress >= ritualTime; const ritualComplete = ritualProgress >= ritualTime;
// Build element label: single element name, or "Fire + Water" for combos
const elementLabel = isCombo
? elemDisplays.map(e => e.name).join(' + ')
: elemDisplays[0]?.name ?? guardian.element;
return ( return (
<Card <Card
className={clsx( className={clsx(
@@ -66,11 +92,18 @@ export const GuardianCard: React.FC<GuardianCardProps> = React.memo(({
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div className="min-w-0"> <div className="min-w-0">
<CardTitle className="text-sm flex items-center gap-2" style={{ color: elemColor }}> <CardTitle className="text-sm flex items-center gap-2" style={{ color: primaryColor }}>
<span>{elemSym}</span> <span className="flex items-center gap-0.5">
{elemDisplays.map((e, i) => (
<span key={i}>{e.sym}</span>
))}
</span>
<span className="truncate">{guardian.name}</span> <span className="truncate">{guardian.name}</span>
</CardTitle> </CardTitle>
<div className="text-xs text-gray-500 mt-0.5">Floor {floor} · {elemDef?.name ?? guardian.element}</div> <div className="text-xs text-gray-500 mt-0.5">
Floor {floor} · {elementLabel}
{isCombo && <span className="ml-1 text-purple-400"> Combo</span>}
</div>
</div> </div>
<Badge className={clsx('text-[10px] px-1.5 py-0 shrink-0', sc.bg, sc.color)}> <Badge className={clsx('text-[10px] px-1.5 py-0 shrink-0', sc.bg, sc.color)}>
{sc.label} {sc.label}
+28 -3
View File
@@ -264,10 +264,35 @@ export function getExtendedGuardian(floor: number): GuardianDef | null {
return null; return null;
} }
// All guardian floors (extended) // ─── Unified Guardian System ─────────────────────────────────────────────────
// Merges the static GUARDIANS constant (floors 10100) with the extended
// procedural system (compound 110, exotic 120140, combo 150+).
// For floors 90100 the static definitions take precedence (canonical names).
import { GUARDIANS } from '../constants/guardians';
/** Get the guardian for any floor, merging static and extended systems. */
export function getGuardianForFloor(floor: number): GuardianDef | null {
// Static GUARDIANS take precedence for floors 10100 (canonical definitions)
if (GUARDIANS[floor]) {
return { ...GUARDIANS[floor] };
}
// Extended system for floors beyond 100 (and compound floors not in GUARDIANS)
return getExtendedGuardian(floor);
}
/** All guardian floors — merged from static + extended. */
export function getAllGuardianFloors(): number[] {
const staticFloors = Object.keys(GUARDIANS).map(Number);
const extendedFloors = [110, 120, 130, 140, ...Array.from({ length: 10 }, (_, i) => 150 + i * 10)];
const all = new Set([...staticFloors, ...extendedFloors]);
return Array.from(all).sort((a, b) => a - b);
}
// All guardian floors (extended — kept for backwards compatibility)
export const ALL_GUARDIAN_FLOORS: number[] = [ export const ALL_GUARDIAN_FLOORS: number[] = [
10, 20, 30, 40, 50, 60, 80, 100, // Original 10, 20, 30, 40, 50, 60, 80, 90, 100, // Original (all static floors)
90, 110, // Compound 110, // Compound (90,100 already in static)
120, 130, 140, // Exotic 120, 130, 140, // Exotic
...Array.from({ length: 10 }, (_, i) => 150 + i * 10), // Combo ...Array.from({ length: 10 }, (_, i) => 150 + i * 10), // Combo
].sort((a, b) => a - b); ].sort((a, b) => a - b);
@@ -0,0 +1,63 @@
// ─── Crafting Store Initial State ─────────────────────────────────────────────
// Helper to generate the starting equipment instances for new games.
import * as CraftingUtils from '../crafting-utils';
export function createInitialEquipmentInstances() {
const staffId = CraftingUtils.generateInstanceId();
const staffInstance = {
instanceId: staffId,
typeId: 'basicStaff',
name: 'Basic Staff',
enchantments: [{ effectId: 'spell_manaBolt', stacks: 1, actualCost: 50 }],
usedCapacity: 50,
totalCapacity: 50,
rarity: 'common',
quality: 100,
tags: [],
};
const shirtId = CraftingUtils.generateInstanceId();
const shirtInstance = {
instanceId: shirtId,
typeId: 'civilianShirt',
name: 'Civilian Shirt',
enchantments: [],
usedCapacity: 0,
totalCapacity: 30,
rarity: 'common',
quality: 100,
tags: [],
};
const shoesId = CraftingUtils.generateInstanceId();
const shoesInstance = {
instanceId: shoesId,
typeId: 'civilianShoes',
name: 'Civilian Shoes',
enchantments: [],
usedCapacity: 0,
totalCapacity: 15,
rarity: 'common',
quality: 100,
tags: [],
};
return {
instances: {
[staffId]: staffInstance,
[shirtId]: shirtInstance,
[shoesId]: shoesInstance,
} as Record<string, typeof staffInstance>,
equippedInstances: {
mainHand: staffId,
offHand: null,
head: null,
body: shirtId,
hands: null,
feet: shoesId,
accessory1: null,
accessory2: null,
} as Record<string, string | null>,
};
}
+4 -54
View File
@@ -17,49 +17,12 @@ import { equipItem as equipItemAction, unequipItem as unequipItemAction } from '
import { ErrorCode } from '../utils/result'; import { ErrorCode } from '../utils/result';
import { createSafeStorage } from '../utils/safe-persist'; import { createSafeStorage } from '../utils/safe-persist';
import type { Result } from '../utils/result'; import type { Result } from '../utils/result';
import { createInitialEquipmentInstances } from './crafting-initial-state';
export const useCraftingStore = create<CraftingStore>()( export const useCraftingStore = create<CraftingStore>()(
persist( persist(
(set, get) => { (set, get) => {
const staffId = CraftingUtils.generateInstanceId(); const initial = createInitialEquipmentInstances();
const staffInstance = {
instanceId: staffId,
typeId: 'basicStaff',
name: 'Basic Staff',
enchantments: [{ effectId: 'spell_manaBolt', stacks: 1, actualCost: 50 }],
usedCapacity: 50,
totalCapacity: 50,
rarity: 'common',
quality: 100,
tags: [],
};
const shirtId = CraftingUtils.generateInstanceId();
const shirtInstance = {
instanceId: shirtId,
typeId: 'civilianShirt',
name: 'Civilian Shirt',
enchantments: [],
usedCapacity: 0,
totalCapacity: 30,
rarity: 'common',
quality: 100,
tags: [],
};
const shoesId = CraftingUtils.generateInstanceId();
const shoesInstance = {
instanceId: shoesId,
typeId: 'civilianShoes',
name: 'Civilian Shoes',
enchantments: [],
usedCapacity: 0,
totalCapacity: 15,
rarity: 'common',
quality: 100,
tags: [],
};
return { return {
// Initial state // Initial state
designProgress: null, designProgress: null,
@@ -69,21 +32,8 @@ export const useCraftingStore = create<CraftingStore>()(
equipmentCraftingProgress: null, equipmentCraftingProgress: null,
enchantmentDesigns: [], enchantmentDesigns: [],
unlockedEffects: [], unlockedEffects: [],
equippedInstances: { equippedInstances: initial.equippedInstances,
mainHand: staffId, equipmentInstances: initial.instances,
offHand: null,
head: null,
body: shirtId,
hands: null,
feet: shoesId,
accessory1: null,
accessory2: null,
},
equipmentInstances: {
[staffId]: staffInstance,
[shirtId]: shirtInstance,
[shoesId]: shoesInstance,
},
lootInventory: { lootInventory: {
materials: {}, materials: {},
blueprints: [], blueprints: [],