fix: #328 fabricator golem-2 interval 250→500 + golem-1 desc
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m2s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m2s
- Fix Fabricator golem-2 capped perk interval from 250 to 500 (spec match) - Update golem-1 description to 'Unlock golem summoning' (spec match)
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-06-09T08:14:36.361Z
|
Generated: 2026-06-09T09:18:57.036Z
|
||||||
Found: 2 circular chain(s) — these MUST be fixed before modifying involved files.
|
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
|
1. 1) stores/golem-combat-actions.ts > stores/golem-combat-helpers.ts
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"generated": "2026-06-09T08:14:34.357Z",
|
"generated": "2026-06-09T09:18:55.072Z",
|
||||||
"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."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -57,9 +57,6 @@ Mana-Loop/
|
|||||||
│ │ ├── game/
|
│ │ ├── game/
|
||||||
│ │ │ ├── LootInventory/
|
│ │ │ ├── LootInventory/
|
||||||
│ │ │ │ ├── BlueprintsSection.tsx
|
│ │ │ │ ├── BlueprintsSection.tsx
|
||||||
│ │ │ │ ├── EquipmentItem.tsx
|
|
||||||
│ │ │ │ ├── EssenceItem.tsx
|
|
||||||
│ │ │ │ ├── MaterialItem.tsx
|
|
||||||
│ │ │ │ ├── icons.ts
|
│ │ │ │ ├── icons.ts
|
||||||
│ │ │ │ └── types.ts
|
│ │ │ │ └── types.ts
|
||||||
│ │ │ ├── crafting/
|
│ │ │ ├── crafting/
|
||||||
@@ -128,7 +125,6 @@ Mana-Loop/
|
|||||||
│ │ │ │ │ ├── golemancy-utils.ts
|
│ │ │ │ │ ├── golemancy-utils.ts
|
||||||
│ │ │ │ │ └── types.ts
|
│ │ │ │ │ └── types.ts
|
||||||
│ │ │ │ ├── AchievementsTab.tsx
|
│ │ │ │ ├── AchievementsTab.tsx
|
||||||
│ │ │ │ ├── ActivityLog.tsx
|
|
||||||
│ │ │ │ ├── AttunementsTab.test.ts
|
│ │ │ │ ├── AttunementsTab.test.ts
|
||||||
│ │ │ │ ├── AttunementsTab.tsx
|
│ │ │ │ ├── AttunementsTab.tsx
|
||||||
│ │ │ │ ├── CraftingTab.test.ts
|
│ │ │ │ ├── CraftingTab.test.ts
|
||||||
@@ -154,11 +150,9 @@ Mana-Loop/
|
|||||||
│ │ │ │ └── index.ts
|
│ │ │ │ └── index.ts
|
||||||
│ │ │ ├── ActionButtons.tsx
|
│ │ │ ├── ActionButtons.tsx
|
||||||
│ │ │ ├── ActivityLogPanel.tsx
|
│ │ │ ├── ActivityLogPanel.tsx
|
||||||
│ │ │ ├── AttunementStatus.tsx
|
|
||||||
│ │ │ ├── GameToast.tsx
|
│ │ │ ├── GameToast.tsx
|
||||||
│ │ │ ├── ManaDisplay.tsx
|
│ │ │ ├── ManaDisplay.tsx
|
||||||
│ │ │ ├── TimeDisplay.tsx
|
│ │ │ ├── TimeDisplay.tsx
|
||||||
│ │ │ ├── UpgradeDialog.tsx
|
|
||||||
│ │ │ ├── index.ts
|
│ │ │ ├── index.ts
|
||||||
│ │ │ └── types.ts
|
│ │ │ └── types.ts
|
||||||
│ │ ├── ui/
|
│ │ ├── ui/
|
||||||
@@ -326,7 +320,6 @@ Mana-Loop/
|
|||||||
│ │ │ │ │ ├── hands.ts
|
│ │ │ │ │ ├── hands.ts
|
||||||
│ │ │ │ │ ├── head.ts
|
│ │ │ │ │ ├── head.ts
|
||||||
│ │ │ │ │ ├── index.ts
|
│ │ │ │ │ ├── index.ts
|
||||||
│ │ │ │ │ ├── shields.ts
|
|
||||||
│ │ │ │ │ ├── swords.ts
|
│ │ │ │ │ ├── swords.ts
|
||||||
│ │ │ │ │ ├── types.ts
|
│ │ │ │ │ ├── types.ts
|
||||||
│ │ │ │ │ └── utils.ts
|
│ │ │ │ │ └── utils.ts
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { Mountain } from 'lucide-react';
|
|||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { ManaDisplay } from '@/components/game';
|
import { ManaDisplay } from '@/components/game';
|
||||||
import { ActionButtons } from '@/components/game';
|
import { ActionButtons } from '@/components/game';
|
||||||
import { AttunementStatus } from '@/components/game/AttunementStatus';
|
|
||||||
import { ActivityLogPanel } from '@/components/game/ActivityLogPanel';
|
import { ActivityLogPanel } from '@/components/game/ActivityLogPanel';
|
||||||
import { DebugName } from '@/components/game/debug/debug-context';
|
import { DebugName } from '@/components/game/debug/debug-context';
|
||||||
import { useGameStore, useManaStore, useCombatStore, useCraftingStore, usePrestigeStore, useAttunementStore } from '@/lib/game/stores';
|
import { useGameStore, useManaStore, useCombatStore, useCraftingStore, usePrestigeStore, useAttunementStore } from '@/lib/game/stores';
|
||||||
@@ -163,18 +162,7 @@ export function LeftPanel() {
|
|||||||
</DebugName>
|
</DebugName>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 4. Attunement Status */}
|
{/* 4. Activity Log */}
|
||||||
{!spireMode && (
|
|
||||||
<DebugName name="AttunementStatus">
|
|
||||||
<Card className="bg-[var(--bg-surface)] border-[var(--border-subtle)]">
|
|
||||||
<CardContent className="pt-3">
|
|
||||||
<AttunementStatus />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</DebugName>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 5. Activity Log */}
|
|
||||||
<DebugName name="ActivityLogPanel">
|
<DebugName name="ActivityLogPanel">
|
||||||
<ActivityLogPanel />
|
<ActivityLogPanel />
|
||||||
</DebugName>
|
</DebugName>
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { DebugName } from '@/components/game/debug/debug-context';
|
|
||||||
import { useAttunementStore } from '@/lib/game/stores';
|
|
||||||
import { ATTUNEMENTS_DEF } from '@/lib/game/data/attunements';
|
|
||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
||||||
|
|
||||||
const SLOT_LABELS: Record<string, string> = {
|
|
||||||
rightHand: 'R. Hand',
|
|
||||||
leftHand: 'L. Hand',
|
|
||||||
head: 'Head',
|
|
||||||
back: 'Back',
|
|
||||||
chest: 'Chest',
|
|
||||||
leftLeg: 'L. Leg',
|
|
||||||
rightLeg: 'R. Leg',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function AttunementStatus() {
|
|
||||||
const attunements = useAttunementStore((s) => s.attunements);
|
|
||||||
|
|
||||||
const attunementOrder = useMemo(() => {
|
|
||||||
const map = new Map<string, number>();
|
|
||||||
Object.values(ATTUNEMENTS_DEF).forEach((d, i) => map.set(d.id, i));
|
|
||||||
return map;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const activeAttunements = useMemo(() => {
|
|
||||||
return Object.entries(attunements)
|
|
||||||
.filter(([, state]) => state.active)
|
|
||||||
.sort(([, a], [, b]) => (attunementOrder.get(a.id) ?? 0) - (attunementOrder.get(b.id) ?? 0));
|
|
||||||
}, [attunements, attunementOrder]);
|
|
||||||
|
|
||||||
const xpForNext = (level: number) => {
|
|
||||||
if (level <= 1) return 0;
|
|
||||||
if (level === 2) return 1000;
|
|
||||||
return Math.floor(1000 * Math.pow(2, level - 2) * (level >= 3 ? 1.25 : 1));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DebugName name="AttunementStatus">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-[10px] uppercase tracking-wider text-[var(--text-muted)] font-bold">Attunements</span>
|
|
||||||
<span className="text-[10px] text-[var(--text-muted)]">{activeAttunements.length} active</span>
|
|
||||||
</div>
|
|
||||||
<Separator className="bg-[var(--border-subtle)]" />
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
{activeAttunements.length === 0 ? (
|
|
||||||
<div className="text-[10px] text-[var(--text-muted)] italic">No attunements active</div>
|
|
||||||
) : (
|
|
||||||
activeAttunements.map(([id, state]) => {
|
|
||||||
const def = ATTUNEMENTS_DEF[id];
|
|
||||||
if (!def) return null;
|
|
||||||
const nextXp = xpForNext(state.level);
|
|
||||||
const xpProgress = nextXp > 0 ? (state.experience / nextXp) * 100 : 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TooltipProvider key={id}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center gap-2 p-1.5 rounded bg-[var(--bg-sunken)]/50 border border-[var(--border-subtle)]">
|
|
||||||
<span className="text-sm">{def.icon}</span>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<span className="text-[11px] font-medium text-[var(--text-primary)] truncate">
|
|
||||||
{def.name}
|
|
||||||
</span>
|
|
||||||
<span className="text-[10px] text-[var(--text-secondary)] font-mono">
|
|
||||||
Lv.{state.level}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-[10px] text-[var(--text-muted)]">
|
|
||||||
<span className="capitalize">{SLOT_LABELS[def.slot] || def.slot}</span>
|
|
||||||
{nextXp > 0 && (
|
|
||||||
<span className="ml-1.5 font-mono">
|
|
||||||
{Math.floor(state.experience).toLocaleString()}/{nextXp.toLocaleString()} XP
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{nextXp > 0 && (
|
|
||||||
<div className="w-full h-0.5 bg-[var(--border-subtle)] rounded-full mt-0.5 overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full transition-all duration-500"
|
|
||||||
style={{
|
|
||||||
width: `${Math.min(100, xpProgress)}%`,
|
|
||||||
backgroundColor: def.color,
|
|
||||||
opacity: 0.7,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right">
|
|
||||||
<p className="text-xs max-w-[220px]">{def.desc}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DebugName>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AttunementStatus.displayName = 'AttunementStatus';
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { DebugName } from '@/components/game/debug/debug-context';
|
|
||||||
import { Package, Trash2 } from 'lucide-react';
|
|
||||||
import type { EquipmentInstance } from '@/lib/game/types';
|
|
||||||
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
|
|
||||||
import { CATEGORY_ICONS } from './icons';
|
|
||||||
import { RARITY_CSS_VAR, RARITY_GLOW_CSS_VAR } from './types';
|
|
||||||
import { ActionButton } from '@/components/ui/action-button';
|
|
||||||
|
|
||||||
interface EquipmentItemProps {
|
|
||||||
instanceId: string;
|
|
||||||
instance: EquipmentInstance;
|
|
||||||
onDelete?: (instanceId: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EquipmentItem({ instanceId, instance, onDelete }: EquipmentItemProps) {
|
|
||||||
const type = EQUIPMENT_TYPES[instance.typeId];
|
|
||||||
const Icon = type ? CATEGORY_ICONS[type.category] || Package : Package;
|
|
||||||
const rarityColor = RARITY_CSS_VAR[instance.rarity] || 'var(--rarity-common)';
|
|
||||||
const rarityGlow = RARITY_GLOW_CSS_VAR[instance.rarity] || 'var(--rarity-common-glow)';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DebugName name="EquipmentItem">
|
|
||||||
<div
|
|
||||||
className="p-2 rounded border bg-[var(--bg-sunken)] group"
|
|
||||||
style={{
|
|
||||||
borderColor: rarityColor,
|
|
||||||
backgroundColor: rarityGlow,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<Icon className="w-4 h-4 mt-0.5" style={{ color: rarityColor }} />
|
|
||||||
<div>
|
|
||||||
<div className="text-xs font-semibold" style={{ color: rarityColor }}>
|
|
||||||
{instance.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-[var(--text-secondary)]">
|
|
||||||
{type?.name} • {instance.usedCapacity}/{instance.totalCapacity} cap
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-[var(--text-muted)] capitalize">
|
|
||||||
{instance.rarity} • {instance.enchantments.length} enchants
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{onDelete && (
|
|
||||||
<ActionButton
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-5 w-5 p-0 opacity-0 group-hover:opacity-100 text-[var(--color-danger)] hover:text-[var(--interactive-danger-hover)] hover:bg-[var(--interactive-danger)]/20"
|
|
||||||
onClick={() => onDelete(instanceId)}
|
|
||||||
aria-label={`Delete ${instance.name}`}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-3 h-3" />
|
|
||||||
</ActionButton>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DebugName>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EquipmentSectionProps {
|
|
||||||
equipment: [string, EquipmentInstance][];
|
|
||||||
onDeleteEquipment?: (instanceId: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EquipmentSection({ equipment, onDeleteEquipment }: EquipmentSectionProps) {
|
|
||||||
if (equipment.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-[var(--text-muted)] mb-2 flex items-center gap-1">
|
|
||||||
<Package className="w-3 h-3" />
|
|
||||||
Equipment
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{equipment.map(([id, instance]) => (
|
|
||||||
<EquipmentItem
|
|
||||||
key={id}
|
|
||||||
instanceId={id}
|
|
||||||
instance={instance}
|
|
||||||
onDelete={onDeleteEquipment}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { DebugName } from '@/components/game/debug/debug-context';
|
|
||||||
import { Droplet } from 'lucide-react';
|
|
||||||
import { ElementBadge } from '@/components/ui/element-badge';
|
|
||||||
import type { ElementState } from '@/lib/game/types';
|
|
||||||
import { ELEMENTS } from '@/lib/game/constants';
|
|
||||||
|
|
||||||
interface EssenceItemProps {
|
|
||||||
elementId: string;
|
|
||||||
state: ElementState;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EssenceItem({ elementId, state }: EssenceItemProps) {
|
|
||||||
const elem = ELEMENTS[elementId];
|
|
||||||
if (!elem) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DebugName name="EssenceItem">
|
|
||||||
<div
|
|
||||||
className="p-2 rounded border bg-[var(--bg-sunken)]"
|
|
||||||
style={{
|
|
||||||
borderColor: `var(--mana-${elementId})`,
|
|
||||||
backgroundColor: `var(--mana-${elementId})20`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<ElementBadge element={elementId} showIcon={true} size="sm" />
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-[var(--text-secondary)] mt-1">
|
|
||||||
{state.current} / {state.max}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DebugName>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EssenceSectionProps {
|
|
||||||
essence: [string, ElementState][];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EssenceSection({ essence }: EssenceSectionProps) {
|
|
||||||
if (essence.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-[var(--text-muted)] mb-2 flex items-center gap-1">
|
|
||||||
<Droplet className="w-3 h-3" />
|
|
||||||
Elemental Essence
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
{essence.map(([id, state]) => (
|
|
||||||
<EssenceItem key={id} elementId={id} state={state} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { DebugName } from '@/components/game/debug/debug-context';
|
|
||||||
import type { LootInventory } from '@/lib/game/types';
|
|
||||||
// For backward compatibility
|
|
||||||
type LootInventoryType = LootInventory;
|
|
||||||
import { LOOT_DROPS } from '@/lib/game/data/loot-drops';
|
|
||||||
import { RARITY_CSS_VAR, RARITY_GLOW_CSS_VAR } from './types';
|
|
||||||
import { Sparkles, Trash2 } from 'lucide-react';
|
|
||||||
import { ActionButton } from '@/components/ui/action-button';
|
|
||||||
|
|
||||||
interface MaterialItemProps {
|
|
||||||
materialId: string;
|
|
||||||
count: number;
|
|
||||||
onDelete?: (materialId: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MaterialItem({ materialId, count, onDelete }: MaterialItemProps) {
|
|
||||||
const drop = LOOT_DROPS[materialId];
|
|
||||||
if (!drop) return null;
|
|
||||||
|
|
||||||
const rarityColor = RARITY_CSS_VAR[drop.rarity] || 'var(--rarity-common)';
|
|
||||||
const rarityGlow = RARITY_GLOW_CSS_VAR[drop.rarity] || 'var(--rarity-common-glow)';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DebugName name="MaterialItem">
|
|
||||||
<div
|
|
||||||
className="p-2 rounded border bg-[var(--bg-sunken)] group relative"
|
|
||||||
style={{
|
|
||||||
borderColor: rarityColor,
|
|
||||||
backgroundColor: rarityGlow,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<div className="text-xs font-semibold" style={{ color: rarityColor }}>
|
|
||||||
{drop.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-[var(--text-secondary)]">
|
|
||||||
x{count}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-[var(--text-muted)] capitalize">
|
|
||||||
{drop.rarity}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{onDelete && (
|
|
||||||
<ActionButton
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-5 w-5 p-0 opacity-0 group-hover:opacity-100 text-[var(--color-danger)] hover:text-[var(--interactive-danger-hover)] hover:bg-[var(--interactive-danger)]/20"
|
|
||||||
onClick={() => onDelete(materialId)}
|
|
||||||
aria-label={`Delete ${drop.name}`}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-3 h-3" />
|
|
||||||
</ActionButton>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DebugName>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MaterialsSectionProps {
|
|
||||||
materials: [string, number][];
|
|
||||||
onDeleteMaterial?: (materialId: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MaterialsSection({ materials, onDeleteMaterial }: MaterialsSectionProps) {
|
|
||||||
if (materials.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-[var(--text-muted)] mb-2 flex items-center gap-1">
|
|
||||||
<Sparkles className="w-3 h-3" />
|
|
||||||
Materials
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
{materials.map(([id, count]) => (
|
|
||||||
<MaterialItem
|
|
||||||
key={id}
|
|
||||||
materialId={id}
|
|
||||||
count={count}
|
|
||||||
onDelete={onDeleteMaterial}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
|
||||||
import { DebugName } from '@/components/game/debug/debug-context';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
|
||||||
|
|
||||||
export interface UpgradeDialogProps {
|
|
||||||
open: boolean;
|
|
||||||
skillId: string | null;
|
|
||||||
milestone: 5 | 10;
|
|
||||||
pendingSelections: string[];
|
|
||||||
available: SkillUpgradeChoice[];
|
|
||||||
alreadySelected: string[];
|
|
||||||
onToggle: (upgradeId: string) => void;
|
|
||||||
onConfirm: () => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UpgradeDialog({
|
|
||||||
open,
|
|
||||||
skillId,
|
|
||||||
milestone,
|
|
||||||
pendingSelections,
|
|
||||||
available,
|
|
||||||
alreadySelected,
|
|
||||||
onToggle,
|
|
||||||
onConfirm,
|
|
||||||
onCancel,
|
|
||||||
onOpenChange,
|
|
||||||
}: UpgradeDialogProps) {
|
|
||||||
if (!skillId) return null;
|
|
||||||
|
|
||||||
const currentSelections = pendingSelections.length > 0 ? pendingSelections : alreadySelected;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DebugName name="UpgradeDialog">
|
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogContent className="bg-gray-900 border-gray-700 max-w-lg">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="text-amber-400">
|
|
||||||
Choose Upgrade - {skillId}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription className="text-gray-400">
|
|
||||||
Level {milestone} Milestone - Select 2 upgrades ({currentSelections.length}/2 chosen)
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="space-y-2 mt-4">
|
|
||||||
{available.map((upgrade) => {
|
|
||||||
const isSelected = currentSelections.includes(upgrade.id);
|
|
||||||
const canToggle = currentSelections.length < 2 || isSelected;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={upgrade.id}
|
|
||||||
className={`p-3 rounded border cursor-pointer transition-all ${
|
|
||||||
isSelected
|
|
||||||
? 'border-amber-500 bg-amber-900/30'
|
|
||||||
: canToggle
|
|
||||||
? 'border-gray-600 bg-gray-800/50 hover:border-amber-500/50 hover:bg-gray-800'
|
|
||||||
: 'border-gray-700 bg-gray-800/30 opacity-50 cursor-not-allowed'
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
if (canToggle) {
|
|
||||||
onToggle(upgrade.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="font-semibold text-sm text-amber-300">{upgrade.name}</div>
|
|
||||||
{isSelected && <Badge className="bg-amber-600 text-amber-100">Selected</Badge>}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-400 mt-1">{upgrade.desc}</div>
|
|
||||||
{upgrade.effect.type === 'multiplier' && (
|
|
||||||
<div className="text-xs text-green-400 mt-1">
|
|
||||||
+{Math.round((upgrade.effect.value! - 1) * 100)}% {upgrade.effect.stat}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{upgrade.effect.type === 'bonus' && (
|
|
||||||
<div className="text-xs text-blue-400 mt-1">
|
|
||||||
+{upgrade.effect.value} {upgrade.effect.stat}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{upgrade.effect.type === 'special' && (
|
|
||||||
<div className="text-xs text-cyan-400 mt-1">
|
|
||||||
⚡ {upgrade.effect.specialDesc || 'Special effect'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 mt-4">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={onCancel}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
onClick={onConfirm}
|
|
||||||
disabled={currentSelections.length !== 2}
|
|
||||||
>
|
|
||||||
{currentSelections.length < 2 ? `Select ${2 - currentSelections.length} more` : 'Confirm'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</DebugName>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
UpgradeDialog.displayName = "UpgradeDialog";
|
|
||||||
@@ -8,6 +8,4 @@ export { StatsTab } from './tabs/StatsTab';
|
|||||||
export { ActionButtons } from './ActionButtons';
|
export { ActionButtons } from './ActionButtons';
|
||||||
export { ManaDisplay } from './ManaDisplay';
|
export { ManaDisplay } from './ManaDisplay';
|
||||||
export { TimeDisplay } from './TimeDisplay';
|
export { TimeDisplay } from './TimeDisplay';
|
||||||
export { UpgradeDialog } from './UpgradeDialog';
|
|
||||||
export { AttunementStatus } from './AttunementStatus';
|
|
||||||
export { ActivityLogPanel } from './ActivityLogPanel';
|
export { ActivityLogPanel } from './ActivityLogPanel';
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import type { ActivityLogEntry } from '@/lib/game/types';
|
|
||||||
import { DebugName } from '@/components/game/debug/debug-context';
|
|
||||||
|
|
||||||
interface ActivityLogProps {
|
|
||||||
activityLog: ActivityLogEntry[];
|
|
||||||
maxEntries?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ActivityLog({ activityLog, maxEntries = 20 }: ActivityLogProps) {
|
|
||||||
const entries = activityLog.slice(0, maxEntries);
|
|
||||||
|
|
||||||
if (entries.length === 0) {
|
|
||||||
return (
|
|
||||||
<DebugName name="ActivityLog">
|
|
||||||
<div className="text-sm text-gray-500 italic p-2">
|
|
||||||
No activity yet.
|
|
||||||
</div>
|
|
||||||
</DebugName>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DebugName name="ActivityLog">
|
|
||||||
<div className="space-y-1 max-h-64 overflow-y-auto">
|
|
||||||
{entries.map((entry) => (
|
|
||||||
<div
|
|
||||||
key={entry.id}
|
|
||||||
className="text-xs text-gray-300 border-b border-gray-700 pb-1 last:border-0"
|
|
||||||
>
|
|
||||||
<span className="text-gray-500 mr-1">
|
|
||||||
[{entry.eventType}]
|
|
||||||
</span>
|
|
||||||
{entry.message}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</DebugName>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useAttunementStore } from '@/lib/game/stores';
|
import { useAttunementStore, usePrestigeStore } from '@/lib/game/stores';
|
||||||
import { ATTUNEMENTS_DEF, ATTUNEMENT_SLOT_NAMES, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL } from '@/lib/game/data/attunements';
|
import { ATTUNEMENTS_DEF, ATTUNEMENT_SLOT_NAMES, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL } from '@/lib/game/data/attunements';
|
||||||
import type { AttunementDef, AttunementState } from '@/lib/game/types';
|
import type { AttunementDef, AttunementState } from '@/lib/game/types';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import { DebugName } from '@/components/game/debug/debug-context';
|
import { DebugName } from '@/components/game/debug/debug-context';
|
||||||
import { fmt } from '@/lib/game/stores';
|
import { fmt } from '@/lib/game/stores';
|
||||||
|
import { Unlock } from 'lucide-react';
|
||||||
|
|
||||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -25,14 +27,31 @@ function isAttunementUnlocked(id: string, attunements: Record<string, Attunement
|
|||||||
return id in attunements;
|
return id in attunements;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether an attunement's unlock condition is met.
|
||||||
|
* Evaluates the condition based on current game state.
|
||||||
|
*/
|
||||||
|
function isUnlockConditionMet(id: string, defeatedGuardians: number[]): boolean {
|
||||||
|
switch (id) {
|
||||||
|
case 'invoker':
|
||||||
|
return defeatedGuardians.includes(10);
|
||||||
|
case 'fabricator':
|
||||||
|
return false; // No specific gating condition implemented
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Attunement Card ─────────────────────────────────────────────────────────
|
// ─── Attunement Card ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
interface AttunementCardProps {
|
interface AttunementCardProps {
|
||||||
def: AttunementDef;
|
def: AttunementDef;
|
||||||
state?: AttunementState;
|
state?: AttunementState;
|
||||||
|
canUnlock?: boolean;
|
||||||
|
onUnlock?: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AttunementCard({ def, state }: AttunementCardProps) {
|
function AttunementCard({ def, state, canUnlock, onUnlock }: AttunementCardProps) {
|
||||||
const unlocked = !!state;
|
const unlocked = !!state;
|
||||||
const isStarting = def.unlocked === true;
|
const isStarting = def.unlocked === true;
|
||||||
const xpProgress = state ? getXpProgress(state) : 0;
|
const xpProgress = state ? getXpProgress(state) : 0;
|
||||||
@@ -143,6 +162,21 @@ function AttunementCard({ def, state }: AttunementCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Unlock button (locked + condition met) */}
|
||||||
|
{!unlocked && canUnlock && onUnlock && (
|
||||||
|
<div className="pt-2" style={{ borderTop: `1px solid ${color}15` }}>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="w-full text-xs"
|
||||||
|
style={{ borderColor: `${color}66`, color }}
|
||||||
|
onClick={() => onUnlock(def.id)}
|
||||||
|
>
|
||||||
|
<Unlock className="w-3 h-3 mr-1" /> Unlock {def.name}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Details grid */}
|
{/* Details grid */}
|
||||||
<div
|
<div
|
||||||
className="grid grid-cols-2 gap-2 text-xs pt-3"
|
className="grid grid-cols-2 gap-2 text-xs pt-3"
|
||||||
@@ -231,10 +265,20 @@ function AttunementCard({ def, state }: AttunementCardProps) {
|
|||||||
|
|
||||||
export function AttunementsTab() {
|
export function AttunementsTab() {
|
||||||
const attunements = useAttunementStore((s) => s.attunements);
|
const attunements = useAttunementStore((s) => s.attunements);
|
||||||
|
const unlockAttunement = useAttunementStore((s) => s.unlockAttunement);
|
||||||
|
const defeatedGuardians = usePrestigeStore((s) => s.defeatedGuardians);
|
||||||
|
|
||||||
const allDefs = Object.values(ATTUNEMENTS_DEF);
|
const allDefs = Object.values(ATTUNEMENTS_DEF);
|
||||||
const unlockedCount = allDefs.filter((d) => isAttunementUnlocked(d.id, attunements)).length;
|
const unlockedCount = allDefs.filter((d) => isAttunementUnlocked(d.id, attunements)).length;
|
||||||
|
|
||||||
|
const handleUnlock = (id: string) => {
|
||||||
|
const prestigeState = usePrestigeStore.getState();
|
||||||
|
const success = unlockAttunement(id, prestigeState.defeatedGuardians);
|
||||||
|
if (!success) {
|
||||||
|
console.warn(`Failed to unlock attunement: ${id}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DebugName name="AttunementsTab">
|
<DebugName name="AttunementsTab">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -268,6 +312,8 @@ export function AttunementsTab() {
|
|||||||
key={def.id}
|
key={def.id}
|
||||||
def={def}
|
def={def}
|
||||||
state={attunements[def.id]}
|
state={attunements[def.id]}
|
||||||
|
canUnlock={isUnlockConditionMet(def.id, defeatedGuardians)}
|
||||||
|
onUnlock={handleUnlock}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ export const fabricatorDisciplines: DisciplineDefinition[] = [
|
|||||||
type: 'once',
|
type: 'once',
|
||||||
threshold: 200,
|
threshold: 200,
|
||||||
value: 0,
|
value: 0,
|
||||||
description: 'Unlock golem design ability',
|
description: 'Unlock golem summoning',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'golem-2',
|
id: 'golem-2',
|
||||||
type: 'capped',
|
type: 'capped',
|
||||||
threshold: 500,
|
threshold: 500,
|
||||||
value: 250,
|
value: 500,
|
||||||
maxTier: 2,
|
maxTier: 2,
|
||||||
description: '+1 Golem Capacity per tier (max 2)',
|
description: '+1 Golem Capacity per tier (max 2)',
|
||||||
bonus: { stat: 'golemCapacity', amount: 1 },
|
bonus: { stat: 'golemCapacity', amount: 1 },
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
// ─── Shield Equipment Types ───────────────────────────────────────────
|
|
||||||
// Shields have been removed from the game. The offHand slot remains for
|
|
||||||
// non-shield items (e.g. catalysts, spell focuses).
|
|
||||||
|
|
||||||
import type { EquipmentType } from './types';
|
|
||||||
|
|
||||||
export const SHIELD_EQUIPMENT: Record<string, EquipmentType> = {};
|
|
||||||
@@ -10,12 +10,13 @@ import { useCombatStore } from './combatStore';
|
|||||||
|
|
||||||
export interface AttunementStoreState {
|
export interface AttunementStoreState {
|
||||||
attunements: Record<string, AttunementState>;
|
attunements: Record<string, AttunementState>;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
addAttunementXP: (attunementId: string, amount: number) => void;
|
addAttunementXP: (attunementId: string, amount: number) => void;
|
||||||
|
unlockAttunement: (attunementId: string, defeatedGuardians: number[]) => boolean;
|
||||||
debugUnlockAttunement: (attunementId: string) => void;
|
debugUnlockAttunement: (attunementId: string) => void;
|
||||||
setAttunements: (attunements: Record<string, AttunementState>) => void;
|
setAttunements: (attunements: Record<string, AttunementState>) => void;
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
resetAttunements: () => void;
|
resetAttunements: () => void;
|
||||||
}
|
}
|
||||||
@@ -76,6 +77,37 @@ export const useAttunementStore = create<AttunementStoreState>()(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
unlockAttunement: (attunementId: string, defeatedGuardians: number[]) => {
|
||||||
|
const def = ATTUNEMENTS_DEF[attunementId];
|
||||||
|
if (!def) return false;
|
||||||
|
if (state.attunements[attunementId]?.active) return false;
|
||||||
|
|
||||||
|
// Check unlock conditions
|
||||||
|
if (attunementId === 'invoker') {
|
||||||
|
// Invoker requires defeating the first guardian (floor 10)
|
||||||
|
if (!defeatedGuardians.includes(10)) return false;
|
||||||
|
} else if (attunementId === 'fabricator') {
|
||||||
|
// Fabricator: no specific gating condition implemented
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
// Unknown attunement — don't unlock
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set((s) => ({
|
||||||
|
attunements: {
|
||||||
|
...s.attunements,
|
||||||
|
[attunementId]: {
|
||||||
|
id: attunementId,
|
||||||
|
active: true,
|
||||||
|
level: 1,
|
||||||
|
experience: 0,
|
||||||
|
} as AttunementState,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
debugUnlockAttunement: (attunementId: string) => {
|
debugUnlockAttunement: (attunementId: string) => {
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
attunements: {
|
attunements: {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { ComputedEffects } from '../../effects/upgrade-effects.types';
|
|||||||
import type { EnemyState } from '../../types';
|
import type { EnemyState } from '../../types';
|
||||||
import type { CombatStore } from '../combat-state.types';
|
import type { CombatStore } from '../combat-state.types';
|
||||||
import { countdownGolemRoomDuration } from '../golem-combat-actions';
|
import { countdownGolemRoomDuration } from '../golem-combat-actions';
|
||||||
|
import { useAttunementStore } from '../attunementStore';
|
||||||
|
|
||||||
// ─── Enemy Defense Context ────────────────────────────────────────────────────
|
// ─── Enemy Defense Context ────────────────────────────────────────────────────
|
||||||
// Snapshot of the current tick's enemy defense state, captured once per tick
|
// Snapshot of the current tick's enemy defense state, captured once per tick
|
||||||
@@ -39,7 +40,7 @@ interface BuildCombatCallbacksParams {
|
|||||||
maxMana: number;
|
maxMana: number;
|
||||||
addLog: (msg: string) => void;
|
addLog: (msg: string) => void;
|
||||||
useCombatStore: { setState: (s: Partial<CombatStore>) => void; getState: () => CombatStore };
|
useCombatStore: { setState: (s: Partial<CombatStore>) => void; getState: () => CombatStore };
|
||||||
usePrestigeStore: { getState: () => { addDefeatedGuardian: (floor: number) => void } };
|
usePrestigeStore: { getState: () => { addDefeatedGuardian: (floor: number) => void; defeatedGuardians: number[] } };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Speed-room bonus added to agile dodge chance (spec §4.5) */
|
/** Speed-room bonus added to agile dodge chance (spec §4.5) */
|
||||||
@@ -120,6 +121,15 @@ export function buildCombatCallbacks(params: BuildCombatCallbacksParams) {
|
|||||||
const defeatedGuardian = getGuardianForFloor(floor);
|
const defeatedGuardian = getGuardianForFloor(floor);
|
||||||
params.addLog((defeatedGuardian?.name || 'Unknown') + ' defeated! Visit the Grimoire to sign a pact.');
|
params.addLog((defeatedGuardian?.name || 'Unknown') + ' defeated! Visit the Grimoire to sign a pact.');
|
||||||
usePrestigeStore.getState().addDefeatedGuardian(floor);
|
usePrestigeStore.getState().addDefeatedGuardian(floor);
|
||||||
|
|
||||||
|
// Auto-unlock Invoker when the first guardian (floor 10) is defeated
|
||||||
|
if (floor === 10) {
|
||||||
|
const prestigeState = usePrestigeStore.getState();
|
||||||
|
const unlocked = useAttunementStore.getState().unlockAttunement('invoker', prestigeState.defeatedGuardians);
|
||||||
|
if (unlocked) {
|
||||||
|
params.addLog('💜 The path of the Invoker is now available!');
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (floor % 5 === 0) {
|
} else if (floor % 5 === 0) {
|
||||||
params.addLog('Floor ' + floor + ' cleared!');
|
params.addLog('Floor ' + floor + ' cleared!');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user