refactor: cleanup codebase — remove hydration guards, extract constants, fix bugs
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m20s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m20s
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useAttunementStore } from '@/lib/game/stores';
|
||||
import { ATTUNEMENTS_DEF } from '@/lib/game/data/attunements';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
@@ -18,13 +19,17 @@ const SLOT_LABELS: Record<string, string> = {
|
||||
export function AttunementStatus() {
|
||||
const attunements = useAttunementStore((s) => s.attunements);
|
||||
|
||||
const activeAttunements = Object.entries(attunements)
|
||||
.filter(([, state]) => state.active)
|
||||
.sort(([, a], [, b]) => {
|
||||
const orderA = Object.values(ATTUNEMENTS_DEF).findIndex(d => d.id === a.id);
|
||||
const orderB = Object.values(ATTUNEMENTS_DEF).findIndex(d => d.id === b.id);
|
||||
return orderA - orderB;
|
||||
});
|
||||
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;
|
||||
|
||||
@@ -67,7 +67,7 @@ export function ManaDisplay({
|
||||
style={{
|
||||
background: 'var(--mana-raw)',
|
||||
border: '1px solid var(--border-accent)',
|
||||
color: '#0C1020',
|
||||
color: 'var(--bg-gather-btn)',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
onMouseDown={onGatherStart}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useCombatStore } from '@/lib/game/stores';
|
||||
import {
|
||||
ACHIEVEMENTS,
|
||||
@@ -164,14 +164,8 @@ function CategorySection({
|
||||
|
||||
export function AchievementsTab() {
|
||||
const achievements = useCombatStore((s) => s.achievements);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [collapsedCategories, setCollapsedCategories] = useState<Record<string, boolean>>({});
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const byCategory = useMemo(() => getAchievementsByCategory(), []);
|
||||
const categories = useMemo(
|
||||
() => Object.keys(byCategory).sort(),
|
||||
@@ -188,14 +182,6 @@ export function AchievementsTab() {
|
||||
}));
|
||||
};
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8 text-gray-500">
|
||||
Loading achievements…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DebugName name="AchievementsTab">
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useAttunementStore } from '@/lib/game/stores';
|
||||
import { ATTUNEMENTS_DEF, ATTUNEMENT_SLOT_NAMES, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL } from '@/lib/game/data/attunements';
|
||||
import type { AttunementDef, AttunementState } from '@/lib/game/types';
|
||||
@@ -157,24 +157,10 @@ function AttunementCard({ def, state }: AttunementCardProps) {
|
||||
|
||||
export function AttunementsTab() {
|
||||
const attunements = useAttunementStore((s) => s.attunements);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const allDefs = Object.values(ATTUNEMENTS_DEF);
|
||||
const unlockedCount = allDefs.filter((d) => isAttunementUnlocked(d.id, attunements)).length;
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8 text-gray-500">
|
||||
Loading attunements…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DebugName name="AttunementsTab">
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { useDisciplineStore } from '@/lib/game/stores/discipline-slice';
|
||||
import type { DisciplineDefinition } from '@/lib/game/types/disciplines';
|
||||
import type { ManaType } from '@/lib/game/types/elements';
|
||||
@@ -201,14 +201,8 @@ export const DisciplinesTab: React.FC = () => {
|
||||
const activate = useDisciplineStore((s) => s.activate);
|
||||
const deactivate = useDisciplineStore((s) => s.deactivate);
|
||||
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [activeAttunement, setActiveAttunement] = useState<string>('base');
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const handleToggle = useCallback((id: string, paused: boolean) => {
|
||||
if (paused) {
|
||||
activate(id);
|
||||
@@ -217,14 +211,6 @@ export const DisciplinesTab: React.FC = () => {
|
||||
}
|
||||
}, [activate, deactivate]);
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8 text-gray-500">
|
||||
Loading disciplines…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const activeTab = ATTUNEMENT_TABS.find((t) => t.key === activeAttunement);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { useCraftingStore } from '@/lib/game/stores/craftingStore';
|
||||
import type { EquipmentSlot } from '@/lib/game/types';
|
||||
import { DebugName } from '@/components/game/debug/debug-context';
|
||||
@@ -9,19 +9,12 @@ import { InventoryList } from './EquipmentTab/InventoryList';
|
||||
import { EquipmentEffectsSummary } from './EquipmentTab/EquipmentEffectsSummary';
|
||||
|
||||
export function EquipmentTab() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
|
||||
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
|
||||
const storeEquipItem = useCraftingStore((s) => s.equipItem);
|
||||
const storeUnequipItem = useCraftingStore((s) => s.unequipItem);
|
||||
const storeDeleteEquipment = useCraftingStore((s) => s.deleteEquipmentInstance);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const handleEquip = useCallback(
|
||||
(instanceId: string, slot: EquipmentSlot): boolean => {
|
||||
return storeEquipItem(instanceId, slot);
|
||||
@@ -51,14 +44,6 @@ export function EquipmentTab() {
|
||||
[equipmentInstances, equippedInstances]
|
||||
);
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8 text-[var(--text-muted)]">
|
||||
Loading equipment…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DebugName name="EquipmentTab">
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useCombatStore } from '@/lib/game/stores/combatStore';
|
||||
import { useAttunementStore } from '@/lib/game/stores/attunementStore';
|
||||
@@ -197,7 +197,6 @@ GolemCard.displayName = 'GolemCard';
|
||||
// ─── Main Tab ────────────────────────────────────────────────────────────────
|
||||
|
||||
export const GolemancyTab: React.FC = () => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [activeTier, setActiveTier] = useState<string>('base');
|
||||
|
||||
const { golemancy, toggleGolem } = useCombatStore(useShallow(s => ({
|
||||
@@ -210,11 +209,6 @@ export const GolemancyTab: React.FC = () => {
|
||||
elements: s.elements,
|
||||
})));
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// Build attunement lookup for isGolemUnlocked
|
||||
const attunementLookup = useMemo(() => {
|
||||
const lookup: Record<string, { active: boolean; level: number }> = {};
|
||||
@@ -254,14 +248,6 @@ export const GolemancyTab: React.FC = () => {
|
||||
const golemSlots = getGolemSlots(fabricatorLevel);
|
||||
const enabledCount = golemancy.enabledGolems.length;
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8 text-gray-500">
|
||||
Loading golemancy…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const activeTierGolems = golemsByTier[activeTier] ?? [];
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { usePrestigeStore } from '@/lib/game/stores/prestigeStore';
|
||||
import { useManaStore } from '@/lib/game/stores/manaStore';
|
||||
@@ -53,7 +53,6 @@ function groupFloorsByTier(floors: number[]): FloorTier[] {
|
||||
// ─── Main Tab ────────────────────────────────────────────────────────────────
|
||||
|
||||
export const GuardianPactsTab: React.FC = () => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [activeTier, setActiveTier] = useState<string>('all');
|
||||
|
||||
const {
|
||||
@@ -75,11 +74,6 @@ export const GuardianPactsTab: React.FC = () => {
|
||||
const rawMana = useManaStore(s => s.rawMana);
|
||||
const addLog = useUIStore(s => s.addLog);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const guardianFloors = useMemo(
|
||||
() => getAllGuardianFloors(),
|
||||
[],
|
||||
@@ -126,14 +120,6 @@ export const GuardianPactsTab: React.FC = () => {
|
||||
return boonMap;
|
||||
}, [signedPacts]);
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8 text-gray-500">
|
||||
Loading guardian pacts…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DebugName name="GuardianPactsTab">
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { usePrestigeStore, useGameStore } from '@/lib/game/stores';
|
||||
import { PRESTIGE_DEF } from '@/lib/game/constants/prestige';
|
||||
@@ -186,8 +186,6 @@ function ResetLoopSection({ loopInsight, onReset }: { loopInsight: number; onRes
|
||||
// ─── Main Component ───────────────────────────────────────────────────────────
|
||||
|
||||
export function PrestigeTab() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const {
|
||||
insight,
|
||||
totalInsight,
|
||||
@@ -212,11 +210,6 @@ export function PrestigeTab() {
|
||||
|
||||
const startNewLoop = useGameStore((s) => s.startNewLoop);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const handlePurchase = useCallback((id: string) => {
|
||||
doPrestige(id);
|
||||
}, [doPrestige]);
|
||||
@@ -225,14 +218,6 @@ export function PrestigeTab() {
|
||||
startNewLoop();
|
||||
}, [startNewLoop]);
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8 text-gray-500">
|
||||
Loading prestige…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const upgradeEntries = Object.entries(PRESTIGE_DEF);
|
||||
|
||||
return (
|
||||
|
||||
@@ -71,7 +71,7 @@ function EnemyRow({ enemy }: { enemy: EnemyState }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function RoomDisplay({ floorState, _floor }: RoomDisplayProps) {
|
||||
export function RoomDisplay({ floorState }: RoomDisplayProps) {
|
||||
// Guard against null/undefined/stale floorState
|
||||
if (!floorState || !floorState.roomType) {
|
||||
return (
|
||||
|
||||
@@ -47,7 +47,6 @@ function useSpireStats(prestigeUpgrades: Record<string, number>, equippedInstanc
|
||||
// ─── Main Component ───────────────────────────────────────────────────────────
|
||||
|
||||
export function SpireCombatPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [roomsCleared, setRoomsCleared] = useState(0);
|
||||
|
||||
const {
|
||||
@@ -104,8 +103,6 @@ export function SpireCombatPage() {
|
||||
const totalRooms = useMemo(() => getRoomsForFloor(currentFloor), [currentFloor]);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setMounted(true);
|
||||
setRoomsCleared(0);
|
||||
const newRoom = generateSpireFloorState(currentFloor, 0, totalRooms);
|
||||
setCurrentRoom(newRoom);
|
||||
@@ -166,14 +163,6 @@ export function SpireCombatPage() {
|
||||
addActivityLog('floor_transition', '🚪 Exited the Spire.');
|
||||
};
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen text-gray-500">
|
||||
Loading spire...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 flex flex-col">
|
||||
<header className="sticky top-0 z-50 bg-gray-900/95 border-b border-gray-800 px-4 py-2">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useCombatStore, usePrestigeStore, fmt } from '@/lib/game/stores';
|
||||
import { ELEMENT_OPPOSITES, FLOOR_ELEM_CYCLE } from '@/lib/game/constants';
|
||||
@@ -311,8 +311,6 @@ function GuardianRosterItem({ floor, guardian, isDefeated }: { floor: number; gu
|
||||
// ─── Main Component ───────────────────────────────────────────────────────────
|
||||
|
||||
export function SpireSummaryTab() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const {
|
||||
maxFloorReached,
|
||||
clearedFloors,
|
||||
@@ -327,11 +325,6 @@ export function SpireSummaryTab() {
|
||||
insight: s.insight,
|
||||
})));
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const defeatedGuardians = useMemo(() => {
|
||||
return GUARDIAN_FLOORS.filter((floor) => clearedFloors[floor]);
|
||||
}, [clearedFloors]);
|
||||
@@ -346,14 +339,6 @@ export function SpireSummaryTab() {
|
||||
return Object.values(clearedFloors).filter(Boolean).length;
|
||||
}, [clearedFloors]);
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8 text-gray-500">
|
||||
Loading spire data…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DebugName name="SpireSummaryTab">
|
||||
<div className="space-y-4">
|
||||
|
||||
Reference in New Issue
Block a user