chore: cleanup — remove dead weight (prisma, db, examples, python scripts, workflow docs, redundant tsconfigs)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 34s

This commit is contained in:
2026-05-13 12:16:11 +02:00
parent 6ad48efff9
commit bb268d4dea
108 changed files with 4747 additions and 2000 deletions
View File
-2
View File
@@ -48,5 +48,3 @@ prompt
server.log
# Skills directory
/.zscripts/
.gitnexus
BIN
View File
Binary file not shown.
+14
View File
@@ -0,0 +1,14 @@
{
"repoPath": "/home/user/repos/Mana-Loop",
"lastCommit": "54d5e576abe2890bafb82ec682a6a73c2d7b8617",
"indexedAt": "2026-05-07T10:03:30.497Z",
"remoteUrl": "https://n8n-gitea:tkf9hfgxl2k4cmt@gitea.tailf367e3.ts.net/Anexim/Mana-Loop",
"stats": {
"files": 353,
"nodes": 3795,
"edges": 6409,
"communities": 93,
"processes": 146,
"embeddings": 0
}
}
+1
View File
@@ -16,6 +16,7 @@ fi
# Generate project structure
echo "🗺️ Updating project structure..."
node .husky/scripts/generate-project-tree.js
node .husky/scripts/generate-dependency-graph.js
if [ $? -ne 0 ]; then
exit 1
fi
+1
View File
@@ -14,6 +14,7 @@ const IGNORE_PATTERNS = [
/\.md$/, // Markdown documentation files
/context\.md$/, // Context files for sub-agents
/project-structure\.txt$/, // Generated project structure
/dependency-graph\.json$/,
];
function shouldIgnore(filePath) {
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env node
/**
* generate-dependency-graph.js
*
* Generates two files in docs/ on every commit:
*
* docs/dependency-graph.json — full import graph for src/lib/game/
* docs/circular-deps.txt — list of circular dependency chains (empty = clean)
*
* Run manually: node .husky/scripts/generate-dependency-graph.js
* Requires: bun add -d madge
*/
const path = require('path');
const fs = require('fs');
const { execSync } = require('child_process');
const ROOT = path.resolve(__dirname, '../../');
const DOCS_DIR = path.join(ROOT, 'docs');
const GRAPH_OUT = path.join(DOCS_DIR, 'dependency-graph.json');
const CIRCULAR_OUT = path.join(DOCS_DIR, 'circular-deps.txt');
// Check madge is available
function madgeAvailable() {
try {
execSync('bunx madge --version', { stdio: 'ignore', cwd: ROOT });
return true;
} catch {
return false;
}
}
function run(cmd) {
return execSync(cmd, { cwd: ROOT, encoding: 'utf8' });
}
if (!madgeAvailable()) {
console.error('madge not found. Install with: bun add -d madge');
process.exit(1);
}
if (!fs.existsSync(DOCS_DIR)) {
fs.mkdirSync(DOCS_DIR, { recursive: true });
}
// ── 1. Full dependency graph for the game library ─────────────────────────
try {
const graphJson = run(
'bunx madge --json --extensions ts,tsx --exclude "\\.test\\.|__tests__" src/lib/game'
);
// Parse and re-serialize with readable formatting
const graph = JSON.parse(graphJson);
// Annotate with metadata for AI agents
const output = {
_meta: {
generated: new Date().toISOString(),
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.',
},
graph,
};
fs.writeFileSync(GRAPH_OUT, JSON.stringify(output, null, 2));
const nodeCount = Object.keys(graph).length;
console.log(`✅ Dependency graph: ${nodeCount} modules → docs/dependency-graph.json`);
} catch (err) {
console.error('Failed to generate dependency graph:', err.message);
process.exit(1);
}
// ── 2. Circular dependency report ─────────────────────────────────────────
try {
let circularOutput = '';
try {
// madge exits with code 1 when circulars are found; capture stdout anyway
circularOutput = run(
'bunx madge --circular --extensions ts,tsx --exclude "\\.test\\.|__tests__" src/lib/game'
);
} catch (e) {
// exitCode 1 = circulars found; stdout contains the list
circularOutput = e.stdout || '';
}
const lines = circularOutput.trim().split('\n').filter(Boolean);
// madge circular output starts with "Found N circular dependencies!"
const circularLines = lines.filter(
(l) => !l.startsWith('Found') && !l.startsWith('✔') && l.trim()
);
let content;
if (circularLines.length === 0) {
content = `# Circular Dependencies\nGenerated: ${new Date().toISOString()}\n\nNo circular dependencies found. ✅\n`;
console.log('✅ No circular dependencies found');
} else {
content = [
`# Circular Dependencies`,
`Generated: ${new Date().toISOString()}`,
`Found: ${circularLines.length} circular chain(s) — these MUST be fixed before modifying involved files.`,
'',
...circularLines.map((l, i) => `${i + 1}. ${l.trim()}`),
'',
'## How to fix',
'1. Identify which import in the chain can be extracted to a shared types/utils file.',
'2. Move the shared type or function there.',
'3. Both files import from the new shared module instead of each other.',
'4. Run: bunx madge --circular src/lib/game (should return clean)',
].join('\n');
console.warn(`⚠️ Found ${circularLines.length} circular dependency chain(s) — see docs/circular-deps.txt`);
}
fs.writeFileSync(CIRCULAR_OUT, content);
} catch (err) {
console.error('Failed to check circular dependencies:', err.message);
// Non-fatal: write a note to the file and continue
fs.writeFileSync(CIRCULAR_OUT, `# Circular Dependencies\nError running check: ${err.message}\n`);
}
-11
View File
@@ -1,11 +0,0 @@
Failed to start server
Error: listen EADDRINUSE: address already in use :::3000
at <unknown> (Error: listen EADDRINUSE: address already in use :::3000)
at new Promise (<anonymous>) {
code: 'EADDRINUSE',
errno: -98,
syscall: 'listen',
address: '::',
port: 3000
}
[?25h
+510 -574
View File
File diff suppressed because it is too large Load Diff
-2
View File
@@ -4,12 +4,10 @@ RUN apk add --no-cache libc6-compat openssl
RUN npm install -g bun
# Install dependencies
COPY package.json bun.lock* bun.lockb* ./
COPY prisma ./prisma/
RUN bun install --frozen-lockfile
# Copy source
COPY . .
# Generate Prisma client
RUN bunx prisma generate --schema=./prisma/schema.prisma
# Build the application
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
-139
View File
@@ -1,139 +0,0 @@
#!/usr/bin/env python3
import re
import sys
# List of tab files to modify (file path, component name)
tabs = [
('src/components/game/tabs/SpireTab.tsx', 'SpireTab'),
('src/components/game/tabs/AttunementsTab.tsx', 'AttunementsTab'),
('src/components/game/tabs/GolemancyTab.tsx', 'GolemancyTab'),
('src/components/game/tabs/SpellsTab.tsx', 'SpellsTab'),
('src/components/game/tabs/EquipmentTab.tsx', 'EquipmentTab'),
('src/components/game/tabs/CraftingTab.tsx', 'CraftingTab'),
('src/components/game/tabs/LootTab.tsx', 'LootTab'),
('src/components/game/tabs/AchievementsTab.tsx', 'AchievementsTab'),
('src/components/game/tabs/StatsTab.tsx', 'StatsTab'),
('src/components/game/tabs/DebugTab.tsx', 'DebugTab'),
('src/components/game/SkillsTab.tsx', 'SkillsTab'),
]
for file_path, component_name in tabs:
print(f"Processing {file_path}...")
try:
with open(file_path, 'r') as f:
content = f.read()
except FileNotFoundError:
print(f" - ERROR: File not found: {file_path}")
continue
original_content = content
# Check if DebugName is already imported
if 'from \'@/lib/game/debug-context\'' in content or 'from "@/lib/game/debug-context"' in content:
print(f" - DebugName already imported in {file_path}")
else:
# Find the last import line and add the DebugName import after it
lines = content.split('\n')
last_import_idx = -1
for i, line in enumerate(lines):
if line.startswith('import ') or 'import {' in line:
last_import_idx = i
if last_import_idx >= 0:
# Insert the import after the last import
lines.insert(last_import_idx + 1, "import { DebugName } from '@/lib/game/debug-context';")
content = '\n'.join(lines)
print(f" - Added DebugName import to {file_path}")
else:
print(f" - WARNING: No import found in {file_path}")
continue
# Now find the main return statement and wrap its JSX with DebugName
# Pattern: return ( <jsx...> ) where the jsx starts with <
# We need to find: return ( followed by newline and then <
# Find the pattern: return ( \n <tag
# Replace with: return ( \n <DebugName name="..."> \n <tag
# First, let's find where the main return starts
# Look for "return (" that is followed by a newline and then some whitespace and then <
# Simple approach: find the first occurrence of "return (" and then find the next "("
# Actually, let's find the pattern more carefully
# We'll use a regex to find: return ( \s* \n \s* <
# And replace with: return ( \n <DebugName name="..."> \n <
pattern1 = r'(return\s*\(\s*\n)(\s*)(<)'
def replace_start(m):
indent = m.group(2) # The indentation before the opening tag
return f'{m.group(1)}{indent}<DebugName name="{component_name}">\n{indent}{m.group(3)}'
modified = re.sub(pattern1, replace_start, content, count=1)
if modified == content:
print(f" - WARNING: Could not find return pattern in {file_path}")
continue
# Now find the closing of that return statement and add </DebugName>
# The return ends with: ); followed by } (end of function)
# We need to find the matching closing parenthesis and then add </DebugName> before it
# Let's find the last </div> or similar closing tag before the displayName line
# Actually, a simpler approach: find the line with ");" that is followed by "}"
# and then the displayName line
# Let's find the displayName line and work backwards
display_name_pattern = re.escape(component_name) + r'\.displayName\s*=\s*"[^"]+"'
# Find where the function ends - look for the displayName or end of file
lines = modified.split('\n')
# Find the line with displayName
display_name_line = -1
for i, line in enumerate(lines):
if re.search(display_name_pattern, line):
display_name_line = i
break
if display_name_line == -1:
print(f" - WARNING: Could not find displayName for {component_name}")
continue
# Now find the closing of the return statement - look backwards from displayName
# Find the line with " );" which closes the return
close_paren_line = -1
for i in range(display_name_line - 1, -1, -1):
if ');' in lines[i] and i > 0:
close_paren_line = i
break
if close_paren_line == -1:
print(f" - WARNING: Could not find closing ); for return in {file_path}")
continue
# Insert </DebugName> before the closing );
# We need to find the right place - it should be after the last </div> or similar
# Let's just insert it right before the );
# Find the indentation of the );
close_indent = ''
for char in lines[close_paren_line]:
if char == ' ':
close_indent += ' '
else:
break
# Insert the closing tag before the ); line
lines.insert(close_paren_line, f"{close_indent}</DebugName>")
modified = '\n'.join(lines)
# Write back
with open(file_path, 'w') as f:
f.write(modified)
print(f" - Successfully wrapped {component_name} with DebugName")
print("\nDone!")
+238 -2
View File
@@ -73,6 +73,7 @@
"zustand": "^5.0.6",
},
"devDependencies": {
"@playwright/test": "^1.59.1",
"@tailwindcss/postcss": "^4",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
@@ -83,6 +84,7 @@
"eslint-config-next": "^16.1.1",
"husky": "^9.1.7",
"jsdom": "^29.0.1",
"madge": "^8.0.0",
"tailwindcss": "^4",
"tw-animate-css": "^1.3.5",
"typescript": "^5",
@@ -219,6 +221,8 @@
"@date-fns/tz": ["@date-fns/tz@1.4.1", "https://registry.npmjs.com/@date-fns/tz/-/tz-1.4.1.tgz", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
"@dependents/detective-less": ["@dependents/detective-less@5.0.3", "", { "dependencies": { "gonzales-pe": "^4.3.0", "node-source-walk": "^7.0.1" } }, "sha512-v6oD9Ukp+N7V4n6p5I/+mM5fIohSfkrDSGlFm5w/pYmchvbk+sMIHsLxrFJ5Lnujewj1BzWL0K84d88lwZAMQA=="],
"@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "https://registry.npmjs.com/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="],
"@dnd-kit/core": ["@dnd-kit/core@6.3.1", "https://registry.npmjs.com/@dnd-kit/core/-/core-6.3.1.tgz", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="],
@@ -491,6 +495,8 @@
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.4", "https://registry.npmjs.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.4.tgz", { "os": "win32", "cpu": "x64" }, "sha512-3A6efb6BOKwyw7yk9ro2vus2YTt2nvcd56AuzxdMiVOxL9umDyN5PKkKfZ/gZ9row41SjVmTVQNWQhaRRGpOKw=="],
"@playwright/test": ["@playwright/test@1.60.0", "", { "dependencies": { "playwright": "1.60.0" }, "bin": { "playwright": "cli.js" } }, "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag=="],
"@prisma/client": ["@prisma/client@6.19.2", "https://registry.npmjs.com/@prisma/client/-/client-6.19.2.tgz", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg=="],
"@prisma/config": ["@prisma/config@6.19.2", "https://registry.npmjs.com/@prisma/config/-/config-6.19.2.tgz", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ=="],
@@ -741,6 +747,14 @@
"@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="],
"@ts-graphviz/adapter": ["@ts-graphviz/adapter@2.0.6", "", { "dependencies": { "@ts-graphviz/common": "^2.1.5" } }, "sha512-kJ10lIMSWMJkLkkCG5gt927SnGZcBuG0s0HHswGzcHTgvtUe7yk5/3zTEr0bafzsodsOq5Gi6FhQeV775nC35Q=="],
"@ts-graphviz/ast": ["@ts-graphviz/ast@2.0.7", "", { "dependencies": { "@ts-graphviz/common": "^2.1.5" } }, "sha512-e6+2qtNV99UT6DJSoLbHfkzfyqY84aIuoV8Xlb9+hZAjgpum8iVHprGeAMQ4rF6sKUAxrmY8rfF/vgAwoPc3gw=="],
"@ts-graphviz/common": ["@ts-graphviz/common@2.1.5", "", {}, "sha512-S6/9+T6x8j6cr/gNhp+U2olwo1n0jKj/682QVqsh7yXWV6ednHYqxFw0ZsY3LyzT0N8jaZ6jQY9YD99le3cmvg=="],
"@ts-graphviz/core": ["@ts-graphviz/core@2.0.7", "", { "dependencies": { "@ts-graphviz/ast": "^2.0.7", "@ts-graphviz/common": "^2.1.5" } }, "sha512-w071DSzP94YfN6XiWhOxnLpYT3uqtxJBDYdh6Jdjzt+Ce6DNspJsPQgpC7rbts/B8tEkq0LHoYuIF/O5Jh5rPg=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "https://registry.npmjs.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
@@ -865,6 +879,16 @@
"@vitest/utils": ["@vitest/utils@4.1.2", "", { "dependencies": { "@vitest/pretty-format": "4.1.2", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ=="],
"@vue/compiler-core": ["@vue/compiler-core@3.5.34", "", { "dependencies": { "@babel/parser": "^7.29.3", "@vue/shared": "3.5.34", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw=="],
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.34", "", { "dependencies": { "@vue/compiler-core": "3.5.34", "@vue/shared": "3.5.34" } }, "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw=="],
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.34", "", { "dependencies": { "@babel/parser": "^7.29.3", "@vue/compiler-core": "3.5.34", "@vue/compiler-dom": "3.5.34", "@vue/compiler-ssr": "3.5.34", "@vue/shared": "3.5.34", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.14", "source-map-js": "^1.2.1" } }, "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg=="],
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.34", "", { "dependencies": { "@vue/compiler-dom": "3.5.34", "@vue/shared": "3.5.34" } }, "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ=="],
"@vue/shared": ["@vue/shared@3.5.34", "", {}, "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA=="],
"acorn": ["acorn@8.15.0", "https://registry.npmjs.com/acorn/-/acorn-8.15.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "https://registry.npmjs.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
@@ -877,6 +901,10 @@
"ansi-styles": ["ansi-styles@4.3.0", "https://registry.npmjs.com/ansi-styles/-/ansi-styles-4.3.0.tgz", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
"app-module-path": ["app-module-path@2.2.0", "", {}, "sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ=="],
"argparse": ["argparse@2.0.1", "https://registry.npmjs.com/argparse/-/argparse-2.0.1.tgz", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"aria-hidden": ["aria-hidden@1.2.6", "https://registry.npmjs.com/aria-hidden/-/aria-hidden-1.2.6.tgz", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
@@ -901,6 +929,8 @@
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
"ast-module-types": ["ast-module-types@6.0.2", "", {}, "sha512-6KuK/7nZ/2Qh7sGuVEiwxjCxzTY2Pdb5mTo5z1e6/J8BA0tvjR7G8vQJKrQMTqwmnA3UPEyKIFX4YUS1DO1Hvw=="],
"ast-types-flow": ["ast-types-flow@0.0.8", "https://registry.npmjs.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="],
"async-function": ["async-function@1.0.0", "https://registry.npmjs.com/async-function/-/async-function-1.0.0.tgz", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
@@ -921,13 +951,15 @@
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
"brace-expansion": ["brace-expansion@1.1.12", "https://registry.npmjs.com/brace-expansion/-/brace-expansion-1.1.12.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"braces": ["braces@3.0.3", "https://registry.npmjs.com/braces/-/braces-3.0.3.tgz", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"browserslist": ["browserslist@4.28.1", "https://registry.npmjs.com/browserslist/-/browserslist-4.28.1.tgz", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
"buffer": ["buffer@6.0.3", "https://registry.npmjs.com/buffer/-/buffer-6.0.3.tgz", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"bun-types": ["bun-types@1.3.6", "https://registry.npmjs.com/bun-types/-/bun-types-1.3.6.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
@@ -967,8 +999,14 @@
"clean-set": ["clean-set@1.1.2", "https://registry.npmjs.com/clean-set/-/clean-set-1.1.2.tgz", {}, "sha512-cA8uCj0qSoG9e0kevyOWXwPaELRPVg5Pxp6WskLMwerx257Zfnh8Nl0JBH59d7wQzij2CK7qEfJQK3RjuKKIug=="],
"cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],
"cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
"client-only": ["client-only@0.0.1", "https://registry.npmjs.com/client-only/-/client-only-0.0.1.tgz", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
"clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],
"clsx": ["clsx@2.1.1", "https://registry.npmjs.com/clsx/-/clsx-2.1.1.tgz", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"cm6-theme-basic-light": ["cm6-theme-basic-light@0.2.0", "https://registry.npmjs.com/cm6-theme-basic-light/-/cm6-theme-basic-light-0.2.0.tgz", { "peerDependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/highlight": "^1.0.0" } }, "sha512-1prg2gv44sYfpHscP26uLT/ePrh0mlmVwMSoSd3zYKQ92Ab3jPRLzyCnpyOCQLJbK+YdNs4HvMRqMNYdy4pMhA=="],
@@ -983,6 +1021,10 @@
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "https://registry.npmjs.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
"commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
"commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
"compute-scroll-into-view": ["compute-scroll-into-view@2.0.4", "https://registry.npmjs.com/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz", {}, "sha512-y/ZA3BGnxoM/QHHQ2Uy49CLtnWPbt4tTPpEEZiEmmiWBFKjej7nEyH8Ryz54jH0MLXflUYA3Er2zUxPSJu5R+g=="],
"concat-map": ["concat-map@0.0.1", "https://registry.npmjs.com/concat-map/-/concat-map-0.0.1.tgz", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
@@ -1051,16 +1093,22 @@
"decode-named-character-reference": ["decode-named-character-reference@1.2.0", "https://registry.npmjs.com/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="],
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
"deep-is": ["deep-is@0.1.4", "https://registry.npmjs.com/deep-is/-/deep-is-0.1.4.tgz", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"deepmerge-ts": ["deepmerge-ts@7.1.5", "https://registry.npmjs.com/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="],
"defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="],
"define-data-property": ["define-data-property@1.1.4", "https://registry.npmjs.com/define-data-property/-/define-data-property-1.1.4.tgz", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
"define-properties": ["define-properties@1.2.1", "https://registry.npmjs.com/define-properties/-/define-properties-1.2.1.tgz", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
"defu": ["defu@6.1.4", "https://registry.npmjs.com/defu/-/defu-6.1.4.tgz", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"dependency-tree": ["dependency-tree@11.4.3", "", { "dependencies": { "commander": "^12.1.0", "filing-cabinet": "^5.3.0", "precinct": "^12.3.1", "typescript": "^5.9.3" }, "bin": { "dependency-tree": "bin/cli.js" } }, "sha512-Y2gzOJ2Rb2X7MN6pT9llWpXxl5J5s5/11CBpJ5b85DjEqZH7jv3T9RO6HRV/PI/3MDmaKn/g7uoYdYmSb9vLlw=="],
"dequal": ["dequal@2.0.3", "https://registry.npmjs.com/dequal/-/dequal-2.0.3.tgz", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"destr": ["destr@2.0.5", "https://registry.npmjs.com/destr/-/destr-2.0.5.tgz", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
@@ -1069,6 +1117,24 @@
"detect-node-es": ["detect-node-es@1.1.0", "https://registry.npmjs.com/detect-node-es/-/detect-node-es-1.1.0.tgz", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"detective-amd": ["detective-amd@6.1.0", "", { "dependencies": { "ast-module-types": "^6.0.1", "escodegen": "^2.1.0", "get-amd-module-type": "^6.0.2", "node-source-walk": "^7.0.1" }, "bin": { "detective-amd": "bin/cli.js" } }, "sha512-fmI6LGMvotqd49QaA3ZYw+q0aGp2yXmMjzIuY6fH9j9YFIXY/73yDhMwhX9cPbhWd+AH06NH1Di/LKOuCH0Ubg=="],
"detective-cjs": ["detective-cjs@6.1.1", "", { "dependencies": { "ast-module-types": "^6.0.1", "node-source-walk": "^7.0.1" } }, "sha512-pSh7mkCKEtLlmANqLu3KDFS3NV8Hx41jy/JF1/gAWOgU+Uo5QTkeI1tWNP4dWGo4L0E9j18Ez9EPsTleautKqA=="],
"detective-es6": ["detective-es6@5.0.2", "", { "dependencies": { "node-source-walk": "^7.0.1" } }, "sha512-+qHHGYhjupiVs4rnIpI9nZ5B130A4AmE35ZX1w33hb46vcZ7T3jfDbvmPw0FhWtMHn5BS5HHu7ZtnZ53bMcXZA=="],
"detective-postcss": ["detective-postcss@8.0.3", "", { "dependencies": { "is-url-superb": "^4.0.0", "postcss-values-parser": "^6.0.2" }, "peerDependencies": { "postcss": "^8.4.47" } }, "sha512-0AQjxn13b14tLmeXQq0QAFXSP6vBZhWFfmEazyFQ+JVlVwfrYlKF6dGy4R06hqAiSZ9cRvFx0FW4uvVnx0WXiw=="],
"detective-sass": ["detective-sass@6.0.2", "", { "dependencies": { "gonzales-pe": "^4.3.0", "node-source-walk": "^7.0.1" } }, "sha512-i3xpXHDKS0qI2aFW4asQ7fqlPK00ndOVZELvQapFJCaF0VxYmsNWtd0AmvXbTLMk7bfO5VdIeorhY9KfmHVoVA=="],
"detective-scss": ["detective-scss@5.0.2", "", { "dependencies": { "gonzales-pe": "^4.3.0", "node-source-walk": "^7.0.1" } }, "sha512-9JOEMZ8pDh3ShXmftq7hoQqqJsClaGgxo1hghfCeFlmKf5TC/Twtwb0PAaK8dXwpg9Z0uCmEYSrCxO+kel2eEg=="],
"detective-stylus": ["detective-stylus@5.0.1", "", {}, "sha512-Dgn0bUqdGbE3oZJ+WCKf8Dmu7VWLcmRJGc6RCzBgG31DLIyai9WAoEhYRgIHpt/BCRMrnXLbGWGPQuBUrnF0TA=="],
"detective-typescript": ["detective-typescript@14.1.2", "", { "dependencies": { "@typescript-eslint/typescript-estree": "^8.58.2", "ast-module-types": "^6.0.1", "node-source-walk": "^7.0.1" }, "peerDependencies": { "typescript": "^5.4.4 || ^6.0.2" } }, "sha512-bIeEn0eVi/JRsE1YizBR2ilnMlWRAIBJJ6kXCKNFxEEWhUcEY3R6I3KYIAy48ieURbD1hcb3Ebvl8AqeoPMSzg=="],
"detective-vue2": ["detective-vue2@2.3.0", "", { "dependencies": { "@dependents/detective-less": "^5.0.1", "@vue/compiler-sfc": "^3.5.32", "detective-es6": "^5.0.1", "detective-sass": "^6.0.1", "detective-scss": "^5.0.1", "detective-stylus": "^5.0.1", "detective-typescript": "^14.1.0" }, "peerDependencies": { "typescript": "^5.4.4 || ^6.0.2" } }, "sha512-3gwbZPqVTm9sL9XdZsgEJ7x4x99O853VVZHapQAiEkGuMJMpFPjHDrecSgfqnS5JW3FJfYXesLZGvUOibjn49g=="],
"devlop": ["devlop@1.1.0", "https://registry.npmjs.com/devlop/-/devlop-1.1.0.tgz", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"diff": ["diff@5.2.0", "https://registry.npmjs.com/diff/-/diff-5.2.0.tgz", {}, "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A=="],
@@ -1133,6 +1199,8 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "https://registry.npmjs.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="],
"eslint": ["eslint@9.39.2", "https://registry.npmjs.com/eslint/-/eslint-9.39.2.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="],
"eslint-config-next": ["eslint-config-next@16.1.3", "https://registry.npmjs.com/eslint-config-next/-/eslint-config-next-16.1.3.tgz", { "dependencies": { "@next/eslint-plugin-next": "16.1.3", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-q2Z87VSsoJcv+vgR+Dm8NPRf+rErXcRktuBR5y3umo/j5zLjIWH7rqBCh3X804gUGKbOrqbgsLUkqDE35C93Gw=="],
@@ -1159,6 +1227,8 @@
"espree": ["espree@10.4.0", "https://registry.npmjs.com/espree/-/espree-10.4.0.tgz", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
"esquery": ["esquery@1.7.0", "https://registry.npmjs.com/esquery/-/esquery-1.7.0.tgz", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
"esrecurse": ["esrecurse@4.3.0", "https://registry.npmjs.com/esrecurse/-/esrecurse-4.3.0.tgz", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
@@ -1205,6 +1275,8 @@
"file-entry-cache": ["file-entry-cache@8.0.0", "https://registry.npmjs.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"filing-cabinet": ["filing-cabinet@5.5.1", "", { "dependencies": { "app-module-path": "^2.2.0", "commander": "^12.1.0", "enhanced-resolve": "^5.21.0", "module-definition": "^6.0.2", "module-lookup-amd": "^9.1.3", "resolve": "^1.22.12", "resolve-dependency-path": "^4.0.1", "sass-lookup": "^6.1.2", "stylus-lookup": "^6.1.2", "tsconfig-paths": "^4.2.0", "typescript": "^5.9.3" }, "bin": { "filing-cabinet": "bin/cli.js" } }, "sha512-PzLBTChlVPn6LnNxF0KWs+XqPziVh3Sfmz/3TXOymHxu6a9yhrDcQn7YwgpcRM6mqhR2WHVGPR8RU4fmcF1IVA=="],
"fill-range": ["fill-range@7.1.1", "https://registry.npmjs.com/fill-range/-/fill-range-7.1.1.tgz", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"find-up": ["find-up@5.0.0", "https://registry.npmjs.com/find-up/-/find-up-5.0.0.tgz", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
@@ -1219,7 +1291,7 @@
"framer-motion": ["framer-motion@12.26.2", "https://registry.npmjs.com/framer-motion/-/framer-motion-12.26.2.tgz", { "dependencies": { "motion-dom": "^12.26.2", "motion-utils": "^12.24.10", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-lflOQEdjquUi9sCg5Y1LrsZDlsjrHw7m0T9Yedvnk7Bnhqfkc89/Uha10J3CFhkL+TCZVCRw9eUGyM/lyYhXQA=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
"function-bind": ["function-bind@1.1.2", "https://registry.npmjs.com/function-bind/-/function-bind-1.1.2.tgz", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
@@ -1231,10 +1303,14 @@
"gensync": ["gensync@1.0.0-beta.2", "https://registry.npmjs.com/gensync/-/gensync-1.0.0-beta.2.tgz", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-amd-module-type": ["get-amd-module-type@6.0.2", "", { "dependencies": { "ast-module-types": "^6.0.1", "node-source-walk": "^7.0.1" } }, "sha512-7zShVYAYtMnj9S65CfN+hvpBCByfuB1OY8xID01nZEzXTZbx4YyysAfi+nMl95JSR6odt4q8TCj2W63KAoyVLQ=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "https://registry.npmjs.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-nonce": ["get-nonce@1.0.1", "https://registry.npmjs.com/get-nonce/-/get-nonce-1.0.1.tgz", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"get-own-enumerable-property-symbols": ["get-own-enumerable-property-symbols@3.0.2", "", {}, "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g=="],
"get-proto": ["get-proto@1.0.1", "https://registry.npmjs.com/get-proto/-/get-proto-1.0.1.tgz", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"get-symbol-description": ["get-symbol-description@1.1.0", "https://registry.npmjs.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
@@ -1249,6 +1325,8 @@
"globalthis": ["globalthis@1.0.4", "https://registry.npmjs.com/globalthis/-/globalthis-1.0.4.tgz", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
"gonzales-pe": ["gonzales-pe@4.3.0", "", { "dependencies": { "minimist": "^1.2.5" }, "bin": { "gonzales": "bin/gonzales.js" } }, "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ=="],
"gopd": ["gopd@1.2.0", "https://registry.npmjs.com/gopd/-/gopd-1.2.0.tgz", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "https://registry.npmjs.com/graceful-fs/-/graceful-fs-4.2.11.tgz", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
@@ -1301,6 +1379,10 @@
"indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
"inline-style-parser": ["inline-style-parser@0.2.7", "https://registry.npmjs.com/inline-style-parser/-/inline-style-parser-0.2.7.tgz", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
"input-otp": ["input-otp@1.4.2", "https://registry.npmjs.com/input-otp/-/input-otp-1.4.2.tgz", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="],
@@ -1347,6 +1429,8 @@
"is-hexadecimal": ["is-hexadecimal@2.0.1", "https://registry.npmjs.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
"is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="],
"is-map": ["is-map@2.0.3", "https://registry.npmjs.com/is-map/-/is-map-2.0.3.tgz", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
"is-negative-zero": ["is-negative-zero@2.0.3", "https://registry.npmjs.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
@@ -1355,12 +1439,16 @@
"is-number-object": ["is-number-object@1.1.1", "https://registry.npmjs.com/is-number-object/-/is-number-object-1.1.1.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
"is-obj": ["is-obj@1.0.1", "", {}, "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg=="],
"is-plain-obj": ["is-plain-obj@4.1.0", "https://registry.npmjs.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
"is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
"is-regex": ["is-regex@1.2.1", "https://registry.npmjs.com/is-regex/-/is-regex-1.2.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
"is-regexp": ["is-regexp@1.0.0", "", {}, "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA=="],
"is-set": ["is-set@2.0.3", "https://registry.npmjs.com/is-set/-/is-set-2.0.3.tgz", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
"is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "https://registry.npmjs.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="],
@@ -1371,6 +1459,10 @@
"is-typed-array": ["is-typed-array@1.1.15", "https://registry.npmjs.com/is-typed-array/-/is-typed-array-1.1.15.tgz", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
"is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
"is-url-superb": ["is-url-superb@4.0.0", "", {}, "sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA=="],
"is-weakmap": ["is-weakmap@2.0.2", "https://registry.npmjs.com/is-weakmap/-/is-weakmap-2.0.2.tgz", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="],
"is-weakref": ["is-weakref@1.1.1", "https://registry.npmjs.com/is-weakref/-/is-weakref-1.1.1.tgz", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="],
@@ -1455,6 +1547,8 @@
"lodash.merge": ["lodash.merge@4.6.2", "https://registry.npmjs.com/lodash.merge/-/lodash.merge-4.6.2.tgz", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="],
"longest-streak": ["longest-streak@3.1.0", "https://registry.npmjs.com/longest-streak/-/longest-streak-3.1.0.tgz", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
"loose-envify": ["loose-envify@1.4.0", "https://registry.npmjs.com/loose-envify/-/loose-envify-1.4.0.tgz", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
@@ -1467,6 +1561,8 @@
"lz-string": ["lz-string@1.5.0", "https://registry.npmjs.com/lz-string/-/lz-string-1.5.0.tgz", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
"madge": ["madge@8.0.0", "", { "dependencies": { "chalk": "^4.1.2", "commander": "^7.2.0", "commondir": "^1.0.1", "debug": "^4.3.4", "dependency-tree": "^11.0.0", "ora": "^5.4.1", "pluralize": "^8.0.0", "pretty-ms": "^7.0.1", "rc": "^1.2.8", "stream-to-array": "^2.3.0", "ts-graphviz": "^2.1.2", "walkdir": "^0.4.1" }, "peerDependencies": { "typescript": "^5.4.4" }, "optionalPeers": ["typescript"], "bin": { "madge": "bin/cli.js" } }, "sha512-9sSsi3TBPhmkTCIpVQF0SPiChj1L7Rq9kU2KDG1o6v2XH9cCw086MopjVCD+vuoL5v8S77DTbVopTO8OUiQpIw=="],
"magic-string": ["magic-string@0.30.21", "https://registry.npmjs.com/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"markdown-table": ["markdown-table@3.0.4", "https://registry.npmjs.com/markdown-table/-/markdown-table-3.0.4.tgz", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
@@ -1579,12 +1675,18 @@
"mime-db": ["mime-db@1.54.0", "https://registry.npmjs.com/mime-db/-/mime-db-1.54.0.tgz", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
"minimatch": ["minimatch@3.1.2", "https://registry.npmjs.com/minimatch/-/minimatch-3.1.2.tgz", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"minimist": ["minimist@1.2.8", "https://registry.npmjs.com/minimist/-/minimist-1.2.8.tgz", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"module-definition": ["module-definition@6.0.2", "", { "dependencies": { "ast-module-types": "^6.0.1", "node-source-walk": "^7.0.1" }, "bin": { "module-definition": "bin/cli.js" } }, "sha512-SvAU3lB0+Yjbq55yHY3wkRZBOh+fhU1SnIF3IFbTewv6mtAh7yUT8ACHAJ2mGIJ7tCes2QuCL/cl6m0JSZ/ArA=="],
"module-lookup-amd": ["module-lookup-amd@9.1.3", "", { "dependencies": { "commander": "^12.1.0", "requirejs": "^2.3.8", "requirejs-config-file": "^4.0.0" }, "bin": { "lookup-amd": "bin/cli.js" } }, "sha512-Jc3XmOaR9FdfMJSK8+vyLgsCkzm8z2L0NS6vrlRWi12DjS7MY7TMNE7E1yj8yXx837xtMDbKSSgcdXnFlJ2YLg=="],
"motion-dom": ["motion-dom@12.26.2", "https://registry.npmjs.com/motion-dom/-/motion-dom-12.26.2.tgz", { "dependencies": { "motion-utils": "^12.24.10" } }, "sha512-KLMT1BroY8oKNeliA3JMNJ+nbCIsTKg6hJpDb4jtRAJ7nCKnnpg/LTq/NGqG90Limitz3kdAnAVXecdFVGlWTw=="],
"motion-utils": ["motion-utils@12.24.10", "https://registry.npmjs.com/motion-utils/-/motion-utils-12.24.10.tgz", {}, "sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww=="],
@@ -1619,6 +1721,8 @@
"node-releases": ["node-releases@2.0.27", "https://registry.npmjs.com/node-releases/-/node-releases-2.0.27.tgz", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
"node-source-walk": ["node-source-walk@7.0.2", "", { "dependencies": { "@babel/parser": "^7.29.0" } }, "sha512-71kFFjYaSshDTA8/a2HiTYPLdASWjLJxUyJxGE+ffxU+KhxSBtM9kiLUX+R2yooFdSFKMFpi4n3PFtDy6qXv8A=="],
"nypm": ["nypm@0.6.2", "https://registry.npmjs.com/nypm/-/nypm-0.6.2.tgz", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="],
"oauth": ["oauth@0.9.15", "https://registry.npmjs.com/oauth/-/oauth-0.9.15.tgz", {}, "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA=="],
@@ -1647,10 +1751,14 @@
"oidc-token-hash": ["oidc-token-hash@5.2.0", "https://registry.npmjs.com/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz", {}, "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw=="],
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"openid-client": ["openid-client@5.7.1", "https://registry.npmjs.com/openid-client/-/openid-client-5.7.1.tgz", { "dependencies": { "jose": "^4.15.9", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew=="],
"optionator": ["optionator@0.9.4", "https://registry.npmjs.com/optionator/-/optionator-0.9.4.tgz", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],
"outvariant": ["outvariant@1.4.0", "https://registry.npmjs.com/outvariant/-/outvariant-1.4.0.tgz", {}, "sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw=="],
"own-keys": ["own-keys@1.0.1", "https://registry.npmjs.com/own-keys/-/own-keys-1.0.1.tgz", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
@@ -1663,6 +1771,8 @@
"parse-entities": ["parse-entities@4.0.2", "https://registry.npmjs.com/parse-entities/-/parse-entities-4.0.2.tgz", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
"parse-ms": ["parse-ms@2.1.0", "", {}, "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA=="],
"parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="],
"path-exists": ["path-exists@4.0.0", "https://registry.npmjs.com/path-exists/-/path-exists-4.0.0.tgz", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
@@ -1681,20 +1791,32 @@
"pkg-types": ["pkg-types@2.3.0", "https://registry.npmjs.com/pkg-types/-/pkg-types-2.3.0.tgz", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
"playwright": ["playwright@1.60.0", "", { "dependencies": { "playwright-core": "1.60.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA=="],
"playwright-core": ["playwright-core@1.60.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA=="],
"pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="],
"po-parser": ["po-parser@2.1.1", "https://registry.npmjs.com/po-parser/-/po-parser-2.1.1.tgz", {}, "sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ=="],
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "https://registry.npmjs.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"postcss": ["postcss@8.5.6", "https://registry.npmjs.com/postcss/-/postcss-8.5.6.tgz", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"postcss-values-parser": ["postcss-values-parser@6.0.2", "", { "dependencies": { "color-name": "^1.1.4", "is-url-superb": "^4.0.0", "quote-unquote": "^1.0.0" }, "peerDependencies": { "postcss": "^8.2.9" } }, "sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw=="],
"preact": ["preact@10.28.2", "https://registry.npmjs.com/preact/-/preact-10.28.2.tgz", {}, "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA=="],
"preact-render-to-string": ["preact-render-to-string@5.2.6", "https://registry.npmjs.com/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", { "dependencies": { "pretty-format": "^3.8.0" }, "peerDependencies": { "preact": ">=10" } }, "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw=="],
"precinct": ["precinct@12.3.2", "", { "dependencies": { "@dependents/detective-less": "^5.0.3", "commander": "^12.1.0", "detective-amd": "^6.1.0", "detective-cjs": "^6.1.1", "detective-es6": "^5.0.2", "detective-postcss": "^8.0.3", "detective-sass": "^6.0.2", "detective-scss": "^5.0.2", "detective-stylus": "^5.0.1", "detective-typescript": "^14.1.2", "detective-vue2": "^2.3.0", "module-definition": "^6.0.2", "node-source-walk": "^7.0.2", "postcss": "^8.5.14", "typescript": "^5.9.3" }, "bin": { "precinct": "bin/cli.js" } }, "sha512-JbJevI1K80z8e/WIyDt/4vUN/4qcfBSKKqOjJA4mosPPPb7zODKRJQV7YN7apVWN3k58nZYm/vEsLgEGYmnxwg=="],
"prelude-ls": ["prelude-ls@1.2.1", "https://registry.npmjs.com/prelude-ls/-/prelude-ls-1.2.1.tgz", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
"pretty-ms": ["pretty-ms@7.0.1", "", { "dependencies": { "parse-ms": "^2.1.0" } }, "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q=="],
"prisma": ["prisma@6.19.2", "https://registry.npmjs.com/prisma/-/prisma-6.19.2.tgz", { "dependencies": { "@prisma/config": "6.19.2", "@prisma/engines": "6.19.2" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg=="],
"prismjs": ["prismjs@1.30.0", "https://registry.npmjs.com/prismjs/-/prismjs-1.30.0.tgz", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
@@ -1709,6 +1831,10 @@
"queue-microtask": ["queue-microtask@1.2.3", "https://registry.npmjs.com/queue-microtask/-/queue-microtask-1.2.3.tgz", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"quote-unquote": ["quote-unquote@1.0.0", "", {}, "sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg=="],
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
"rc9": ["rc9@2.1.2", "https://registry.npmjs.com/rc9/-/rc9-2.1.2.tgz", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
"react": ["react@19.2.3", "https://registry.npmjs.com/react/-/react-19.2.3.tgz", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
@@ -1741,6 +1867,8 @@
"react-transition-group": ["react-transition-group@4.4.5", "https://registry.npmjs.com/react-transition-group/-/react-transition-group-4.4.5.tgz", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"readdirp": ["readdirp@4.1.2", "https://registry.npmjs.com/readdirp/-/readdirp-4.1.2.tgz", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"recharts": ["recharts@2.15.4", "https://registry.npmjs.com/recharts/-/recharts-2.15.4.tgz", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw=="],
@@ -1761,12 +1889,20 @@
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"requirejs": ["requirejs@2.3.8", "", { "bin": { "r.js": "bin/r.js", "r_js": "bin/r.js" } }, "sha512-7/cTSLOdYkNBNJcDMWf+luFvMriVm7eYxp4BcFCsAX0wF421Vyce5SXP17c+Jd5otXKGNehIonFlyQXSowL6Mw=="],
"requirejs-config-file": ["requirejs-config-file@4.0.0", "", { "dependencies": { "esprima": "^4.0.0", "stringify-object": "^3.2.1" } }, "sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw=="],
"resolve": ["resolve@1.22.11", "https://registry.npmjs.com/resolve/-/resolve-1.22.11.tgz", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
"resolve-dependency-path": ["resolve-dependency-path@4.0.1", "", {}, "sha512-YQftIIC4vzO9UMhO/sCgXukNyiwVRCVaxiWskCBy7Zpqkplm8kTAISZ8O1MoKW1ca6xzgLUBjZTcDgypXvXxiQ=="],
"resolve-from": ["resolve-from@4.0.0", "https://registry.npmjs.com/resolve-from/-/resolve-from-4.0.0.tgz", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "https://registry.npmjs.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="],
"reusify": ["reusify@1.1.0", "https://registry.npmjs.com/reusify/-/reusify-1.1.0.tgz", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rolldown": ["rolldown@1.0.0-rc.12", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.12" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-x64": "1.0.0-rc.12", "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A=="],
@@ -1777,10 +1913,14 @@
"safe-array-concat": ["safe-array-concat@1.1.3", "https://registry.npmjs.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safe-push-apply": ["safe-push-apply@1.0.0", "https://registry.npmjs.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
"safe-regex-test": ["safe-regex-test@1.1.0", "https://registry.npmjs.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
"sass-lookup": ["sass-lookup@6.1.2", "", { "dependencies": { "commander": "^12.1.0", "enhanced-resolve": "^5.20.0" }, "bin": { "sass-lookup": "bin/cli.js" } }, "sha512-GjmndmKQBtlPil79RK72L7yc5kDXZPCQeH97bP8R8DcxtXQJO6vECExb3WP/m6+cxaV9h4ZxrSRvCkPG2v/VSw=="],
"saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
"scheduler": ["scheduler@0.27.0", "https://registry.npmjs.com/scheduler/-/scheduler-0.27.0.tgz", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
@@ -1811,8 +1951,12 @@
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"sonner": ["sonner@2.0.7", "https://registry.npmjs.com/sonner/-/sonner-2.0.7.tgz", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-js": ["source-map-js@1.2.1", "https://registry.npmjs.com/source-map-js/-/source-map-js-1.2.1.tgz", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"space-separated-tokens": ["space-separated-tokens@2.0.2", "https://registry.npmjs.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
@@ -1827,6 +1971,8 @@
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "https://registry.npmjs.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
"stream-to-array": ["stream-to-array@2.3.0", "", { "dependencies": { "any-promise": "^1.1.0" } }, "sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA=="],
"strict-event-emitter": ["strict-event-emitter@0.4.6", "https://registry.npmjs.com/strict-event-emitter/-/strict-event-emitter-0.4.6.tgz", {}, "sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg=="],
"string.prototype.includes": ["string.prototype.includes@2.0.1", "https://registry.npmjs.com/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="],
@@ -1841,8 +1987,14 @@
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "https://registry.npmjs.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"stringify-entities": ["stringify-entities@4.0.4", "https://registry.npmjs.com/stringify-entities/-/stringify-entities-4.0.4.tgz", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
"stringify-object": ["stringify-object@3.3.0", "", { "dependencies": { "get-own-enumerable-property-symbols": "^3.0.0", "is-obj": "^1.0.1", "is-regexp": "^1.0.0" } }, "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-bom": ["strip-bom@3.0.0", "https://registry.npmjs.com/strip-bom/-/strip-bom-3.0.0.tgz", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
"strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
@@ -1857,6 +2009,8 @@
"styled-jsx": ["styled-jsx@5.1.6", "https://registry.npmjs.com/styled-jsx/-/styled-jsx-5.1.6.tgz", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
"stylus-lookup": ["stylus-lookup@6.1.2", "", { "dependencies": { "commander": "^12.1.0" }, "bin": { "stylus-lookup": "bin/cli.js" } }, "sha512-O+Q/SJ8s1X2aMLh4213fQ9X/bND9M3dhSsyTRe+O1OXPcewGLiYmAtKCrnP7FDvDBaXB2ZHPkCt3zi4cJXBlCQ=="],
"supports-color": ["supports-color@7.2.0", "https://registry.npmjs.com/supports-color/-/supports-color-7.2.0.tgz", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "https://registry.npmjs.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
@@ -1899,6 +2053,8 @@
"ts-api-utils": ["ts-api-utils@2.4.0", "https://registry.npmjs.com/ts-api-utils/-/ts-api-utils-2.4.0.tgz", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
"ts-graphviz": ["ts-graphviz@2.1.6", "", { "dependencies": { "@ts-graphviz/adapter": "^2.0.6", "@ts-graphviz/ast": "^2.0.7", "@ts-graphviz/common": "^2.1.5", "@ts-graphviz/core": "^2.0.7" } }, "sha512-XyLVuhBVvdJTJr2FJJV2L1pc4MwSjMhcunRVgDE9k4wbb2ee7ORYnPewxMWUav12vxyfUM686MSGsqnVRIInuw=="],
"tsconfig-paths": ["tsconfig-paths@3.15.0", "https://registry.npmjs.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
"tslib": ["tslib@2.8.1", "https://registry.npmjs.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -1957,6 +2113,8 @@
"use-sync-external-store": ["use-sync-external-store@1.6.0", "https://registry.npmjs.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"uuid": ["uuid@11.1.0", "https://registry.npmjs.com/uuid/-/uuid-11.1.0.tgz", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
"uvu": ["uvu@0.5.6", "https://registry.npmjs.com/uvu/-/uvu-0.5.6.tgz", { "dependencies": { "dequal": "^2.0.0", "diff": "^5.0.0", "kleur": "^4.0.3", "sade": "^1.7.3" }, "bin": { "uvu": "bin.js" } }, "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA=="],
@@ -1977,6 +2135,10 @@
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
"walkdir": ["walkdir@0.4.1", "", {}, "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ=="],
"wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="],
"webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="],
"whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="],
@@ -2027,6 +2189,8 @@
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "https://registry.npmjs.com/semver/-/semver-6.3.1.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@codesandbox/sandpack-client/buffer": ["buffer@6.0.3", "https://registry.npmjs.com/buffer/-/buffer-6.0.3.tgz", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
"@codesandbox/sandpack-react/react-is": ["react-is@17.0.2", "https://registry.npmjs.com/react-is/-/react-is-17.0.2.tgz", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "https://registry.npmjs.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
@@ -2089,6 +2253,22 @@
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "https://registry.npmjs.com/minimatch/-/minimatch-9.0.5.tgz", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@vue/compiler-core/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
"@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@vue/compiler-sfc/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
"@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@vue/compiler-sfc/postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
"dependency-tree/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
"detective-typescript/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.2", "@typescript-eslint/tsconfig-utils": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg=="],
"downshift/react-is": ["react-is@17.0.2", "https://registry.npmjs.com/react-is/-/react-is-17.0.2.tgz", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
"eslint-import-resolver-node/debug": ["debug@3.2.7", "https://registry.npmjs.com/debug/-/debug-3.2.7.tgz", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
@@ -2105,6 +2285,14 @@
"fast-glob/glob-parent": ["glob-parent@5.1.2", "https://registry.npmjs.com/glob-parent/-/glob-parent-5.1.2.tgz", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"filing-cabinet/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
"filing-cabinet/enhanced-resolve": ["enhanced-resolve@5.21.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ=="],
"filing-cabinet/resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
"filing-cabinet/tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="],
"hastscript/@types/hast": ["@types/hast@2.3.10", "https://registry.npmjs.com/@types/hast/-/hast-2.3.10.tgz", { "dependencies": { "@types/unist": "^2" } }, "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw=="],
"hastscript/comma-separated-tokens": ["comma-separated-tokens@1.0.8", "https://registry.npmjs.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", {}, "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw=="],
@@ -2119,26 +2307,44 @@
"micromatch/picomatch": ["picomatch@2.3.1", "https://registry.npmjs.com/picomatch/-/picomatch-2.3.1.tgz", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"module-lookup-amd/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
"next/postcss": ["postcss@8.4.31", "https://registry.npmjs.com/postcss/-/postcss-8.4.31.tgz", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
"next-auth/uuid": ["uuid@8.3.2", "https://registry.npmjs.com/uuid/-/uuid-8.3.2.tgz", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
"node-source-walk/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
"openid-client/lru-cache": ["lru-cache@6.0.0", "https://registry.npmjs.com/lru-cache/-/lru-cache-6.0.0.tgz", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"parse-entities/@types/unist": ["@types/unist@2.0.11", "https://registry.npmjs.com/@types/unist/-/unist-2.0.11.tgz", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"preact-render-to-string/pretty-format": ["pretty-format@3.8.0", "https://registry.npmjs.com/pretty-format/-/pretty-format-3.8.0.tgz", {}, "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="],
"precinct/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
"precinct/postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
"pretty-format/react-is": ["react-is@17.0.2", "https://registry.npmjs.com/react-is/-/react-is-17.0.2.tgz", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
"prop-types/react-is": ["react-is@16.13.1", "https://registry.npmjs.com/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
"refractor/parse-entities": ["parse-entities@2.0.0", "https://registry.npmjs.com/parse-entities/-/parse-entities-2.0.0.tgz", { "dependencies": { "character-entities": "^1.0.0", "character-entities-legacy": "^1.0.0", "character-reference-invalid": "^1.0.0", "is-alphanumerical": "^1.0.0", "is-decimal": "^1.0.0", "is-hexadecimal": "^1.0.0" } }, "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ=="],
"refractor/prismjs": ["prismjs@1.27.0", "https://registry.npmjs.com/prismjs/-/prismjs-1.27.0.tgz", {}, "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA=="],
"sass-lookup/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
"sass-lookup/enhanced-resolve": ["enhanced-resolve@5.21.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ=="],
"stylus-lookup/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
"vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"vite/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"vite/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
@@ -2149,8 +2355,30 @@
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "https://registry.npmjs.com/brace-expansion/-/brace-expansion-2.0.2.tgz", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"@vue/compiler-core/@babel/parser/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@vue/compiler-sfc/@babel/parser/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"detective-typescript/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.2", "@typescript-eslint/types": "^8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw=="],
"detective-typescript/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw=="],
"detective-typescript/@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.59.2", "", {}, "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q=="],
"detective-typescript/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA=="],
"detective-typescript/@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"detective-typescript/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
"filing-cabinet/enhanced-resolve/tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
"filing-cabinet/tsconfig-paths/json5": ["json5@2.2.3", "https://registry.npmjs.com/json5/-/json5-2.2.3.tgz", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"hastscript/@types/hast/@types/unist": ["@types/unist@2.0.11", "https://registry.npmjs.com/@types/unist/-/unist-2.0.11.tgz", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"node-source-walk/@babel/parser/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"refractor/parse-entities/character-entities": ["character-entities@1.2.4", "https://registry.npmjs.com/character-entities/-/character-entities-1.2.4.tgz", {}, "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw=="],
"refractor/parse-entities/character-entities-legacy": ["character-entities-legacy@1.1.4", "https://registry.npmjs.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", {}, "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA=="],
@@ -2163,6 +2391,8 @@
"refractor/parse-entities/is-hexadecimal": ["is-hexadecimal@1.0.4", "https://registry.npmjs.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", {}, "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw=="],
"sass-lookup/enhanced-resolve/tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
"vite/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"vite/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
@@ -2185,6 +2415,12 @@
"vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"detective-typescript/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"detective-typescript/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
"refractor/parse-entities/is-alphanumerical/is-alphabetical": ["is-alphabetical@1.0.4", "https://registry.npmjs.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz", {}, "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg=="],
"detective-typescript/@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
}
}
BIN
View File
Binary file not shown.
@@ -1,25 +0,0 @@
# TASK-001: Playwright Setup + Baseline E2E Tests
## Status: COMPLETE
## Objective
Add Playwright E2E testing to the Mana Loop project and create baseline tests that validate core gameplay systems work correctly.
## Completion Date
Completed in single session.
## Results
- **13 E2E tests created and all passing**
- Playwright configured with Chromium headless
- WebServer integration configured to auto-start Next.js dev server
## Files Created/Modified
- `package.json` added `@playwright/test` and `test:e2e` script
- `playwright.config.ts` NEW: Playwright configuration
- `e2e/combat.spec.ts` NEW: 5 combat system tests
- `e2e/enchanting.spec.ts` NEW: 4 enchanting flow tests
- `e2e/equipment.spec.ts` NEW: 5 equipment management tests
- `docs/tasks/TASK-001-playwright-setup.md` Task tracking doc
## Push
Committed as `47b2a0b` and pushed to `origin/master`.
@@ -1,16 +0,0 @@
{
"taskId": "TASK-006-left-panel-redesign",
"status": "completed",
"completedSteps": [
"Created AttunementStatus component (src/components/game/AttunementStatus.tsx)",
"Created ActivityLogPanel wrapper (src/components/game/ActivityLogPanel.tsx)",
"Redesigned LeftPanel.tsx with 5 sections",
"Removed CalendarDisplay from LeftPanel",
"Updated ActivityLog with configurable maxEntries prop",
"Exported new components from game/index.ts",
"Applied design tokens from globals.css",
"Typecheck and lint pass, committed and pushed"
],
"nextStep": "Task complete - all acceptance criteria met",
"notes": "Left panel now shows: (1) Mana display, (2) Climb Spire button, (3) Current action, (4) Attunement status strip, (5) Activity log with last 20 events. Calendar removed. No functional regression."
}
-82
View File
@@ -1,82 +0,0 @@
# PLAN: SpireTab Refresh & Casting Fixes
## Phase 1: Fix Cast Bar Not Updating
1. **Audit `SpireTab.tsx` cast progress subscription**
- Check if `useCombatStore((s) => s.castProgress)` is properly subscribed
- Verify the progress bar component receives the latest `castProgress` value
2. **Check `combatStore.ts` for `castProgress` updates**
- Ensure `processCombatTick()` properly updates `castProgress` in state
- Verify `set({ castProgress })` is called with correct value (0-1 range)
3. **Fix Zustand subscription if broken**
- Use `useCombatStore((s) => s.castProgress)` with proper selector
- Ensure component re-renders when `castProgress` changes
## Phase 2: Fix Casting Not Costing Mana
1. **Audit `combat-actions.ts` mana deduction**
- Verify `deductSpellCost()` is called when spell completes
- Check `canAffordSpellCost()` is checked before casting starts
2. **Ensure `rawMana`/`elements` state updates**
- Confirm `deductSpellCost()` returns updated state
- Verify `combatStore` passes correct state to `processCombatTick()`
3. **Test with modular stores only**
- Use `useManaStore` from `stores/manaStore` (not legacy)
- Verify `deductSpellCost` from `utils/` (not legacy `store-modules/`)
## Phase 3: Make SpireTab Full-Screen (No Study/Crafting)
1. **Remove study components when `simpleMode=true`**
- In `SpireTab.tsx`, conditionally render study progress ONLY if `!simpleMode`
- Remove `currentStudyTarget` subscription when in spire mode
2. **Remove crafting progress components**
- Conditionally render crafting progress ONLY if `!simpleMode`
- Remove `designProgress`, `preparationProgress`, etc. when in spire
3. **Add "Climb Down to Exit" button**
- Add button in `FloorControls.tsx` or `SpireTab.tsx`
- Button calls `exitSpireMode()` from `combatStore`
- Visible only when `simpleMode=true` (in spire)
## Phase 4: Refresh SpireTab Layout
1. **Reorganize component sections**
- Clear order: SpireHeader → Combat Info → Floor Controls → Activity Log
- Remove redundant elements (duplicated stats, etc.)
2. **Improve visual hierarchy**
- Use consistent card layouts
- Proper spacing between sections
- Clear headings for each section
3. **Clean up confusing elements**
- Remove any UI that's irrelevant to spire (study, crafting, etc.)
- Simplify floor controls for combat focus
## Phase 5: Enforce Modular Stores Only
1. **Audit all imports in modified files**
- `SpireTab.tsx`: Ensure NO imports from `store.ts` or `store-modules/`
- `combat-actions.ts`: Use `utils/` for helpers
- `combatStore.ts`: Already uses modular stores (verify)
2. **Replace any legacy imports**
- Search for `@/lib/game/store` or `@/lib/game/store-modules` in modified files
- Replace with `@/lib/game/stores/` or `@/lib/game/utils/`
## Phase 6: Add Regression Tests
1. **Create `spire-tab-refresh.test.ts`**
- Test cast progress updates during combat ticks
- Test mana costs deducted when spells cast
- Test `simpleMode` hides study/crafting components
- Test "Climb Down" button exits spire mode
2. **Add to `stores/__tests__/` directory**
- Follow existing test patterns (Vitest)
- Mock store state as needed
- Assert acceptance criteria from SPEC
## Files to Modify (Summary)
| File | Changes |
|------|---------|
| `src/components/game/tabs/SpireTab.tsx` | Fix cast bar, remove study/crafting in spire, refresh layout |
| `src/components/game/tabs/FloorControls.tsx` | Add "Climb Down" button |
| `src/lib/game/stores/combat-actions.ts` | Verify mana deduction logic |
| `src/lib/game/stores/combatStore.ts` | Ensure `castProgress` updates correctly |
| `src/lib/game/stores/__tests__/spire-tab-refresh.test.ts` | New regression tests |
## Verification Before Implementation
- ☑ All SPEC acceptance criteria mapped to plan items above
- ☑ No legacy store imports in plan
- ☑ All files <400 lines (combat-actions.ts is 117 lines, OK)
- ☑ Tests planned for all fixed issues
-142
View File
@@ -1,142 +0,0 @@
# SPEC: SpireTab Refresh & Casting Fixes
## 1. Objective
Fix multiple issues with the SpireTab and spell casting system:
1. **Cast bar not updating**: Spell cast progress (`castProgress`) doesn't update visually during combat
2. **Casting doesn't cost mana**: Mana costs are not deducted when spells are cast
3. **SpireTab full-screen experience**: SpireTab should be a dedicated screen where:
- Player cannot study skills (must climb down to exit spire first)
- Layout is optimized for combat focus
4. **Confusing layout**: Current SpireTab layout is cluttered and needs refresh for better UX
**Why**:
- Casting feels broken when progress bar doesn't update
- Players get free spells (no mana cost) which breaks game balance
- Spire should feel like a separate "mode" with focused combat
- Current layout confuses players about available actions
## 2. Controls/API
### Player-Facing Controls
- **Climb Up/Down buttons**: Change floors (already exists)
- **Spell selection**: Click to set active spell (already exists)
- **Enter Spire Mode**: Button to enter dedicated spire screen (already exists)
- **Cast progress bar**: Visual indicator of spell casting progress (BROKEN - needs fix)
- **Climb Down to Exit**: Only way to leave spire mode (NEW behavior)
### Modified Game Internals
- `combatStore.ts`:
- `castProgress`: Should update 0-1 per tick (fix binding)
- `processCombatTick()`: Should properly deduct mana costs via `deductSpellCost()`
- `SpireTab.tsx`:
- Remove study progress components (player can't study in spire)
- Remove crafting progress components (irrelevant in spire)
- Add "Climb Down" button to exit spire mode
- Refresh layout for clarity
### Public API Changes
None (internal bug fixes + UI refresh)
## 3. Project Layout
Follow modular architecture rules from AGENTS.md:
### Files to Modify
| File | Purpose | Line Count Check |
|------|---------|------------------|
| `src/components/game/tabs/SpireTab.tsx` | Main SpireTab component - refresh layout, remove study/crafting | Must stay <400 lines |
| `src/components/game/tabs/SpireHeader.tsx` | Spire header - ensure maxFloorReached works | Must stay <400 lines |
| `src/components/game/tabs/FloorControls.tsx` | Floor controls - add "Climb Down to Exit" | Must stay <400 lines |
| `src/lib/game/stores/combat-actions.ts` | Fix mana deduction in `processCombatTick()` | Must stay <400 lines |
| `src/lib/game/stores/combatStore.ts` | Ensure `castProgress` updates correctly | Must stay <400 lines |
### Files to Create
| File | Purpose | Line Count Check |
|------|---------|------------------|
| None (modifying existing files only) | | |
### Stores to Use (NO LEGACY STORES!)
-`useCombatStore` from `src/lib/game/stores/combatStore`
-`useManaStore` from `src/lib/game/stores/manaStore`
-`usePrestigeStore` from `src/lib/game/stores/prestigeStore`
- ❌ NO `import from '@/lib/game/store'` (legacy!)
- ❌ NO `import from '@/lib/game/store-modules/'` (legacy!)
### Module Ownership
- SpireTab UI: `components/game/tabs/`
- Combat logic: `stores/combat-actions.ts`
- Store state: `stores/combatStore.ts`
## 4. Code Style
Follow existing project conventions:
- TypeScript strict mode, explicit type annotations
- Zustand store patterns: `set()`, `get()` for state updates
- Use modular stores ONLY (AGENTS.md rule)
- Naming: camelCase for variables/functions, PascalCase for interfaces/types
- No `any` types, use defined interfaces from `src/lib/game/types/`
- Follow ESLint rules (run `npm run lint` before committing)
- Use existing patterns for combat/spell casting
### Key Patterns
- Cast progress: `useCombatStore((s) => s.castProgress)` (0-1 value)
- Mana deduction: Use `deductSpellCost()` from `src/lib/game/utils/`
- Guard against undefined stores: Optional chaining `state?.castProgress`
## 5. Testing
### What to Test
1. **Cast bar updates**: `castProgress` changes during combat ticks
2. **Mana costs deducted**: Raw mana/elements decrease when spells cast
3. **No studying in spire**: Study components not rendered when `simpleMode=true`
4. **Climb down to exit**: Button appears in spire mode, clears `spireMode`
5. **Layout refresh**: SpireTab renders cleanly without clutter
### How to Test
- Unit tests using Vitest (existing test framework)
- Test files in `src/lib/game/stores/__tests__/`
- Mock game state to simulate combat ticks
- Assert castProgress changes over time
- Assert mana decreases after spell cast
- Use `useCombatStore.getState()` for assertions
### Tooling
- Vitest (test runner)
- Zustand store testing patterns (use `getState()`)
- Mock `deductSpellCost()` to verify calls
## 6. Boundaries (Out-of-Scope Items)
- No changes to spell definitions or damage calculations
- No changes to attunement system (separate from spire)
- No changes to prestige/system (unless directly related to spire exit)
- No new tabs or major architectural changes
- No changes to legacy store (we're avoiding it, not fixing it here)
- No changes to other tabs (SkillsTab, CraftingTab, etc.)
## Acceptance Criteria (Per Requirement)
| # | Requirement | Acceptance Criterion |
|---|-------------|----------------------|
| 1 | Cast bar updates during spell casting | `castProgress` in combatStore changes 0→1 over time, and UI reflects this |
| 2 | Casting costs mana | `rawMana` or element mana decreases by `spell.cost` when spell completes |
| 3 | SpireTab is full-screen combat focus | Study/Crafting components NOT rendered when `simpleMode=true` |
| 4 | Player must climb down to exit spire | "Climb Down" button visible in spire mode, clears `spireMode` on click |
| 5 | Layout refresh for clarity | SpireTab has clear sections: Floor Info, Combat, Controls (no clutter) |
| 6 | Use modular stores only | Zero imports from `store.ts` or `store-modules/` in modified files |
| 7 | All files <400 lines | Pre-commit hook passes for all modified files |
| 8 | Regression tests added | New test file `spire-tab-refresh.test.ts` passes in `npm run test` |
## Verification Checklist (Do NOT implement until approved!)
- ☐ SPEC.md committed to version control (not yet)
- ☑ Every feature has explicit acceptance criterion (above)
- ☑ Out-of-scope items listed (Boundaries section)
- ☐ Human has reviewed and approved (**WAITING FOR THIS**)
- ☐ No implementation code exists yet (correct - spec only)
**I will NOT proceed to PLAN or IMPLEMENT phases until this spec is approved by you (the human).**
Please review and approve, or request changes.
-47
View File
@@ -1,47 +0,0 @@
# TASKS: SpireTab Refresh & Casting Fixes
## Task 1: Fix Cast Bar Not Updating
- [ ] 1.1 Check `SpireTab.tsx` for `castProgress` subscription from `useCombatStore`
- [ ] 1.2 Verify `combat-actions.ts` updates `castProgress` in `processCombatTick()`
- [ ] 1.3 Fix Zustand subscription if `castProgress` not updating
- [ ] 1.4 Test: `castProgress` changes during combat ticks
## Task 2: Fix Casting Not Costing Mana
- [ ] 2.1 Audit `combat-actions.ts` for `deductSpellCost()` call
- [ ] 2.2 Verify `canAffordSpellCost()` checked before casting
- [ ] 2.3 Ensure `rawMana`/`elements` state updates after cast
- [ ] 2.4 Test: Mana decreases when spells cast
## Task 3: Make SpireTab Full-Screen (No Study/Crafting)
- [ ] 3.1 Remove study progress components when `simpleMode=true`
- [ ] 3.2 Remove crafting progress components when `simpleMode=true`
- [ ] 3.3 Add "Climb Down to Exit" button in `FloorControls.tsx` or `SpireTab.tsx`
- [ ] 3.4 Button calls `exitSpireMode()` from `combatStore`
- [ ] 3.5 Test: Study/crafting not rendered in spire mode
## Task 4: Refresh SpireTab Layout
- [ ] 4.1 Reorganize `SpireTab.tsx` sections: Header → Combat → Controls → Log
- [ ] 4.2 Remove redundant elements (duplicated stats, etc.)
- [ ] 4.3 Improve visual hierarchy with consistent card layouts
- [ ] 4.4 Test: Layout renders cleanly without clutter
## Task 5: Enforce Modular Stores Only
- [ ] 5.1 Audit imports in `SpireTab.tsx`, `combat-actions.ts`, `combatStore.ts`
- [ ] 5.2 Replace any `@/lib/game/store` or `@/lib/game/store-modules` imports
- [ ] 5.3 Verify all use `src/lib/game/stores/` or `src/lib/game/utils/`
- [ ] 5.4 Test: Zero legacy imports in modified files
## Task 6: Add Regression Tests
- [ ] 6.1 Create `src/lib/game/stores/__tests__/spire-tab-refresh.test.ts`
- [ ] 6.2 Test cast progress updates (acceptance criterion #1)
- [ ] 6.3 Test mana costs deducted (acceptance criterion #2)
- [ ] 6.4 Test no study in spire (acceptance criterion #3)
- [ ] 6.5 Test climb down to exit (acceptance criterion #4)
- [ ] 6.6 Run `npm run test` to verify all pass
## Task 7: Commit & Push
- [ ] 7.1 Run `npm run lint` to check code style
- [ ] 7.2 Run pre-commit checks (auto on commit)
- [ ] 7.3 Commit with message: "fix: SpireTab refresh, cast bar, mana costs, full-screen mode"
- [ ] 7.4 Push to origin/master
- [ ] 7.5 Update task list to completed
-8
View File
@@ -1,8 +0,0 @@
# Active Task Log
| # | Task | Status |
|---|------|--------|
| TASK-001 | Playwright setup + baseline E2E tests (includes 002-004) | ✅ ARCHIVED |
| TASK-005 | `globals.css` design tokens | ✅ ARCHIVED |
| TASK-006 | Left panel redesign | ✅ ARCHIVED |
| TASK-007 | Skill system v2 (`computeStats` + migration) | 🔄 IN PROGRESS |
+2 -2
View File
@@ -1,8 +1,8 @@
# Circular Dependencies
Generated: 2026-05-11T09:20:28.554Z
Generated: 2026-05-13T10:00:12.422Z
Found: 8 circular chain(s) — these MUST be fixed before modifying involved files.
1. Processed 161 files (4.4s) (31 warnings)
1. Processed 174 files (1.9s) (28 warnings)
2. 1) data/equipment/index.ts > data/equipment/utils.ts
3. 2) data/golems/index.ts > data/golems/utils.ts
4. 3) stores/combat-actions.ts > stores/combatStore.ts
+62 -1
View File
@@ -1,6 +1,6 @@
{
"_meta": {
"generated": "2026-05-11T09:20:23.712Z",
"generated": "2026-05-13T10:00:10.280Z",
"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."
},
@@ -37,6 +37,8 @@
"constants/guardians.ts",
"constants/prestige.ts",
"constants/rooms.ts",
"constants/skills-v2-types.ts",
"constants/skills-v2.ts",
"constants/skills.ts",
"constants/spells.ts"
],
@@ -44,6 +46,63 @@
"types.ts"
],
"constants/rooms.ts": [],
"constants/skills-combat.ts": [
"constants/skills-v2-types.ts"
],
"constants/skills-core.ts": [
"constants/skills-v2-types.ts"
],
"constants/skills-crafting.ts": [
"constants/skills-v2-types.ts"
],
"constants/skills-element-caps.ts": [
"constants/skills-v2-types.ts"
],
"constants/skills-enchant.ts": [
"constants/skills-v2-types.ts"
],
"constants/skills-golemancy.ts": [
"constants/skills-v2-types.ts"
],
"constants/skills-hybrid.ts": [
"constants/skills-v2-types.ts"
],
"constants/skills-invocation.ts": [
"constants/skills-v2-types.ts"
],
"constants/skills-research.ts": [
"constants/skills-v2-types.ts"
],
"constants/skills-v2-defs.ts": [
"constants/skills-v2-registry.ts",
"constants/skills-v2-types.ts"
],
"constants/skills-v2-registry.ts": [
"constants/skills-combat.ts",
"constants/skills-core.ts",
"constants/skills-crafting.ts",
"constants/skills-element-caps.ts",
"constants/skills-enchant.ts",
"constants/skills-golemancy.ts",
"constants/skills-hybrid.ts",
"constants/skills-invocation.ts",
"constants/skills-research.ts",
"constants/skills-v2-types.ts"
],
"constants/skills-v2-types.ts": [],
"constants/skills-v2.ts": [
"constants/skills-combat.ts",
"constants/skills-core.ts",
"constants/skills-crafting.ts",
"constants/skills-element-caps.ts",
"constants/skills-enchant.ts",
"constants/skills-golemancy.ts",
"constants/skills-hybrid.ts",
"constants/skills-invocation.ts",
"constants/skills-research.ts",
"constants/skills-v2-defs.ts",
"constants/skills-v2-types.ts"
],
"constants/skills.ts": [
"types.ts"
],
@@ -508,6 +567,7 @@
],
"store-modules/tick-logic.ts": [
"constants.ts",
"crafting-slice.ts",
"data/attunements.ts",
"data/golems/index.ts",
"effects.ts",
@@ -516,6 +576,7 @@
"store-modules/computed-stats.ts",
"store-modules/room-utils.ts",
"types.ts",
"utils/combat-utils.ts",
"utils/floor-utils.ts"
],
"store-tests/test-utils.ts": [
+650
View File
@@ -0,0 +1,650 @@
# Mana Loop — Remediation & Redesign Strategy
**Document Status:** Working Draft
**Purpose:** Systematic plan to stabilise the game, redesign broken systems, and deliver a genuinely good product.
---
## The Current State
The codebase arrived in a state where several systems need attention:
1. **The skill system is incoherent** — it evolved without a clear design philosophy and the attunement pivot was never cleanly landed.
2. **The UI is visually unacceptable** — generic AI-generated aesthetics, not a designed game.
These problems require focused solutions. This document covers all of them in a prioritised, structured way.
---
## Part 1 — Skill System Redesign
### Philosophy: Trash and Restart
The existing system has 15 skill evolution modules, 5 tiers with 10,000x scaling, milestone upgrade trees, hybrid skills, and research unlocks. It grew organically and now no one — including the AI agent — can reliably predict what a skill change does.
The new system has one guiding principle: **every skill is just a collection of named effects, and every effect has a single number that says how much it changes.**
---
### New Skill Architecture
#### Concept: Skills as Effect Bundles
```typescript
// Every skill is just metadata + an array of effects
interface SkillDef {
id: string;
name: string;
description: string;
category: SkillCategory;
attunementRequired?: string; // Which attunement unlocks this
maxLevel: number; // Usually 10
studyCost: (level: number) => number;
studyTime: (level: number) => number; // hours
effects: SkillEffect[]; // Applied at level 1, scale linearly
}
// An effect is a single stat change
interface SkillEffect {
stat: StatKey; // e.g. 'maxMana', 'regenRate', 'damageMultiplier'
mode: 'add' | 'multiply';
valuePerLevel: number; // e.g. 100 (add 100 per level) or 0.05 (add 5% per level)
}
// The full set of game stats
type StatKey =
| 'maxMana'
| 'manaRegen'
| 'clickMana'
| 'elementCap'
| 'studySpeed'
| 'studyCostMult'
| 'meditationMult'
| 'enchantCapacity'
| 'enchantSpeed'
| 'enchantPower'
| 'disenchantRecovery'
| 'baseDamage'
| 'damageMultiplier'
| 'attackSpeed'
| 'critChance'
| 'critMultiplier'
| 'armorPierce'
| 'insightGain'
| 'golemDamage'
| 'golemDuration'
| 'pactMultiplier'
| 'conversionRate';
```
#### Concept: Milestone Choices (Simplified)
Keep milestone choices at level 5 — they're fun and create build identity. Simplify to 3 choices max:
```typescript
interface SkillMilestone {
atLevel: number; // 5 or 10
choices: MilestoneChoice[]; // Always exactly 2-3 options
}
interface MilestoneChoice {
id: string;
label: string;
description: string;
effects: SkillEffect[]; // Same format as skill effects
}
```
No upgrade paths, no prerequisite trees within milestones. Choose once. Done.
#### Concept: Tiers as New Skills, Not Multipliers
Tiers-as-10,000x-multipliers is a design smell. It makes early choices feel irrelevant and creates absurd numbers. Instead:
**Tiering up unlocks a new skill in the same category, not a multiplied version of the old one.**
```
Mana Well (max 10)
→ Tier-up unlocks: "Deep Reservoir" skill (a genuinely different bonus)
Deep Reservoir (max 5)
→ Tier-up unlocks: "Mana Conduit" skill (yet another distinct ability)
```
Each tier-unlocked skill has its own effects, its own flavour. Power grows because you're stacking multiple skills, not because a single skill has a 10,000x internal multiplier.
---
### New Skill Categories
#### Core (No Attunement)
| Skill | Effect | Max |
|-------|--------|-----|
| Mana Well | +100 maxMana/level | 10 |
| Mana Flow | +1 manaRegen/level | 10 |
| Elemental Affinity | +50 elementCap/level | 10 |
| Quick Learner | +10% studySpeed/level | 10 |
| Focused Mind | -5% studyCost/level | 10 |
| Meditation Mastery | +15% meditationMult/level | 5 |
#### Enchanter Attunement
| Skill | Effect | Max | Requires |
|-------|--------|-----|---------|
| Enchanting | Unlocks 3-step enchant | 10 | Enchanter 1 |
| Efficient Enchant | -5% enchantCapacity cost/level | 5 | Enchanting 3 |
| Enchant Speed | -10% enchantSpeed/level | 5 | Enchanting 2 |
| Essence Refining | +10% enchantPower/level | 3 | Enchanting 5 |
| Disenchanting | +20% disenchantRecovery/level | 3 | Enchanting 2 |
#### Invoker Attunement
| Skill | Effect | Max | Requires |
|-------|--------|-----|---------|
| Pact Binding | +10% pactMultiplier/level | 10 | Invoker 1 |
| Invocation Mastery | +5% damageMultiplier/level | 10 | Invoker 2 |
| Guardian Lore | +20% damage vs guardians/level | 5 | Invoker 3 |
| Ritual Speed | -15% pact ritual time/level | 3 | Invoker 2 |
#### Fabricator Attunement
| Skill | Effect | Max | Requires |
|-------|--------|-----|---------|
| Golem Mastery | +10% golemDamage/level | 10 | Fabricator 2 |
| Golem Efficiency | +5% attackSpeed (golems)/level | 5 | Fabricator 2 |
| Golem Longevity | +1 golemDuration/level | 3 | Fabricator 3 |
| Crafting Mastery | -10% craft time/level | 5 | Fabricator 1 |
#### Attunement-Specific Research (Unlock Skills)
These are `max: 1` skills that unlock new capabilities. They don't need tiers or upgrade trees:
```typescript
// Flat unlock structure — no evolution needed
const RESEARCH_SKILLS: ResearchSkill[] = [
{ id: 'fireResearch', unlocks: ['emberShot', 'fireball'], req: { enchanting: 1 } },
{ id: 'waterResearch', unlocks: ['waterJet', 'iceShard'], req: { enchanting: 1 } },
{ id: 'lightningResearch', unlocks: ['spark', 'lightningBolt'], req: { enchanting: 3 } },
// ...
];
```
---
### Computed Stats: Single Source of Truth
All these skills feed into one `computeStats(state)` function that returns a flat `ComputedStats` object. Nothing reads from individual skill levels directly — everything reads from `ComputedStats`.
```typescript
function computeStats(state: GameState): ComputedStats {
const stats: ComputedStats = { ...BASE_STATS };
// Apply every skill level × its effects
for (const [skillId, level] of Object.entries(state.skills)) {
const def = SKILLS[skillId];
if (!def || level === 0) continue;
for (const effect of def.effects) {
if (effect.mode === 'add') {
stats[effect.stat] += effect.valuePerLevel * level;
} else {
stats[effect.stat] *= 1 + (effect.valuePerLevel * level);
}
}
}
// Apply milestone choices
for (const choiceId of state.skillUpgrades) {
const choice = MILESTONE_CHOICES[choiceId];
if (!choice) continue;
for (const effect of choice.effects) {
// same logic
}
}
// Apply equipment enchantments
// Apply prestige upgrades
return stats;
}
```
This is **testable by design**. Every skill test is: given skill X at level Y, `computeStats()` returns Z.
---
### Migration Plan
1. Write `computeStats()` with tests (TDD).
2. Define all skills in the new flat format in `constants/skills-v2.ts`.
3. Keep the old skill IDs — just change how they're computed. The existing `state.skills` shape doesn't change.
4. Delete `skill-evolution-modules/` entirely.
5. Delete `skill-evolution.ts`.
6. Update all callers of computed stats to use the new function.
7. Run all existing tests. Fix any that fail.
---
## Part 2 — Attunement Expansion
### Vision: Many Paths, Player Chooses
Current state: 3 attunements, all unlocked via linear progression.
Target state: **810 attunements** grouped into paths. Player picks one path at each milestone. Paths are:
- **Combat Path** — focus on raw damage, speed, and floor clearing
- **Crafting Path** — focus on enchantments, equipment power, and golemancy
- **Utility Path** — focus on mana generation, study speed, and loop efficiency
---
### Attunement Redesign
#### The 3 Existing (Reworked)
| Attunement | Path | Slot | Primary Grant |
|------------|------|------|---------------|
| Enchanter | Crafting | Right Hand | Transference mana + enchanting access |
| Invoker | Combat | Chest | Pact power + guardian damage |
| Fabricator | Crafting | Left Hand | Earth mana + golem access |
#### New Attunements (Phase 2 additions)
| Attunement | Path | Slot | Primary Grant | Unlock Condition |
|------------|------|------|---------------|-----------------|
| **Battle Mage** | Combat | Head | +damage, attackSpeed | Reach floor 20 |
| **Arcanist** | Utility | Back | +mana cap, conversion rate | Study 5 skills to max |
| **Sage** | Utility | Head | +study speed, insight gain | Complete 3 loops |
| **Runesmith** | Crafting | Left Leg | +enchant capacity, crafting speed | Enchant 5 items |
| **Warden** | Combat | Right Leg | +elemental resist, armor pierce | Sign 3 pacts |
| **Timeweaver** | Utility | Back | -incursion penalty, +loop bonuses | Survive incursion |
#### Path Selection Moment
At **first prestige** (loop completion), player is presented with their first **Path Choice**:
> "Your magic has matured. Choose how to develop it:"
>
> 🗡️ **Combat Path** — Unlock Battle Mage + Warden attunements first. Focus: raw power, floor clearing.
> ✨ **Crafting Path** — Unlock Runesmith + Fabricator advanced tiers first. Focus: equipment domination.
> 🔮 **Utility Path** — Unlock Sage + Arcanist attunements first. Focus: meta progression, loop efficiency.
This choice doesn't lock out the other attunements permanently — it determines **unlock order and starting bonuses**. By loop 5, most players will have all attunements. The path just shapes the early and mid game.
---
### Attunement State Structure
Keep the existing `AttunementState` shape. Add:
```typescript
interface AttunementState {
id: string;
active: boolean;
level: number;
experience: number;
title?: string;
// NEW:
path?: 'combat' | 'crafting' | 'utility'; // For path-specific bonuses
unlockedAt?: number; // Loop number when this was unlocked
}
```
---
## Part 3 — Enchanting System (Stable)
### Keep the 3-Step Flow
The 3-step flow is well-designed. Here is what each step does, stated precisely:
**Step 1 — Design**
- Player selects a piece of owned equipment.
- Player picks effects from their **unlocked pool** (what they've researched).
- System previews: total capacity cost, time to enchant.
- Player confirms → `startDesign(gearInstanceId, selectedEffects[])` is called.
- Transitions to `currentAction: 'designing'`.
- On completion → transitions to `currentAction: 'meditate'`. Design is saved.
**Step 2 — Prepare**
- Player selects the piece of gear they want to prepare (the one they designed for).
- If gear already has enchantments → they are removed, mana is returned (scaled by Disenchanting skill).
- System shows mana cost for preparation.
- Player confirms → `startPreparation(gearInstanceId, designId)`.
- Transitions to `currentAction: 'preparing'`.
- On completion → transitions to `currentAction: 'meditate'`. Gear is marked "prepared".
**Step 3 — Apply**
- Player selects the prepared gear + matching design.
- System shows time cost, mana cost, XP gain.
- Player confirms → `startApplication(gearInstanceId, designId)`.
- Transitions to `currentAction: 'enchanting'`.
- On completion → enchantment applied, Enchanter XP gained, transitions to `currentAction: 'meditate'`.
---
### UI for Enchanting
The selection implementation must use the store as the single source of truth. Audit the `EnchantmentDesigner` component:
```typescript
// WRONG pattern — local state doesn't sync with store
const [selectedEffects, setSelectedEffects] = useState([]);
// ...
<EffectButton onClick={() => setSelectedEffects([...selectedEffects, effect])} />
// CORRECT pattern — store is the single source of truth
const selectedEffects = useCraftingStore(s => s.enchantmentDesignState.selectedEffects);
const toggleEffect = useCraftingStore(s => s.toggleEffectSelection);
// ...
<EffectButton onClick={() => toggleEffect(effect.id)} />
```
---
## Part 4 — Prestige System Rework
### Vision: Loop Memories + Path Bonuses
Instead of a generic idle-game upgrade shop, prestige is split into two parts:
#### Part A: Loop Memories (Keep)
The Memory system (preserving spells/skills between loops) is the best part of the prestige system. Keep it. Expand it slightly:
- **Memory Slots** persist across loops (deep memory prestige upgrade is fine).
- Memories can be: a skill level, a spell, a completed enchantment design, or an attunement XP chunk.
- Add "Memory Imprinting" — at loop end, player chooses which memories to keep.
#### Part B: Path Bonuses
Instead of one flat upgrade shop, give each **path** its own upgrade tree that unlocks when you commit to that path:
```
Combat Path Permanents:
- Veteran's Edge: Start each loop at floor 5 instead of 1
- Battle-Hardened: +10% pact multipliers carry forward
- Guardian's Boon: Guardian XP from last loop carries forward 25%
Crafting Path Permanents:
- Master Craftsman: 1 enchantment design persists across loops
- Runework Memory: Enchanter XP carries forward 30%
- Crafting Legacy: 1 crafted item persists per loop
Utility Path Permanents:
- Eternal Scholar: +20% starting mana per loop
- Time Mastery: Incursion starts 2 days later
- Insight Cascade: +15% insight per loop permanently
```
#### Part C: Universal Upgrades (Minimal)
Keep a small set of universal upgrades that any path can buy. These are just QoL, not power:
- Extra memory slot (+insight cost)
- UI options (loop history, achievement display)
- Starting equipment quality (common → uncommon after loop 5)
---
## Part 5 — UI Redesign
### Design Direction: Dark Arcane Codex
The game is about a mage in a time loop. The UI should feel like **a wizard's spellbook interface** — dark, deliberate, with glowing mana colors and a sense of weight and history.
**NOT:** Material Design, rounded pastel cards, generic dashboards, or Bootstrap tables.
**YES:** Dark background, warm amber/teal accent colors tied to the mana system, monospaced numbers for game stats, subtle texture via border treatments, clear information hierarchy.
---
### Design System
Define these tokens in `globals.css` before writing any component:
```css
/* Mana Loop Design Tokens */
:root {
/* Backgrounds */
--bg-void: #0d0d0f; /* Page background */
--bg-panel: #141418; /* Panel background */
--bg-surface: #1c1c22; /* Card/surface background */
--bg-raised: #242430; /* Elevated elements */
/* Text */
--text-primary: #e8e6dc; /* Main content */
--text-secondary: #9e9c90; /* Labels, captions */
--text-muted: #5e5c56; /* Disabled, placeholder */
/* Mana Colors (tie to game elements) */
--mana-raw: #8b7fd4; /* Raw mana — purple */
--mana-fire: #e85d24; /* Fire — orange-red */
--mana-water: #2ea8c4; /* Water — teal */
--mana-air: #a8d4e8; /* Air — pale blue */
--mana-earth: #b07d3c; /* Earth — amber-brown */
--mana-light: #e8c84a; /* Light — gold */
--mana-dark: #7a4db0; /* Dark — deep purple */
--mana-death: #6e8a96; /* Death — grey-blue */
--mana-transference: #1abc9c;/* Transference — teal-green */
/* Semantic */
--color-success: #4caf7d;
--color-warning: #e8a84a;
--color-danger: #c44b3a;
--color-info: var(--mana-raw);
/* Borders */
--border-subtle: rgba(255,255,255,0.06);
--border-default: rgba(255,255,255,0.12);
--border-accent: rgba(255,255,255,0.22);
/* Typography */
--font-display: 'Cinzel', serif; /* Headings, tab names */
--font-body: 'Source Serif 4', serif; /* Prose text, descriptions */
--font-ui: 'JetBrains Mono', monospace; /* Stats, numbers, game values */
/* Spacing */
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 10px;
}
```
**Font sourcing:** All available via Google Fonts. Add to `layout.tsx`:
```typescript
import { Cinzel, Source_Serif_4, JetBrains_Mono } from 'next/font/google';
```
---
### Component Guidelines
**Stats and numbers** → always `font-family: var(--font-ui)`. Numbers should look precise, not soft.
**Tab headers**`font-family: var(--font-display)`, muted color normally, accent color when active. No underlines or pills — use a subtle left or bottom border.
**Descriptions and lore**`font-family: var(--font-body)`. The game has narrative flavor; let descriptions read like a spellbook.
**Progress bars** → use the element colors. A mana bar is `--mana-raw`. A fire element bar is `--mana-fire`. The color is the information.
**Panels**`--bg-panel` background with a `1px solid var(--border-subtle)` border. No drop shadows. Use spacing to create hierarchy, not shadows.
**Buttons** — Three variants:
```
Primary: bg --bg-raised, border --border-accent, text --text-primary
Secondary: bg transparent, border --border-default, text --text-secondary
Danger: bg transparent, border --color-danger, text --color-danger
```
**Never use:** shadcn default styles without overriding, `rounded-full` for non-pill elements, white backgrounds, blue link colors, or any stock Tailwind color like `bg-blue-500`.
---
### Layout Rework
The current layout has a LeftPanel + main tabbed area. Keep this structure but rework the visual language:
```
┌──────────────────────────────────────────────────────────────┐
│ MANA LOOP Day 12 / 30 │ ← Top bar: game title, time
├──────────┬───────────────────────────────────────────────────┤
│ │ [Skills] [Spire] [Crafting] [Equipment] [...] │ ← Tab bar
│ STATUS ├───────────────────────────────────────────────────┤
│ PANEL │ │
│ │ ACTIVE TAB CONTENT │
│ Mana │ │
│ Elements│ │
│ Action │ │
│ Activity│ │
│ Log │ │
└──────────┴───────────────────────────────────────────────────┘
```
Left panel content (from top):
1. Mana display (raw mana bar + current/max)
2. Elemental mana bars (only show unlocked elements)
3. Current action with progress bar
4. Attunement status strip
5. Activity log (scrollable, last 20 events)
---
### UI Implementation Order
1. `globals.css` — design tokens only. No component styles yet.
2. Left panel redesign (most-seen element).
3. Tab bar redesign.
4. Mana display component.
5. Skill tab (most complex, do last after skill system redesign).
6. Equipment tab.
7. Enchanting crafting tab.
Each component gets its own TASK.md. The agent must not redesign multiple components in one task.
---
## Execution Sequence
Work in this order. Do not start a phase until the previous phase's acceptance criteria are met.
```
Phase 0 ── E2E test coverage + validate existing systems
│ DONE WHEN: enchanting flow, gear equipping, and combat all have passing E2E tests
│ GATE: all E2E tests green, no regressions
Phase 1 ── Skill system redesign (Part 1 above)
│ DONE WHEN: computeStats() replaces all skill-evolution-modules/
│ GATE: all unit tests pass, no regression in game behaviour
Phase 2 ── Enchanting UI (Part 3 above)
│ DONE WHEN: 3-step flow works with store as single source of truth
│ GATE: enchanting E2E test passes
Phase 3 ── UI design system (Part 5 above — tokens + left panel only)
│ DONE WHEN: design tokens defined, left panel redesigned
│ GATE: no functional regression
Phase 4 ── Attunement expansion (Part 2 above)
│ DONE WHEN: new attunements defined, path choice works at prestige
│ GATE: attunement store tests pass
Phase 5 ── Prestige rework (Part 4 above — path bonuses)
│ DONE WHEN: path bonuses replace generic shop (or coexist cleanly)
│ GATE: prestige store tests pass
Phase 6 ── Full UI redesign (Part 5 above — all remaining tabs)
DONE WHEN: all tabs use new design system
GATE: visual review + E2E tests still pass
```
---
## E2E Test Plan (Playwright) — Priority Order
These tests validate that core gameplay loops work correctly and remain stable. Each test should be written **before** any related implementation work begins (TDD).
```typescript
// e2e/enchanting.spec.ts
test('can select enchantment effect from unlocked pool', async ({ page }) => {
// Navigate to enchanting tab
// Click an available effect
// Assert it appears in the design panel with correct capacity cost
});
test('can complete full 3-step enchant flow', async ({ page }) => {
// Design → Prepare → Apply
// Assert enchantment is applied to the gear and Enchanter XP increased
});
test('cannot select locked enchantment effects', async ({ page }) => {
// Assert unresearched effects are visually disabled / non-interactive
});
// e2e/equipment.spec.ts
test('equipping item updates the correct equipment slot', async ({ page }) => {
// Pick up an item → click a slot → assert slot shows the item
});
test('2-handed weapon blocks offhand slot', async ({ page }) => {
// Equip 2H weapon → assert offhand is greyed out / blocked
});
test('unequipping item returns it to inventory', async ({ page }) => {
// Remove item from slot → assert it appears in inventory
});
// e2e/combat.spec.ts
test('spell cast progress advances over time during combat', async ({ page }) => {
// Enter combat → wait → assert cast progress bar has advanced
});
test('enemy HP decreases on spell completion', async ({ page }) => {
// Complete a spell cast → assert enemy HP is reduced by expected amount
});
test('defeating all enemies on a floor advances to next floor', async ({ page }) => {
// Kill last enemy → assert floor counter increments and new enemies appear
});
test('death resets to correct floor on reincarnation', async ({ page }) => {
// Die → reincarnate → assert floor reset matches prestige expectations
});
```
---
## Task Structure for the Agent
For each phase, create individual TASK.md files. Keep each task under 200 lines of code change. Example structure:
```
docs/tasks/
TASK-001-playwright-setup.md
TASK-002-enchanting-e2e-tests.md
TASK-003-equipment-e2e-tests.md
TASK-004-combat-e2e-tests.md
TASK-005-globals-css-tokens.md
TASK-006-left-panel-redesign.md
...
```
Each task file follows the TASK_TEMPLATE.md format. The agent receives ONE task at a time. After it's committed, you verify it, then send the next task.
**Prevent blast radius:** The "Files NOT to Touch" field in each task is critical. The combat tests should not touch the enchanting files. The UI redesign should not touch the store. Explicit constraints prevent the agent from "helpfully" refactoring adjacent code.
---
## Quick Reference: First 5 Tasks
If you're starting today, create these tasks in order:
1. **TASK-001-playwright-setup.md** — Add Playwright to the project, configure `playwright.config.ts`, establish baseline test runner.
2. **TASK-002-enchanting-e2e-tests.md** — Write E2E tests covering the 3-step enchant flow and effect selection. Must pass.
3. **TASK-003-equipment-e2e-tests.md** — Write E2E tests for gear equipping, 2H weapon slot blocking, and unequip-to-inventory. Must pass.
4. **TASK-004-combat-e2e-tests.md** — Write E2E tests for spell casting progression, enemy HP reduction, and floor advancement. Must pass.
5. **TASK-005-globals-css-tokens.md** — Define the design tokens in `globals.css`. No component styles yet.
Get those 5 done and you'll have validated gameplay with a solid test safety net and the foundation for the visual redesign. Everything else is iterative improvement.
-58
View File
@@ -1,58 +0,0 @@
# TASK-001: Playwright Setup + Baseline E2E Tests
## Objective
Add Playwright E2E testing to the Mana Loop project and create baseline tests that validate core gameplay systems work correctly. This establishes the test safety net required before any refactoring work begins.
## Acceptance Criteria
1. Playwright is installed and configured (`playwright.config.ts` exists)
2. `e2e/` directory exists with at least 3 passing test files
3. All baseline E2E tests pass (`npx playwright test` succeeds)
4. Tests cover: enchanting flow (3-step), equipment equipping (2H block), and combat progression
## Tasks
### Step 1: Install Playwright and create config
- Run `npx playwright install` and add `@playwright/test` to devDependencies
- Create `playwright.config.ts` with appropriate viewport, baseURL, and testDir settings
- Verify: `npx playwright --version` works
- Files: `package.json`, `playwright.config.ts`
### Step 2: Create baseline enchanting E2E test
- Create `e2e/enchanting.spec.ts` testing:
- Page loads and game initializes
- Player can navigate to Crafting tab
- Effect selection works (select an effect from unlocked pool)
- Design → Prepare → Apply flow completes
- File: `e2e/enchanting.spec.ts`
### Step 3: Create baseline equipment E2E test
- Create `e2e/equipment.spec.ts` testing:
- Player can equip items from inventory
- 2H weapon blocks offhand slot
- Unequipping returns item to inventory
- File: `e2e/equipment.spec.ts`
### Step 4: Create baseline combat E2E test
- Create `e2e/combat.spec.ts` testing:
- Player enters combat (clicks "Climb the Spire")
- Spell casting progresses over time
- Enemy HP decreases on spell completion
- Floor advances after clearing
- File: `e2e/combat.spec.ts`
### Step 5: Run tests and fix issues
- Run `npx playwright test` and ensure all tests pass
- Run `npx playwright test --headed` to visually verify if needed
- Fix any test flakes or timing issues
## Files to be touched
- `package.json` — add @playwright/test dependency
- `playwright.config.ts` — NEW file
- `e2e/enchanting.spec.ts` — NEW file
- `e2e/equipment.spec.ts` — NEW file
- `e2e/combat.spec.ts` — NEW file
## Dependencies
- None (first task in sequence)
## Time Estimate: ~2 hours
-1
View File
@@ -1 +0,0 @@
Here are all the generated files.
-196
View File
@@ -1,196 +0,0 @@
'use client';
import { useEffect, useState } from 'react';
import { io } from 'socket.io-client';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ScrollArea } from '@/components/ui/scroll-area';
type User = {
id: string;
username: string;
}
type Message = {
id: string;
username: string;
content: string;
timestamp: Date | string;
type: 'user' | 'system';
}
export default function SocketDemo() {
const [messages, setMessages] = useState<Message[]>([]);
const [inputMessage, setInputMessage] = useState('');
const [username, setUsername] = useState('');
const [isUsernameSet, setIsUsernameSet] = useState(false);
const [socket, setSocket] = useState<any>(null);
const [isConnected, setIsConnected] = useState(false);
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
// Connect to websocket server
// Never use PORT in the URL, alyways use XTransformPort
// DO NOT change the path, it is used by Caddy to forward the request to the correct port
const socketInstance = io('/?XTransformPort=3003', {
transports: ['websocket', 'polling'],
forceNew: true,
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
timeout: 10000
})
setSocket(socketInstance);
socketInstance.on('connect', () => {
setIsConnected(true);
});
socketInstance.on('disconnect', () => {
setIsConnected(false);
});
socketInstance.on('message', (msg: Message) => {
setMessages(prev => [...prev, msg]);
});
socketInstance.on('user-joined', (data: { user: User; message: Message }) => {
setMessages(prev => [...prev, data.message]);
setUsers(prev => {
if (!prev.find(u => u.id === data.user.id)) {
return [...prev, data.user];
}
return prev;
});
});
socketInstance.on('user-left', (data: { user: User; message: Message }) => {
setMessages(prev => [...prev, data.message]);
setUsers(prev => prev.filter(u => u.id !== data.user.id));
});
socketInstance.on('users-list', (data: { users: User[] }) => {
setUsers(data.users);
});
return () => {
socketInstance.disconnect();
};
}, []);
const handleJoin = () => {
if (socket && username.trim() && isConnected) {
socket.emit('join', { username: username.trim() });
setIsUsernameSet(true);
}
};
const sendMessage = () => {
if (socket && inputMessage.trim() && username.trim()) {
socket.emit('message', {
content: inputMessage.trim(),
username: username.trim()
});
setInputMessage('');
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
sendMessage();
}
};
return (
<div className="container mx-auto p-4 max-w-2xl">
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
WebSocket Demo
<span className={`text-sm px-2 py-1 rounded ${isConnected ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
{isConnected ? 'Connected' : 'Disconnected'}
</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{!isUsernameSet ? (
<div className="space-y-2">
<Input
value={username}
onChange={(e) => setUsername(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleJoin();
}
}}
placeholder="Enter your username..."
disabled={!isConnected}
className="flex-1"
/>
<Button
onClick={handleJoin}
disabled={!isConnected || !username.trim()}
className="w-full"
>
Join Chat
</Button>
</div>
) : (
<>
<ScrollArea className="h-80 w-full border rounded-md p-4">
<div className="space-y-2">
{messages.length === 0 ? (
<p className="text-gray-500 text-center">No messages yet</p>
) : (
messages.map((msg) => (
<div key={msg.id} className="border-b pb-2 last:border-b-0">
<div className="flex justify-between items-start">
<div className="flex-1">
<p className={`text-sm font-medium ${msg.type === 'system'
? 'text-blue-600 italic'
: 'text-gray-700'
}`}>
{msg.username}
</p>
<p className={`${msg.type === 'system'
? 'text-blue-500 italic'
: 'text-gray-900'
}`}>
{msg.content}
</p>
</div>
<span className="text-xs text-gray-500">
{new Date(msg.timestamp).toLocaleTimeString()}
</span>
</div>
</div>
))
)}
</div>
</ScrollArea>
<div className="flex space-x-2">
<Input
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Type a message..."
disabled={!isConnected}
className="flex-1"
/>
<Button
onClick={sendMessage}
disabled={!isConnected || !inputMessage.trim()}
>
Send
</Button>
</div>
</>
)}
</CardContent>
</Card>
</div>
);
}
-138
View File
@@ -1,138 +0,0 @@
import { createServer } from 'http'
import { Server } from 'socket.io'
const httpServer = createServer()
const io = new Server(httpServer, {
// DO NOT change the path, it is used by Caddy to forward the request to the correct port
path: '/',
cors: {
origin: "*",
methods: ["GET", "POST"]
},
pingTimeout: 60000,
pingInterval: 25000,
})
interface User {
id: string
username: string
}
interface Message {
id: string
username: string
content: string
timestamp: Date
type: 'user' | 'system'
}
const users = new Map<string, User>()
const generateMessageId = () => Math.random().toString(36).substr(2, 9)
const createSystemMessage = (content: string): Message => ({
id: generateMessageId(),
username: 'System',
content,
timestamp: new Date(),
type: 'system'
})
const createUserMessage = (username: string, content: string): Message => ({
id: generateMessageId(),
username,
content,
timestamp: new Date(),
type: 'user'
})
io.on('connection', (socket) => {
console.log(`User connected: ${socket.id}`)
// Add test event handler
socket.on('test', (data) => {
console.log('Received test message:', data)
socket.emit('test-response', {
message: 'Server received test message',
data: data,
timestamp: new Date().toISOString()
})
})
socket.on('join', (data: { username: string }) => {
const { username } = data
// Create user object
const user: User = {
id: socket.id,
username
}
// Add to user list
users.set(socket.id, user)
// Send join message to all users
const joinMessage = createSystemMessage(`${username} joined the chat room`)
io.emit('user-joined', { user, message: joinMessage })
// Send current user list to new user
const usersList = Array.from(users.values())
socket.emit('users-list', { users: usersList })
console.log(`${username} joined the chat room, current online users: ${users.size}`)
})
socket.on('message', (data: { content: string; username: string }) => {
const { content, username } = data
const user = users.get(socket.id)
if (user && user.username === username) {
const message = createUserMessage(username, content)
io.emit('message', message)
console.log(`${username}: ${content}`)
}
})
socket.on('disconnect', () => {
const user = users.get(socket.id)
if (user) {
// Remove from user list
users.delete(socket.id)
// Send leave message to all users
const leaveMessage = createSystemMessage(`${user.username} left the chat room`)
io.emit('user-left', { user: { id: socket.id, username: user.username }, message: leaveMessage })
console.log(`${user.username} left the chat room, current online users: ${users.size}`)
} else {
console.log(`User disconnected: ${socket.id}`)
}
})
socket.on('error', (error) => {
console.error(`Socket error (${socket.id}):`, error)
})
})
const PORT = 3003
httpServer.listen(PORT, () => {
console.log(`WebSocket server running on port ${PORT}`)
})
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('Received SIGTERM signal, shutting down server...')
httpServer.close(() => {
console.log('WebSocket server closed')
process.exit(0)
})
})
process.on('SIGINT', () => {
console.log('Received SIGINT signal, shutting down server...')
httpServer.close(() => {
console.log('WebSocket server closed')
process.exit(0)
})
})
-134
View File
@@ -1,134 +0,0 @@
#!/usr/bin/env python3
import re
def add_debugname_wrapper(file_path, component_name):
"""Add DebugName import and wrap the main return with DebugName"""
print(f"Processing {file_path} for {component_name}...")
with open(file_path, 'r') as f:
content = f.read()
# Check if DebugName is already imported
if 'from \'@/lib/game/debug-context\'' in content or 'from "@/lib/game/debug-context"' in content:
print(f" - DebugName already imported")
else:
# Find the last import line and add the DebugName import after it
lines = content.split('\n')
last_import_idx = -1
for i, line in enumerate(lines):
if line.startswith('import ') or 'import {' in line:
last_import_idx = i
if last_import_idx >= 0:
lines.insert(last_import_idx + 1, "import { DebugName } from '@/lib/game/debug-context';")
content = '\n'.join(lines)
print(f" - Added DebugName import")
else:
print(f" - WARNING: No import found")
return False
# Now find the main return statement and wrap it
# The return statement should be: return ( \n <something>
# We need to wrap the entire JSX returned
# Find where the return statement starts
# Look for "return (" followed by newline
return_pattern = r'(return\s*\(\s*\n)'
match = re.search(return_pattern, content)
if not match:
print(f" - WARNING: Could not find return pattern")
return False
# Find the matching closing parenthesis for the return
# We need to count parentheses to find the correct closing one
start_pos = match.end()
# Insert <DebugName name="..."> after the return (
before_return = content[:match.end()]
after_return = content[match.end():]
# Add opening DebugName tag with proper indentation
# Find the indentation of the first line after return
lines_after = after_return.split('\n')
first_line_indent = ''
for char in lines_after[0]:
if char == ' ':
first_line_indent += ' '
else:
break
# Add the opening tag
opening_tag = f"{first_line_indent}<DebugName name=\"{component_name}\">\n"
modified = before_return + opening_tag + after_return
# Now find the closing ); for the return statement and add </DebugName> before it
# We need to find the matching closing paren for return (
# Let's find the position of the return (
return_start = modified.find('return (')
if return_start == -1:
print(f" - WARNING: Could not find 'return ('")
return False
# Find matching closing paren
paren_count = 0
in_string = False
string_char = None
i = return_start + len('return (')
while i < len(modified):
char = modified[i]
if in_string:
if char == string_char and modified[i-1] != '\\':
in_string = False
i += 1
continue
if char == '"' or char == "'":
in_string = True
string_char = char
elif char == '(':
paren_count += 1
elif char == ')':
if paren_count == 0:
# This is the matching closing paren
# Check if followed by ;
if i + 1 < len(modified) and modified[i+1] == ';':
# Insert </DebugName> before this )
before_close = modified[:i]
after_close = modified[i:]
# Get indentation
lines_before = before_close.split('\n')
last_line = lines_before[-1]
indent = ''
for char in last_line:
if char == ' ':
indent += ' '
else:
break
closing_tag = f"\n{indent}</DebugName>"
modified = before_close + closing_tag + after_close
print(f" - Successfully wrapped {component_name} with DebugName")
break
else:
# Just a normal paren
pass
else:
paren_count -= 1
i += 1
# Write back
with open(file_path, 'w') as f:
f.write(modified)
return True
# Fix CraftingTab and EquipmentTab
add_debugname_wrapper('src/components/game/tabs/CraftingTab.tsx', 'CraftingTab')
add_debugname_wrapper('src/components/game/tabs/EquipmentTab.tsx', 'EquipmentTab')
print("\nDone with tabs!")
-104
View File
@@ -1,104 +0,0 @@
#!/usr/bin/env python3
import re
def fix_tab_file(file_path, component_name):
"""Add DebugName import and wrap the main return with DebugName"""
print(f"Processing {file_path} for {component_name}...")
with open(file_path, 'r') as f:
content = f.read()
# Check if DebugName is already imported
if "from '@/lib/game/debug-context'" in content:
print(f" - DebugName already imported")
else:
# Find the last import line and add the DebugName import after it
lines = content.split('\n')
last_import_idx = -1
for i, line in enumerate(lines):
if line.startswith('import ') or line.strip().startswith('import {'):
last_import_idx = i
if last_import_idx >= 0:
# Check if next line is also part of import
if i + 1 < len(lines) and (lines[i+1].strip().startswith('} from') or lines[i+1].strip() == '}'):
# Multi-line import, find the closing
for j in range(i+1, len(lines)):
if '} from' in lines[j]:
last_import_idx = j
break
if last_import_idx >= 0:
lines.insert(last_import_idx + 1, "import { DebugName } from '@/lib/game/debug-context';")
content = '\n'.join(lines)
print(f" - Added DebugName import")
else:
print(f" - WARNING: No import found")
return False
# Now find the main return statement (not early returns)
# Look for "return (" followed by newline and then some JSX
pattern = r'(export function \w+\(\)\s*\{.*?return\s*\(\s*\n)(\s*)(<)'
match = re.search(pattern, content, re.DOTALL)
if not match:
print(f" - WARNING: Could not find main return pattern")
return False
# Get the indentation
indent = match.group(2)
# Add opening DebugName tag after the return (
before_return = content[:match.end(1)]
after_return = content[match.end(1):]
# Add <DebugName name="..."> after the return (
modified = before_return + f'{indent}<DebugName name="{component_name}">\n{indent}{after_return[0]}'
# Now find the closing ); for the main return
# Find "displayName" to locate the end of the component
display_pattern = re.escape(component_name) + r'\.displayName\s*=\s*[\'"].*?[\'"];\s*\n'
display_match = re.search(display_pattern, modified)
if not display_match:
print(f" - WARNING: Could not find displayName")
return False
# Find the ); before displayName
before_display = modified[:display_match.start()]
after_display = modified[display_match.start():]
# Find the last ); in before_display that's at the start of a line
lines_before = before_display.split('\n')
close_paren_line = -1
close_paren_indent = ''
for i in range(len(lines_before) - 1, -1, -1):
line = lines_before[i]
if line.strip() == ');':
close_paren_line = i
close_paren_indent = line[:len(line) - len(line.lstrip())]
break
if close_paren_line == -1:
print(f" - WARNING: Could not find closing );\")
return False
# Insert </DebugName> before );
lines_before.insert(close_paren_line, f"{close_paren_indent}</DebugName>")
before_display_fixed = '\n'.join(lines_before)
modified = before_display_fixed + after_display
# Write back
with open(file_path, 'w') as f:
f.write(modified)
print(f" - Successfully wrapped {component_name} with DebugName")
return True
# Fix the remaining files
fix_tab_file('src/components/game/tabs/GolemancyTab.tsx', 'GolemancyTab')
fix_tab_file('src/components/game/tabs/SpellsTab.tsx', 'SpellsTab')
print("\nDone!")
-174
View File
@@ -1,174 +0,0 @@
#!/usr/bin/env python3
import re
def fix_tab_file(file_path, component_name):
"""Add DebugName import and wrap the main return with DebugName"""
print(f"Processing {file_path} for {component_name}...")
with open(file_path, 'r') as f:
content = f.read()
# Check if DebugName is already imported
if "from '@/lib/game/debug-context'" in content:
print(f" - DebugName already imported")
else:
# Find the last import line and add the DebugName import after it
lines = content.split('\n')
last_import_idx = -1
for i, line in enumerate(lines):
stripped = line.strip()
if stripped.startswith('import ') or (stripped.startswith('{') and 'from' in stripped):
last_import_idx = i
elif stripped.startswith('} from') or stripped.endswith(';'):
# End of multi-line import
last_import_idx = i
if last_import_idx >= 0:
lines.insert(last_import_idx + 1, "import { DebugName } from '@/lib/game/debug-context';")
content = '\n'.join(lines)
print(f" - Added DebugName import")
else:
print(f" - WARNING: No import found")
return False
# Find the component function and its return statement
# Look for "export function ComponentName()" or "function ComponentName()"
func_pattern = r'(export\s+)?function\s+' + re.escape(component_name) + r'\([^)]*\)\s*\{'
func_match = re.search(func_pattern, content)
if not func_match:
print(f" - WARNING: Could not find function {component_name}")
return False
# Find the return statement after the function start
func_start = func_match.end()
content_after_func = content[func_start:]
# Find the main return (not inside an if/else)
# Look for "return (" at the start of a line (with possible whitespace)
return_pattern = r'(\n\s*)return\s*\(\s*\n'
return_match = re.search(return_pattern, content_after_func)
if not return_match:
print(f" - WARNING: Could not find return pattern")
return False
# Get the indentation before return
indent = return_match.group(1)
# Insert <DebugName name="..."> after the return (
# The return_match gives us the position relative to content_after_func
# We need to insert in the full content
insert_pos = func_start + return_match.end()
# Back up to the newline before the JSX starts
# The return_match ends with \n, so the next line is the start of JSX
# We need to add <DebugName> before that JSX
# Find the first non-empty line after return (
remaining = content[insert_pos:]
lines_after = remaining.split('\n')
first_line = None
for i, line in enumerate(lines_after):
if line.strip():
first_line = line
first_line_idx = i
break
if first_line is None:
print(f" - WARNING: Could not find JSX after return")
return False
# Get the indentation of the first JSX line
jsx_indent = ''
for char in first_line:
if char == ' ':
jsx_indent += ' '
else:
break
# Insert <DebugName> before the first JSX line
# We need to insert at: func_start + return_match.end() + position of first JSX line
# Actually, let's just insert the DebugName tag right after return (
# and before the first <tag
debug_name_open = f"\n{jsx_indent}<DebugName name=\"{component_name}\">"
# Find where to insert (after the newline following return ()
# The return_match ends with \n, so we insert at insert_pos
before_insert = content[:insert_pos]
after_insert = content[insert_pos:]
# Add the opening DebugName tag
modified = before_insert + debug_name_open + '\n' + after_insert
# Now find the closing ); for the return statement
# We need to find the matching ) for the return (
# Find the position of the return ( in modified
return_pos = modified.find('return (', func_start)
if return_pos == -1:
print(f" - WARNING: Could not find 'return ('")
return False
# Find matching closing paren
paren_count = 0
in_string = False
string_char = None
i = return_pos + len('return (')
while i < len(modified):
char = modified[i]
if in_string:
if char == string_char and (i == 0 or modified[i-1] != '\\'):
in_string = False
i += 1
continue
if char == '"' or char == "'":
in_string = True
string_char = char
elif char == '(':
paren_count += 1
elif char == ')':
if paren_count == 0:
# This is the matching closing paren
# Check if followed by ;
if i + 1 < len(modified) and modified[i+1] == ';':
# Insert </DebugName> before this )
before_close = modified[:i]
after_close = modified[i:]
# Get indentation
lines_before = before_close.split('\n')
last_line = lines_before[-1]
close_indent = ''
for char in last_line:
if char == ' ':
close_indent += ' '
else:
break
closing_tag = f"\n{close_indent}</DebugName>"
modified = before_close + closing_tag + after_close
print(f" - Successfully wrapped {component_name} with DebugName")
break
else:
# Just a normal paren
pass
else:
paren_count -= 1
i += 1
# Write back
with open(file_path, 'w') as f:
f.write(modified)
return True
# Fix the remaining files
fix_tab_file('src/components/game/tabs/GolemancyTab.tsx', 'GolemancyTab')
fix_tab_file('src/components/game/tabs/SpellsTab.tsx', 'SpellsTab')
print("\nDone!")
View File
-10
View File
@@ -11,10 +11,6 @@
"test": "vitest",
"test:e2e": "playwright test",
"test:coverage": "vitest --coverage",
"db:push": "prisma db push",
"db:generate": "prisma generate",
"db:migrate": "prisma migrate dev",
"db:reset": "prisma migrate reset",
"prepare": "husky"
},
"dependencies": {
@@ -22,8 +18,6 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@hookform/resolvers": "^5.1.1",
"@mdxeditor/editor": "^3.39.1",
"@prisma/client": "^6.11.1",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-aspect-ratio": "^1.1.7",
@@ -63,10 +57,7 @@
"input-otp": "^1.4.2",
"lucide-react": "^0.525.0",
"next": "^16.1.1",
"next-auth": "^4.24.11",
"next-intl": "^4.3.4",
"next-themes": "^0.4.6",
"prisma": "^6.11.1",
"react": "^19.0.0",
"react-day-picker": "^9.8.0",
"react-dom": "^19.0.0",
@@ -81,7 +72,6 @@
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.1.0",
"vaul": "^1.1.2",
"z-ai-web-dev-sdk": "^0.0.17",
"zod": "^4.0.2",
"zustand": "^5.0.6"
},
@@ -0,0 +1,285 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: combat.spec.ts >> Combat System >> shows floor information in spire mode
- Location: e2e/combat.spec.ts:65:7
# Error details
```
Error: expect(locator).toBeVisible() failed
Locator: locator('text="Floor"').first()
Expected: visible
Timeout: 5000ms
Error: element(s) not found
Call log:
- Expect "toBeVisible" with timeout 5000ms
- waiting for locator('text="Floor"').first()
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e2]:
- banner [ref=e3]:
- generic [ref=e4]:
- heading "MANA LOOP" [level=1] [ref=e5]
- generic [ref=e7]:
- generic [ref=e8]:
- generic [ref=e9]: Day 1
- generic [ref=e10]: 02:04
- generic [ref=e11]:
- generic [ref=e12]: "0"
- generic [ref=e13]: Insight
- main [ref=e14]:
- generic [ref=e15]:
- generic [ref=e17]:
- generic [ref=e18]:
- generic [ref=e19]:
- generic [ref=e20]: "15"
- generic [ref=e21]: / 100
- generic [ref=e22]:
- text: +3.0 mana/hr
- generic [ref=e23]: (1.5x med)
- progressbar [ref=e24]
- button "Gather +1 Mana" [ref=e26]:
- img
- text: Gather +1 Mana
- generic [ref=e27]:
- button "Elemental Mana (1)" [ref=e28]:
- generic [ref=e29]: Elemental Mana (1)
- img [ref=e30]
- generic [ref=e33]:
- generic [ref=e34]:
- generic [ref=e35]: 🔗
- generic [ref=e36]: Transference
- generic [ref=e39]: 0/10
- generic [ref=e40]:
- generic [ref=e41]: "1"
- generic [ref=e42]: "2"
- generic [ref=e43]: "3"
- generic [ref=e44]: "4"
- generic [ref=e45]: "5"
- generic [ref=e46]: "6"
- generic [ref=e47]: "7"
- generic [ref=e48]: "8"
- generic [ref=e49]: "9"
- generic [ref=e50]: "10"
- generic [ref=e51]: "11"
- generic [ref=e52]: "12"
- generic [ref=e53]: "13"
- generic [ref=e54]: "14"
- generic [ref=e55]: "15"
- generic [ref=e56]: "16"
- generic [ref=e57]: "17"
- generic [ref=e58]: "18"
- generic [ref=e59]: "19"
- generic [ref=e60]: "20"
- generic [ref=e61]: "21"
- generic [ref=e62]: "22"
- generic [ref=e63]: "23"
- generic [ref=e64]: "24"
- generic [ref=e65]: "25"
- generic [ref=e66]: "26"
- generic [ref=e67]: "27"
- generic [ref=e68]: "28"
- generic [ref=e69]: "29"
- generic [ref=e70]: "30"
- generic [ref=e72]:
- tablist [ref=e73]:
- tab "⚔️ Spire" [selected] [ref=e74]
- tab "✨ Attune" [ref=e75]
- tab "🗿 Golems" [ref=e76]
- tab "📚 Skills" [ref=e77]
- tab "🔮 Spells" [ref=e78]
- tab "🛡️ Gear" [ref=e79]
- tab "🔧 Craft" [ref=e80]
- tab "💎 Loot" [ref=e81]
- tab "🏆 Achieve" [ref=e82]
- tab "📊 Stats" [ref=e83]
- tab "🐛 Debug" [ref=e84]
- tab "📖 Grimoire" [ref=e85]
- tabpanel "⚔️ Spire" [ref=e86]:
- generic [ref=e87]:
- generic [ref=e89]:
- button "Exit Spire Mode" [ref=e90]:
- img
- text: Exit Spire Mode
- generic [ref=e91]: Climb down to floor 1 to return to the main game
- generic [ref=e92]:
- heading "Current Floor 🐝 Swarm" [level=3] [ref=e94]:
- generic [ref=e95]: Current Floor
- generic [ref=e96]: 🐝 Swarm
- generic [ref=e97]:
- generic [ref=e98]:
- generic [ref=e99]: "1"
- generic [ref=e100]: / 100
- generic [ref=e101]: 🔥 Fire
- generic [ref=e102]:
- text: "Best: Floor"
- strong [ref=e103]: "1"
- text: "• Pacts:"
- strong [ref=e104]: "0"
- generic [ref=e106]:
- generic [ref=e108]: Active Spells (1)
- generic [ref=e110]:
- generic [ref=e111]:
- generic [ref=e112]: Mana BoltBasic
- generic [ref=e113]:
- generic [ref=e114]: ⚔️ 5 dmg • 3 raw • ⚡ 15 dmg/hr
- generic [ref=e115]:
- generic [ref=e116]: Swarm Enemies (6)
- generic [ref=e118]:
- generic [ref=e119]:
- img [ref=e120]
- generic [ref=e125]: Emberling
- generic [ref=e126]: 🔥 60/60 HP
- generic [ref=e130]:
- generic [ref=e131]:
- img [ref=e132]
- generic [ref=e137]: Fire Imp
- generic [ref=e138]: 🔥 60/60 HP
- generic [ref=e142]:
- generic [ref=e143]:
- img [ref=e144]
- generic [ref=e149]: Scorchling
- generic [ref=e150]: 🔥 60/60 HP
- generic [ref=e154]:
- generic [ref=e155]:
- img [ref=e156]
- generic [ref=e161]: Flame Sprite
- generic [ref=e162]: 🔥 60/60 HP
- generic [ref=e166]:
- generic [ref=e167]:
- img [ref=e168]
- generic [ref=e173]: Emberling
- generic [ref=e174]: 🔥 60/60 HP
- generic [ref=e178]:
- generic [ref=e179]:
- img [ref=e180]
- generic [ref=e185]: Inferno Whelp
- generic [ref=e186]: 🔥 60/60 HP
- generic [ref=e189]:
- generic [ref=e191]: Floor Navigation
- generic [ref=e192]:
- generic [ref=e193]:
- button "Climb Up" [ref=e194]:
- img
- text: Climb Up
- button "Climb Down" [disabled]:
- img
- text: Climb Down
- generic [ref=e195]: Click Climb Up/Down to begin climbing
- generic [ref=e196]:
- generic [ref=e198]: Combat Stats
- generic [ref=e199]:
- generic [ref=e200]: "Total DPS: —"
- generic [ref=e201]:
- generic [ref=e202]: Active Spells
- generic [ref=e203]:
- generic [ref=e204]:
- generic [ref=e205]:
- text: Mana Bolt
- generic [ref=e206]: Basic
- generic [ref=e207]:
- generic [ref=e208]: ⚔️ 5 dmg • 3 raw • ⚡ 15 dmg/hr
- generic [ref=e210]: "Study Speed: 100%"
- generic [ref=e211]:
- generic [ref=e213]: Activity Log
- generic [ref=e219]: No activity yet...
- region "Notifications (F8)":
- list
- region "Notifications (F8)":
- list
- button "Open Next.js Dev Tools" [ref=e225] [cursor=pointer]:
- img [ref=e226]
- alert [ref=e229]
```
# Test source
```ts
1 | import { test, expect } from '@playwright/test';
2 |
3 | /**
4 | * E2E tests for combat system:
5 | * - Entering spire mode (climbing)
6 | * - Casting spells and seeing progress
7 | * - Enemy HP reduction
8 | * - Floor advancement
9 | */
10 |
11 | test.describe('Combat System', () => {
12 | test.beforeEach(async ({ page }) => {
13 | await page.goto('/');
14 | // Clear game state to ensure a fresh start
15 | await page.evaluate(() => {
16 | Object.keys(localStorage)
17 | .filter((k) => k.startsWith('mana-loop-'))
18 | .forEach((k) => localStorage.removeItem(k));
19 | });
20 | await page.reload();
21 | await page.waitForLoadState('networkidle');
22 | });
23 |
24 | test('can see the Spire tab and "Climb the Spire" button', async ({ page }) => {
25 | // The Spire tab uses an icon + text, so match by the tab role
26 | const spireTab = page.getByRole('tab', { name: /⚔️ Spire/ });
27 | await expect(spireTab).toBeVisible();
28 |
29 | // Main page should show "Climb the Spire" button
30 | const climbBtn = page.getByRole('button', { name: 'Climb the Spire' });
31 | await expect(climbBtn).toBeVisible();
32 | });
33 |
34 | test('can enter Spire mode by clicking Climb button', async ({ page }) => {
35 | // Click "Climb the Spire" button on the main page (via left panel)
36 | await page.getByRole('button', { name: 'Climb the Spire' }).click();
37 |
38 | // Should now see Spire mode UI elements
39 | // The "Enter Spire Mode" button appears when on the Spire tab
40 | const enterBtn = page.getByRole('button', { name: 'Enter Spire Mode' });
41 | await expect(enterBtn).toBeVisible({ timeout: 5000 });
42 | });
43 |
44 | test('can navigate to Spire tab', async ({ page }) => {
45 | // Click the Spire tab specifically (using role=tab to disambiguate)
46 | await page.getByRole('tab', { name: /⚔️ Spire/ }).click();
47 |
48 | // Should see Spire-specific UI
49 | const enterSpireBtn = page.getByRole('button', { name: 'Enter Spire Mode' });
50 | await expect(enterSpireBtn).toBeVisible({ timeout: 5000 });
51 | });
52 |
53 | test('can enter spire mode from the Spire tab', async ({ page }) => {
54 | await page.getByRole('tab', { name: /⚔️ Spire/ }).click();
55 |
56 | const enterBtn = page.getByRole('button', { name: 'Enter Spire Mode' });
57 | await expect(enterBtn).toBeEnabled();
58 | await enterBtn.click();
59 |
60 | // After entering, should see exit button
61 | const exitBtn = page.getByRole('button', { name: 'Exit Spire Mode' });
62 | await expect(exitBtn).toBeVisible({ timeout: 5000 });
63 | });
64 |
65 | test('shows floor information in spire mode', async ({ page }) => {
66 | await page.getByRole('tab', { name: /⚔️ Spire/ }).click();
67 | await page.getByRole('button', { name: 'Enter Spire Mode' }).click();
68 |
69 | // Should display floor number - look for "Floor" label or the floor counter
70 | const floorDisplay = page.locator('text="Floor"').first();
> 71 | await expect(floorDisplay).toBeVisible({ timeout: 5000 });
| ^ Error: expect(locator).toBeVisible() failed
72 | });
73 | });
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

@@ -0,0 +1,348 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: equipment.spec.ts >> Equipment Management >> can unequip an item from a slot
- Location: e2e/equipment.spec.ts:113:7
# Error details
```
Error: expect(locator).toBeVisible() failed
Locator: locator('text=Hands').locator('..').locator('button').first()
Expected: visible
Timeout: 5000ms
Error: element(s) not found
Call log:
- Expect "toBeVisible" with timeout 5000ms
- waiting for locator('text=Hands').locator('..').locator('button').first()
```
# Page snapshot
```yaml
- generic [ref=e1]:
- generic [ref=e2]:
- banner [ref=e3]:
- generic [ref=e4]:
- heading "MANA LOOP" [level=1] [ref=e5]
- generic [ref=e7]:
- generic [ref=e8]:
- generic [ref=e9]: Day 1
- generic [ref=e10]: 01:55
- generic [ref=e11]:
- generic [ref=e12]: "0"
- generic [ref=e13]: Insight
- main [ref=e14]:
- generic [ref=e15]:
- generic [ref=e17]:
- generic [ref=e18]:
- generic [ref=e19]:
- generic [ref=e20]: "14"
- generic [ref=e21]: / 100
- generic [ref=e22]:
- text: +2.8 mana/hr
- generic [ref=e23]: (1.4x med)
- progressbar [ref=e24]
- button "Gather +1 Mana" [ref=e26]:
- img
- text: Gather +1 Mana
- generic [ref=e27]:
- button "Elemental Mana (1)" [ref=e28]:
- generic [ref=e29]: Elemental Mana (1)
- img [ref=e30]
- generic [ref=e33]:
- generic [ref=e34]:
- generic [ref=e35]: 🔗
- generic [ref=e36]: Transference
- generic [ref=e39]: 0/10
- button "Climb the Spire" [ref=e40]:
- img
- text: Climb the Spire
- generic [ref=e42]:
- generic [ref=e43]:
- img [ref=e44]
- generic [ref=e46]: Current Activity
- generic [ref=e47]: Meditating
- generic [ref=e48]:
- generic [ref=e49]: "1"
- generic [ref=e50]: "2"
- generic [ref=e51]: "3"
- generic [ref=e52]: "4"
- generic [ref=e53]: "5"
- generic [ref=e54]: "6"
- generic [ref=e55]: "7"
- generic [ref=e56]: "8"
- generic [ref=e57]: "9"
- generic [ref=e58]: "10"
- generic [ref=e59]: "11"
- generic [ref=e60]: "12"
- generic [ref=e61]: "13"
- generic [ref=e62]: "14"
- generic [ref=e63]: "15"
- generic [ref=e64]: "16"
- generic [ref=e65]: "17"
- generic [ref=e66]: "18"
- generic [ref=e67]: "19"
- generic [ref=e68]: "20"
- generic [ref=e69]: "21"
- generic [ref=e70]: "22"
- generic [ref=e71]: "23"
- generic [ref=e72]: "24"
- generic [ref=e73]: "25"
- generic [ref=e74]: "26"
- generic [ref=e75]: "27"
- generic [ref=e76]: "28"
- generic [ref=e77]: "29"
- generic [ref=e78]: "30"
- generic [ref=e80]:
- tablist [ref=e81]:
- tab "⚔️ Spire" [ref=e82]
- tab "✨ Attune" [ref=e83]
- tab "🗿 Golems" [ref=e84]
- tab "📚 Skills" [ref=e85]
- tab "🔮 Spells" [ref=e86]
- tab "🛡️ Gear" [active] [selected] [ref=e87]
- tab "🔧 Craft" [ref=e88]
- tab "💎 Loot" [ref=e89]
- tab "🏆 Achieve" [ref=e90]
- tab "📊 Stats" [ref=e91]
- tab "🐛 Debug" [ref=e92]
- tab "📖 Grimoire" [ref=e93]
- tabpanel "🛡️ Gear" [ref=e94]:
- generic [ref=e95]:
- generic [ref=e96]:
- generic [ref=e97]:
- heading "Equipped Gear" [level=3] [ref=e98]
- generic [ref=e100]: 4 / 8 slots filled
- generic [ref=e101]:
- generic [ref=e102]:
- heading "Weapon & Shield" [level=4] [ref=e103]
- generic [ref=e104]:
- 'button "Main Hand slot: Basic Staff" [ref=e106]':
- generic [ref=e107]:
- generic [ref=e108]:
- img [ref=e109]
- generic [ref=e114]: Main Hand
- button "Unequip Basic Staff" [ref=e115]:
- img [ref=e116]
- generic [ref=e119]:
- generic [ref=e120]:
- text: Basic Staff
- generic [ref=e121]: 2-Handed
- generic [ref=e122]: "Enchantments: 1/50"
- generic [ref=e124]: Mana Bolt
- button "Off Hand slot (blocked by 2-handed weapon) (empty)" [ref=e125]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Off Hand
- generic [ref=e131]:
- img
- text: Occupied — 2H Weapon
- generic [ref=e132]:
- img [ref=e133]
- text: Blocked by 2-handed weapon
- generic [ref=e135]:
- heading "Armor" [level=4] [ref=e136]
- generic [ref=e137]:
- button "Head slot (empty)" [ref=e139]:
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e147]: Head
- generic [ref=e148]: Head
- 'button "Body slot: Civilian Shirt" [ref=e150]':
- generic [ref=e151]:
- generic [ref=e152]:
- img [ref=e153]
- generic [ref=e155]: Body
- button "Unequip Civilian Shirt" [ref=e156]:
- img [ref=e157]
- generic [ref=e160]:
- generic [ref=e161]: Civilian Shirt
- generic [ref=e162]: "Enchantments: 0/30"
- 'button "Hands slot: Civilian Gloves" [ref=e164]':
- generic [ref=e165]:
- generic [ref=e166]:
- img [ref=e167]
- generic [ref=e172]: Hands
- button "Unequip Civilian Gloves" [ref=e173]:
- img [ref=e174]
- generic [ref=e177]:
- generic [ref=e178]: Civilian Gloves
- generic [ref=e179]: "Enchantments: 0/20"
- 'button "Feet slot: Civilian Shoes" [ref=e181]':
- generic [ref=e182]:
- generic [ref=e183]:
- img [ref=e184]
- generic [ref=e187]: Feet
- button "Unequip Civilian Shoes" [ref=e188]:
- img [ref=e189]
- generic [ref=e192]:
- generic [ref=e193]: Civilian Shoes
- generic [ref=e194]: "Enchantments: 0/15"
- generic [ref=e195]:
- heading "Accessories" [level=4] [ref=e196]
- generic [ref=e197]:
- button "Accessory 1 slot (empty)" [ref=e199]:
- generic [ref=e201]:
- img [ref=e202]
- generic [ref=e205]: Accessory 1
- generic [ref=e206]: Accessory 1
- button "Accessory 2 slot (empty)" [ref=e208]:
- generic [ref=e210]:
- img [ref=e211]
- generic [ref=e214]: Accessory 2
- generic [ref=e215]: Accessory 2
- generic [ref=e216]:
- heading "Equipment Inventory (0 items)" [level=3] [ref=e218]
- status [ref=e219]: No unequipped items. Craft new gear in the Crafting tab.
- generic [ref=e220]:
- heading "Equipment Stats Summary" [level=3] [ref=e222]
- generic [ref=e223]:
- generic [ref=e224]:
- generic [ref=e225]: "4"
- generic [ref=e226]: Total Items
- generic [ref=e227]:
- generic [ref=e228]: "4"
- generic [ref=e229]: Equipped
- generic [ref=e230]:
- generic [ref=e231]: "0"
- generic [ref=e232]: In Inventory
- generic [ref=e233]:
- generic [ref=e234]: "1"
- generic [ref=e235]: Total Enchantments
- generic [ref=e236]:
- heading "✨ Enchantment Power" [level=3] [ref=e238]
- generic [ref=e239]:
- generic [ref=e240]:
- generic [ref=e241]: "Enchantment Power:"
- generic [ref=e242]: 1.00×
- paragraph [ref=e243]: Increases the power of all enchantments by 0%. Multiplier applied to all enchantment effects.
- generic [ref=e244]:
- generic [ref=e245]: "Active Effects from Equipment:"
- generic [ref=e247]: No active effects
- region "Notifications (F8)":
- list
- region "Notifications (F8)":
- list
- button "Open Next.js Dev Tools" [ref=e253] [cursor=pointer]:
- img [ref=e254]
- alert [ref=e257]
```
# Test source
```ts
28 |
29 | // Verify equipment UI elements
30 | const equippedGearHeading = page.locator('text="Equipped Gear"');
31 | await expect(equippedGearHeading).toBeVisible({ timeout: 5000 });
32 | });
33 |
34 | test('shows equipment slots with labels', async ({ page }) => {
35 | await page.goto('/');
36 | await page.evaluate(() => {
37 | Object.keys(localStorage)
38 | .filter((k) => k.startsWith('mana-loop-'))
39 | .forEach((k) => localStorage.removeItem(k));
40 | });
41 | await page.reload();
42 | await page.waitForLoadState('networkidle');
43 |
44 | await page.getByRole('tab', { name: /🛡️ Gear/ }).click();
45 |
46 | // Check for expected slot labels - use role=heading or more specific selectors
47 | // Main Hand slot
48 | const mainHandSection = page.locator('text=Main Hand');
49 | await expect(mainHandSection.first()).toBeVisible();
50 |
51 | // Off Hand
52 | const offHandSection = page.locator('text=Off Hand');
53 | await expect(offHandSection.first()).toBeVisible();
54 |
55 | // Head
56 | const headSection = page.locator('text=Head');
57 | await expect(headSection.first()).toBeVisible();
58 |
59 | // Body
60 | const bodySection = page.locator('text=Body');
61 | await expect(bodySection.first()).toBeVisible();
62 |
63 | // Hands
64 | const handsSection = page.locator('text=Hands');
65 | await expect(handsSection.first()).toBeVisible();
66 |
67 | // Feet
68 | const feetSection = page.locator('text=Feet');
69 | await expect(feetSection.first()).toBeVisible();
70 |
71 | // Accessory 1 and 2
72 | const acc1Section = page.locator('text=Accessory 1');
73 | await expect(acc1Section.first()).toBeVisible();
74 | const acc2Section = page.locator('text=Accessory 2');
75 | await expect(acc2Section.first()).toBeVisible();
76 | });
77 |
78 | test('shows starting equipment already equipped', async ({ page }) => {
79 | await page.goto('/');
80 | await page.evaluate(() => {
81 | Object.keys(localStorage)
82 | .filter((k) => k.startsWith('mana-loop-'))
83 | .forEach((k) => localStorage.removeItem(k));
84 | });
85 | await page.reload();
86 | await page.waitForLoadState('networkidle');
87 |
88 | await page.getByRole('tab', { name: /🛡️ Gear/ }).click();
89 |
90 | // The player starts with a Basic Staff in main hand (as an equipped item)
91 | const mainHandSlot = page.locator('text=Main Hand >> .. >> text=Basic Staff');
92 | await expect(mainHandSlot).toBeVisible({ timeout: 5000 });
93 | });
94 |
95 | test('2-handed weapon blocks offhand slot', async ({ page }) => {
96 | await page.goto('/');
97 | await page.evaluate(() => {
98 | Object.keys(localStorage)
99 | .filter((k) => k.startsWith('mana-loop-'))
100 | .forEach((k) => localStorage.removeItem(k));
101 | });
102 | await page.reload();
103 | await page.waitForLoadState('networkidle');
104 |
105 | await page.getByRole('tab', { name: /🛡️ Gear/ }).click();
106 |
107 | // The starting basic staff is 2-handed
108 | // The offhand slot should show as blocked with "Occupied — 2H Weapon"
109 | const offHandBlocked = page.locator('text=Occupied').first();
110 | await expect(offHandBlocked).toBeVisible({ timeout: 5000 });
111 | });
112 |
113 | test('can unequip an item from a slot', async ({ page }) => {
114 | await page.goto('/');
115 | await page.evaluate(() => {
116 | Object.keys(localStorage)
117 | .filter((k) => k.startsWith('mana-loop-'))
118 | .forEach((k) => localStorage.removeItem(k));
119 | });
120 | await page.reload();
121 | await page.waitForLoadState('networkidle');
122 |
123 | await page.getByRole('tab', { name: /🛡️ Gear/ }).click();
124 |
125 | // Find an equiped slot with an unequip button (the X button)
126 | // The hands slot has civilian gloves equipped
127 | const handsSlot = page.locator('text=Hands >> .. >> button').first();
> 128 | await expect(handsSlot).toBeVisible({ timeout: 5000 });
| ^ Error: expect(locator).toBeVisible() failed
129 | // Note: exact behavior of unequip depends on implementation state
130 | });
131 | });
```
@@ -0,0 +1,285 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: enchanting.spec.ts >> Enchanting Flow >> can navigate to Crafting tab
- Location: e2e/enchanting.spec.ts:28:7
# Error details
```
Error: expect(locator).toBeVisible() failed
Locator: getByRole('button')
Expected: visible
Error: strict mode violation: getByRole('button') resolved to 6 elements:
1) <button data-slot="button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive text-primary-foreground shad…>…</button> aka getByRole('button', { name: 'Gather +1 Mana' })
2) <button class="flex items-center justify-between w-full text-xs text-gray-400 hover:text-gray-300 mb-2">…</button> aka getByRole('button', { name: 'Elemental Mana (1)' })
3) <button data-slot="button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive text-primary-foreground shadow-xs hover…>…</button> aka getByRole('button', { name: 'Climb the Spire' })
4) <button data-slot="action-button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[var(--radius)] text-sm font-medium transition-all duration-100 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-focus)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-base)] bg-[var(--interactive-primary)] text-white …>…</button> aka getByRole('button', { name: 'Fabricate' })
5) <button data-slot="action-button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[var(--radius)] text-sm font-medium transition-all duration-100 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-focus)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-base)] bg-[var(--interactive-secondary)] text-[var…>…</button> aka getByRole('button', { name: 'Enchant' })
6) <button id="next-logo" aria-haspopup="menu" data-next-mark="true" aria-expanded="false" aria-label="Open Next.js Dev Tools" data-nextjs-dev-tools-button="true" aria-controls="nextjs-dev-tools-menu">…</button> aka getByRole('button', { name: 'Open Next.js Dev Tools' })
Call log:
- Expect "toBeVisible" with timeout 5000ms
- waiting for getByRole('button')
```
# Page snapshot
```yaml
- generic [ref=e1]:
- generic [ref=e2]:
- banner [ref=e3]:
- generic [ref=e4]:
- heading "MANA LOOP" [level=1] [ref=e5]
- generic [ref=e7]:
- generic [ref=e8]:
- generic [ref=e9]: Day 1
- generic [ref=e10]: 00:55
- generic [ref=e11]:
- generic [ref=e12]: "0"
- generic [ref=e13]: Insight
- main [ref=e14]:
- generic [ref=e15]:
- generic [ref=e17]:
- generic [ref=e18]:
- generic [ref=e19]:
- generic [ref=e20]: "11"
- generic [ref=e21]: / 100
- generic [ref=e22]:
- text: +2.4 mana/hr
- generic [ref=e23]: (1.2x med)
- progressbar [ref=e24]
- button "Gather +1 Mana" [ref=e26]:
- img
- text: Gather +1 Mana
- generic [ref=e27]:
- button "Elemental Mana (1)" [ref=e28]:
- generic [ref=e29]: Elemental Mana (1)
- img [ref=e30]
- generic [ref=e33]:
- generic [ref=e34]:
- generic [ref=e35]: 🔗
- generic [ref=e36]: Transference
- generic [ref=e39]: 0/10
- button "Climb the Spire" [ref=e40]:
- img
- text: Climb the Spire
- generic [ref=e42]:
- generic [ref=e43]:
- img [ref=e44]
- generic [ref=e46]: Current Activity
- generic [ref=e47]: Meditating
- generic [ref=e48]:
- generic [ref=e49]: "1"
- generic [ref=e50]: "2"
- generic [ref=e51]: "3"
- generic [ref=e52]: "4"
- generic [ref=e53]: "5"
- generic [ref=e54]: "6"
- generic [ref=e55]: "7"
- generic [ref=e56]: "8"
- generic [ref=e57]: "9"
- generic [ref=e58]: "10"
- generic [ref=e59]: "11"
- generic [ref=e60]: "12"
- generic [ref=e61]: "13"
- generic [ref=e62]: "14"
- generic [ref=e63]: "15"
- generic [ref=e64]: "16"
- generic [ref=e65]: "17"
- generic [ref=e66]: "18"
- generic [ref=e67]: "19"
- generic [ref=e68]: "20"
- generic [ref=e69]: "21"
- generic [ref=e70]: "22"
- generic [ref=e71]: "23"
- generic [ref=e72]: "24"
- generic [ref=e73]: "25"
- generic [ref=e74]: "26"
- generic [ref=e75]: "27"
- generic [ref=e76]: "28"
- generic [ref=e77]: "29"
- generic [ref=e78]: "30"
- generic [ref=e80]:
- tablist [ref=e81]:
- tab "⚔️ Spire" [ref=e82]
- tab "✨ Attune" [ref=e83]
- tab "🗿 Golems" [ref=e84]
- tab "📚 Skills" [ref=e85]
- tab "🔮 Spells" [ref=e86]
- tab "🛡️ Gear" [ref=e87]
- tab "🔧 Craft" [active] [selected] [ref=e88]
- tab "💎 Loot" [ref=e89]
- tab "🏆 Achieve" [ref=e90]
- tab "📊 Stats" [ref=e91]
- tab "🐛 Debug" [ref=e92]
- tab "📖 Grimoire" [ref=e93]
- tabpanel "🔧 Craft" [ref=e94]:
- generic [ref=e95]:
- generic [ref=e97]:
- button "Fabricate" [ref=e98]:
- img
- text: Fabricate
- button "Enchant" [ref=e99]:
- img
- text: Enchant
- generic [ref=e100]:
- generic [ref=e101]:
- generic [ref=e103]:
- img [ref=e104]
- text: Available Blueprints
- generic [ref=e113]:
- img [ref=e114]
- paragraph [ref=e118]: No blueprints discovered yet.
- paragraph [ref=e119]: Defeat guardians to find blueprints!
- generic [ref=e120]:
- generic [ref=e122]:
- img [ref=e123]
- text: Materials (0)
- generic [ref=e131]:
- img [ref=e132]
- paragraph [ref=e134]: No materials collected yet.
- paragraph [ref=e135]: Defeat floors to gather materials!
- region "Notifications (F8)":
- list
- region "Notifications (F8)":
- list
- button "Open Next.js Dev Tools" [ref=e141] [cursor=pointer]:
- img [ref=e142]
- alert [ref=e145]
```
# Test source
```ts
1 | import { test, expect } from '@playwright/test';
2 |
3 | /**
4 | * E2E tests for the 3-step enchantment flow:
5 | * Design → Prepare → Apply
6 | *
7 | * These tests validate the core crafting loop works end-to-end.
8 | */
9 |
10 | test.describe('Enchanting Flow', () => {
11 | /**
12 | * Before each test, ensure we start with a clean state.
13 | * The game persists state in localStorage, so we clear it.
14 | */
15 | test.beforeEach(async ({ page }) => {
16 | await page.goto('/');
17 | // Clear game state to ensure a fresh start
18 | await page.evaluate(() => {
19 | Object.keys(localStorage)
20 | .filter((k) => k.startsWith('mana-loop-'))
21 | .forEach((k) => localStorage.removeItem(k));
22 | });
23 | await page.reload();
24 | // Wait for the game to initialize
25 | await page.waitForLoadState('networkidle');
26 | });
27 |
28 | test('can navigate to Crafting tab', async ({ page }) => {
29 | // The tab bar contains a "Craft" tab
30 | const craftTab = page.getByRole('tab', { name: /🔧 Craft/ });
31 | await expect(craftTab).toBeVisible();
32 | await craftTab.click();
33 |
34 | // Verify we're on the crafting tab by checking for sub-tabs
35 | const fabricateBtn = page.getByRole('button', { hasText: 'Fabricate' });
36 | const enchantBtn = page.getByRole('button', { hasText: 'Enchant' });
> 37 | await expect(fabricateBtn).toBeVisible();
| ^ Error: expect(locator).toBeVisible() failed
38 | await expect(enchantBtn).toBeVisible();
39 | });
40 |
41 | test('can switch to Enchant sub-tab and see design UI', async ({ page }) => {
42 | await page.goto('/');
43 | await page.evaluate(() => {
44 | Object.keys(localStorage)
45 | .filter((k) => k.startsWith('mana-loop-'))
46 | .forEach((k) => localStorage.removeItem(k));
47 | });
48 | await page.reload();
49 | await page.waitForLoadState('networkidle');
50 |
51 | // Navigate to Crafting tab
52 | await page.getByRole('tab', { name: /🔧 Craft/ }).click();
53 |
54 | // Click Enchant sub-tab
55 | const enchantBtn = page.getByRole('button', { hasText: 'Enchant' });
56 | await enchantBtn.click();
57 |
58 | // Should see the design stage UI
59 | const designBtn = page.getByRole('button', { hasText: 'Design' });
60 | const prepareBtn = page.getByRole('button', { hasText: 'Prepare' });
61 | const applyBtn = page.getByRole('button', { hasText: 'Apply' });
62 | await expect(designBtn).toBeVisible();
63 | await expect(prepareBtn).toBeVisible();
64 | await expect(applyBtn).toBeVisible();
65 | });
66 |
67 | test('can select equipment type and effect in Design stage', async ({ page }) => {
68 | await page.goto('/');
69 | await page.evaluate(() => {
70 | Object.keys(localStorage)
71 | .filter((k) => k.startsWith('mana-loop-'))
72 | .forEach((k) => localStorage.removeItem(k));
73 | });
74 | await page.reload();
75 | await page.waitForLoadState('networkidle');
76 |
77 | // Navigate to Crafting > Enchant > Design
78 | await page.getByRole('tab', { name: /🔧 Craft/ }).click();
79 | await page.getByRole('button', { hasText: 'Enchant' }).click();
80 |
81 | // The design section should show effect selectors once an equipment type is chosen
82 | // Look for any element matching equipment type buttons and effect-related content
83 | const equipmentButtons = page.locator('button:has-text("Basic Staff"), button:has-text("Apprentice Wand"), button:has-text("Oak Staff"), button:has-text("Crystal Wand")');
84 | const count = await equipmentButtons.count();
85 | expect(count).toBeGreaterThan(0);
86 | });
87 |
88 | test('can navigate through all 3 enchant stages', async ({ page }) => {
89 | await page.goto('/');
90 | await page.evaluate(() => {
91 | Object.keys(localStorage)
92 | .filter((k) => k.startsWith('mana-loop-'))
93 | .forEach((k) => localStorage.removeItem(k));
94 | });
95 | await page.reload();
96 | await page.waitForLoadState('networkidle');
97 |
98 | // Navigate to Crafting > Enchant
99 | await page.getByRole('tab', { name: /🔧 Craft/ }).click();
100 | await page.getByRole('button', { hasText: 'Enchant' }).click();
101 |
102 | // Verify Design stage is active
103 | const designBtn = page.getByRole('button', { hasText: 'Design' });
104 | await expect(designBtn).toBeVisible();
105 |
106 | // Switch to Prepare stage
107 | const prepareBtn = page.getByRole('button', { hasText: 'Prepare' });
108 | await prepareBtn.click();
109 |
110 | // Should see preparation UI
111 | const prepareHeading = page.locator('text=Select Equipment to Prepare');
112 | await expect(prepareHeading).toBeVisible({ timeout: 5000 });
113 |
114 | // Switch to Apply stage
115 | const applyBtn = page.getByRole('button', { hasText: 'Apply' });
116 | await applyBtn.click();
117 |
118 | // Should see application UI
119 | const applyHeading = page.locator('text=Select Equipment & Design');
120 | await expect(applyHeading).toBeVisible({ timeout: 5000 });
121 | });
122 | });
```
@@ -0,0 +1,260 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: combat.spec.ts >> Combat System >> can enter Spire mode by clicking Climb button
- Location: e2e/combat.spec.ts:34:7
# Error details
```
Error: expect(locator).toBeVisible() failed
Locator: getByRole('button', { name: 'Enter Spire Mode' })
Expected: visible
Timeout: 5000ms
Error: element(s) not found
Call log:
- Expect "toBeVisible" with timeout 5000ms
- waiting for getByRole('button', { name: 'Enter Spire Mode' })
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e2]:
- banner [ref=e3]:
- generic [ref=e4]:
- heading "MANA LOOP" [level=1] [ref=e5]
- generic [ref=e7]:
- generic [ref=e8]:
- generic [ref=e9]: Day 1
- generic [ref=e10]: 01:43
- generic [ref=e11]:
- generic [ref=e12]: "0"
- generic [ref=e13]: Insight
- main [ref=e14]:
- generic [ref=e15]:
- generic [ref=e17]:
- generic [ref=e18]:
- generic [ref=e19]:
- generic [ref=e20]: "14"
- generic [ref=e21]: / 100
- generic [ref=e22]:
- text: +2.9 mana/hr
- generic [ref=e23]: (1.4x med)
- progressbar [ref=e24]
- button "Gather +1 Mana" [ref=e26]:
- img
- text: Gather +1 Mana
- generic [ref=e27]:
- button "Elemental Mana (1)" [ref=e28]:
- generic [ref=e29]: Elemental Mana (1)
- img [ref=e30]
- generic [ref=e33]:
- generic [ref=e34]:
- generic [ref=e35]: 🔗
- generic [ref=e36]: Transference
- generic [ref=e39]: 0/10
- generic [ref=e40]:
- generic [ref=e41]: "1"
- generic [ref=e42]: "2"
- generic [ref=e43]: "3"
- generic [ref=e44]: "4"
- generic [ref=e45]: "5"
- generic [ref=e46]: "6"
- generic [ref=e47]: "7"
- generic [ref=e48]: "8"
- generic [ref=e49]: "9"
- generic [ref=e50]: "10"
- generic [ref=e51]: "11"
- generic [ref=e52]: "12"
- generic [ref=e53]: "13"
- generic [ref=e54]: "14"
- generic [ref=e55]: "15"
- generic [ref=e56]: "16"
- generic [ref=e57]: "17"
- generic [ref=e58]: "18"
- generic [ref=e59]: "19"
- generic [ref=e60]: "20"
- generic [ref=e61]: "21"
- generic [ref=e62]: "22"
- generic [ref=e63]: "23"
- generic [ref=e64]: "24"
- generic [ref=e65]: "25"
- generic [ref=e66]: "26"
- generic [ref=e67]: "27"
- generic [ref=e68]: "28"
- generic [ref=e69]: "29"
- generic [ref=e70]: "30"
- generic [ref=e72]:
- tablist [ref=e73]:
- tab "⚔️ Spire" [selected] [ref=e74]
- tab "✨ Attune" [ref=e75]
- tab "🗿 Golems" [ref=e76]
- tab "📚 Skills" [ref=e77]
- tab "🔮 Spells" [ref=e78]
- tab "🛡️ Gear" [ref=e79]
- tab "🔧 Craft" [ref=e80]
- tab "💎 Loot" [ref=e81]
- tab "🏆 Achieve" [ref=e82]
- tab "📊 Stats" [ref=e83]
- tab "🐛 Debug" [ref=e84]
- tab "📖 Grimoire" [ref=e85]
- tabpanel "⚔️ Spire" [ref=e86]:
- generic [ref=e87]:
- generic [ref=e89]:
- button "Exit Spire Mode" [ref=e90]:
- img
- text: Exit Spire Mode
- generic [ref=e91]: Climb down to floor 1 to return to the main game
- generic [ref=e92]:
- heading "Current Floor ⚔️ Combat" [level=3] [ref=e94]:
- generic [ref=e95]: Current Floor
- generic [ref=e96]: ⚔️ Combat
- generic [ref=e97]:
- generic [ref=e98]:
- generic [ref=e99]: "1"
- generic [ref=e100]: / 100
- generic [ref=e101]: 🔥 Fire
- generic [ref=e102]:
- text: "Best: Floor"
- strong [ref=e103]: "1"
- text: "• Pacts:"
- strong [ref=e104]: "0"
- generic [ref=e106]:
- generic [ref=e108]: Active Spells (1)
- generic [ref=e110]:
- generic [ref=e111]:
- generic [ref=e112]: Mana BoltBasic
- generic [ref=e113]:
- generic [ref=e114]: ⚔️ 5 dmg • 3 raw • ⚡ 15 dmg/hr
- generic [ref=e115]:
- generic [ref=e116]:
- generic [ref=e117]:
- img [ref=e118]
- generic [ref=e123]: Inferno Whelp
- generic [ref=e124]: 🔥 Fire
- generic [ref=e129]: 151 / 151 HP
- generic [ref=e130]:
- generic [ref=e132]: Floor Navigation
- generic [ref=e133]:
- generic [ref=e134]:
- button "Climb Up" [ref=e135]:
- img
- text: Climb Up
- button "Climb Down" [disabled]:
- img
- text: Climb Down
- generic [ref=e136]: Click Climb Up/Down to begin climbing
- generic [ref=e137]:
- generic [ref=e139]: Combat Stats
- generic [ref=e140]:
- generic [ref=e141]: "Total DPS: —"
- generic [ref=e142]:
- generic [ref=e143]: Active Spells
- generic [ref=e144]:
- generic [ref=e145]:
- generic [ref=e146]:
- text: Mana Bolt
- generic [ref=e147]: Basic
- generic [ref=e148]:
- generic [ref=e149]: ⚔️ 5 dmg • 3 raw • ⚡ 15 dmg/hr
- generic [ref=e151]: "Study Speed: 100%"
- generic [ref=e152]:
- generic [ref=e154]: Activity Log
- generic [ref=e160]: No activity yet...
- region "Notifications (F8)":
- list
- region "Notifications (F8)":
- list
- button "Open Next.js Dev Tools" [ref=e166] [cursor=pointer]:
- img [ref=e167]
- alert [ref=e170]
```
# Test source
```ts
1 | import { test, expect } from '@playwright/test';
2 |
3 | /**
4 | * E2E tests for combat system:
5 | * - Entering spire mode (climbing)
6 | * - Casting spells and seeing progress
7 | * - Enemy HP reduction
8 | * - Floor advancement
9 | */
10 |
11 | test.describe('Combat System', () => {
12 | test.beforeEach(async ({ page }) => {
13 | await page.goto('/');
14 | // Clear game state to ensure a fresh start
15 | await page.evaluate(() => {
16 | Object.keys(localStorage)
17 | .filter((k) => k.startsWith('mana-loop-'))
18 | .forEach((k) => localStorage.removeItem(k));
19 | });
20 | await page.reload();
21 | await page.waitForLoadState('networkidle');
22 | });
23 |
24 | test('can see the Spire tab and "Climb the Spire" button', async ({ page }) => {
25 | // The Spire tab uses an icon + text, so match by the tab role
26 | const spireTab = page.getByRole('tab', { name: /⚔️ Spire/ });
27 | await expect(spireTab).toBeVisible();
28 |
29 | // Main page should show "Climb the Spire" button
30 | const climbBtn = page.getByRole('button', { name: 'Climb the Spire' });
31 | await expect(climbBtn).toBeVisible();
32 | });
33 |
34 | test('can enter Spire mode by clicking Climb button', async ({ page }) => {
35 | // Click "Climb the Spire" button on the main page (via left panel)
36 | await page.getByRole('button', { name: 'Climb the Spire' }).click();
37 |
38 | // Should now see Spire mode UI elements
39 | // The "Enter Spire Mode" button appears when on the Spire tab
40 | const enterBtn = page.getByRole('button', { name: 'Enter Spire Mode' });
> 41 | await expect(enterBtn).toBeVisible({ timeout: 5000 });
| ^ Error: expect(locator).toBeVisible() failed
42 | });
43 |
44 | test('can navigate to Spire tab', async ({ page }) => {
45 | // Click the Spire tab specifically (using role=tab to disambiguate)
46 | await page.getByRole('tab', { name: /⚔️ Spire/ }).click();
47 |
48 | // Should see Spire-specific UI
49 | const enterSpireBtn = page.getByRole('button', { name: 'Enter Spire Mode' });
50 | await expect(enterSpireBtn).toBeVisible({ timeout: 5000 });
51 | });
52 |
53 | test('can enter spire mode from the Spire tab', async ({ page }) => {
54 | await page.getByRole('tab', { name: /⚔️ Spire/ }).click();
55 |
56 | const enterBtn = page.getByRole('button', { name: 'Enter Spire Mode' });
57 | await expect(enterBtn).toBeEnabled();
58 | await enterBtn.click();
59 |
60 | // After entering, should see exit button
61 | const exitBtn = page.getByRole('button', { name: 'Exit Spire Mode' });
62 | await expect(exitBtn).toBeVisible({ timeout: 5000 });
63 | });
64 |
65 | test('shows floor information in spire mode', async ({ page }) => {
66 | await page.getByRole('tab', { name: /⚔️ Spire/ }).click();
67 | await page.getByRole('button', { name: 'Enter Spire Mode' }).click();
68 |
69 | // Should display floor number - look for "Floor" label or the floor counter
70 | const floorDisplay = page.locator('text="Floor"').first();
71 | await expect(floorDisplay).toBeVisible({ timeout: 5000 });
72 | });
73 | });
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

@@ -0,0 +1,280 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: enchanting.spec.ts >> Enchanting Flow >> can switch to Enchant sub-tab and see design UI
- Location: e2e/enchanting.spec.ts:41:7
# Error details
```
Error: locator.click: Error: strict mode violation: getByRole('button') resolved to 6 elements:
1) <button data-slot="button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive text-primary-foreground shad…>…</button> aka getByRole('button', { name: 'Gather +1 Mana' })
2) <button class="flex items-center justify-between w-full text-xs text-gray-400 hover:text-gray-300 mb-2">…</button> aka getByRole('button', { name: 'Elemental Mana (1)' })
3) <button data-slot="button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive text-primary-foreground shadow-xs hover…>…</button> aka getByRole('button', { name: 'Climb the Spire' })
4) <button data-slot="action-button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[var(--radius)] text-sm font-medium transition-all duration-100 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-focus)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-base)] bg-[var(--interactive-primary)] text-white …>…</button> aka getByRole('button', { name: 'Fabricate' })
5) <button data-slot="action-button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[var(--radius)] text-sm font-medium transition-all duration-100 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-focus)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-base)] bg-[var(--interactive-secondary)] text-[var…>…</button> aka getByRole('button', { name: 'Enchant' })
6) <button id="next-logo" aria-haspopup="menu" data-next-mark="true" aria-expanded="false" aria-label="Open Next.js Dev Tools" data-nextjs-dev-tools-button="true" aria-controls="nextjs-dev-tools-menu">…</button> aka getByRole('button', { name: 'Open Next.js Dev Tools' })
Call log:
- waiting for getByRole('button')
```
# Page snapshot
```yaml
- generic [ref=e1]:
- generic [ref=e2]:
- banner [ref=e3]:
- generic [ref=e4]:
- heading "MANA LOOP" [level=1] [ref=e5]
- generic [ref=e7]:
- generic [ref=e8]:
- generic [ref=e9]: Day 1
- generic [ref=e10]: 01:04
- generic [ref=e11]:
- generic [ref=e12]: "0"
- generic [ref=e13]: Insight
- main [ref=e14]:
- generic [ref=e15]:
- generic [ref=e17]:
- generic [ref=e18]:
- generic [ref=e19]:
- generic [ref=e20]: "12"
- generic [ref=e21]: / 100
- generic [ref=e22]:
- text: +2.3 mana/hr
- generic [ref=e23]: (1.1x med)
- progressbar [ref=e24]
- button "Gather +1 Mana" [ref=e26]:
- img
- text: Gather +1 Mana
- generic [ref=e27]:
- button "Elemental Mana (1)" [ref=e28]:
- generic [ref=e29]: Elemental Mana (1)
- img [ref=e30]
- generic [ref=e33]:
- generic [ref=e34]:
- generic [ref=e35]: 🔗
- generic [ref=e36]: Transference
- generic [ref=e39]: 0/10
- button "Climb the Spire" [ref=e40]:
- img
- text: Climb the Spire
- generic [ref=e42]:
- generic [ref=e43]:
- img [ref=e44]
- generic [ref=e46]: Current Activity
- generic [ref=e47]: Meditating
- generic [ref=e48]:
- generic [ref=e49]: "1"
- generic [ref=e50]: "2"
- generic [ref=e51]: "3"
- generic [ref=e52]: "4"
- generic [ref=e53]: "5"
- generic [ref=e54]: "6"
- generic [ref=e55]: "7"
- generic [ref=e56]: "8"
- generic [ref=e57]: "9"
- generic [ref=e58]: "10"
- generic [ref=e59]: "11"
- generic [ref=e60]: "12"
- generic [ref=e61]: "13"
- generic [ref=e62]: "14"
- generic [ref=e63]: "15"
- generic [ref=e64]: "16"
- generic [ref=e65]: "17"
- generic [ref=e66]: "18"
- generic [ref=e67]: "19"
- generic [ref=e68]: "20"
- generic [ref=e69]: "21"
- generic [ref=e70]: "22"
- generic [ref=e71]: "23"
- generic [ref=e72]: "24"
- generic [ref=e73]: "25"
- generic [ref=e74]: "26"
- generic [ref=e75]: "27"
- generic [ref=e76]: "28"
- generic [ref=e77]: "29"
- generic [ref=e78]: "30"
- generic [ref=e80]:
- tablist [ref=e81]:
- tab "⚔️ Spire" [ref=e82]
- tab "✨ Attune" [ref=e83]
- tab "🗿 Golems" [ref=e84]
- tab "📚 Skills" [ref=e85]
- tab "🔮 Spells" [ref=e86]
- tab "🛡️ Gear" [ref=e87]
- tab "🔧 Craft" [active] [selected] [ref=e88]
- tab "💎 Loot" [ref=e89]
- tab "🏆 Achieve" [ref=e90]
- tab "📊 Stats" [ref=e91]
- tab "🐛 Debug" [ref=e92]
- tab "📖 Grimoire" [ref=e93]
- tabpanel "🔧 Craft" [ref=e94]:
- generic [ref=e95]:
- generic [ref=e97]:
- button "Fabricate" [ref=e98]:
- img
- text: Fabricate
- button "Enchant" [ref=e99]:
- img
- text: Enchant
- generic [ref=e100]:
- generic [ref=e101]:
- generic [ref=e103]:
- img [ref=e104]
- text: Available Blueprints
- generic [ref=e113]:
- img [ref=e114]
- paragraph [ref=e118]: No blueprints discovered yet.
- paragraph [ref=e119]: Defeat guardians to find blueprints!
- generic [ref=e120]:
- generic [ref=e122]:
- img [ref=e123]
- text: Materials (0)
- generic [ref=e131]:
- img [ref=e132]
- paragraph [ref=e134]: No materials collected yet.
- paragraph [ref=e135]: Defeat floors to gather materials!
- region "Notifications (F8)":
- list
- region "Notifications (F8)":
- list
- button "Open Next.js Dev Tools" [ref=e141] [cursor=pointer]:
- img [ref=e142]
- alert [ref=e145]
```
# Test source
```ts
1 | import { test, expect } from '@playwright/test';
2 |
3 | /**
4 | * E2E tests for the 3-step enchantment flow:
5 | * Design → Prepare → Apply
6 | *
7 | * These tests validate the core crafting loop works end-to-end.
8 | */
9 |
10 | test.describe('Enchanting Flow', () => {
11 | /**
12 | * Before each test, ensure we start with a clean state.
13 | * The game persists state in localStorage, so we clear it.
14 | */
15 | test.beforeEach(async ({ page }) => {
16 | await page.goto('/');
17 | // Clear game state to ensure a fresh start
18 | await page.evaluate(() => {
19 | Object.keys(localStorage)
20 | .filter((k) => k.startsWith('mana-loop-'))
21 | .forEach((k) => localStorage.removeItem(k));
22 | });
23 | await page.reload();
24 | // Wait for the game to initialize
25 | await page.waitForLoadState('networkidle');
26 | });
27 |
28 | test('can navigate to Crafting tab', async ({ page }) => {
29 | // The tab bar contains a "Craft" tab
30 | const craftTab = page.getByRole('tab', { name: /🔧 Craft/ });
31 | await expect(craftTab).toBeVisible();
32 | await craftTab.click();
33 |
34 | // Verify we're on the crafting tab by checking for sub-tabs
35 | const fabricateBtn = page.getByRole('button', { hasText: 'Fabricate' });
36 | const enchantBtn = page.getByRole('button', { hasText: 'Enchant' });
37 | await expect(fabricateBtn).toBeVisible();
38 | await expect(enchantBtn).toBeVisible();
39 | });
40 |
41 | test('can switch to Enchant sub-tab and see design UI', async ({ page }) => {
42 | await page.goto('/');
43 | await page.evaluate(() => {
44 | Object.keys(localStorage)
45 | .filter((k) => k.startsWith('mana-loop-'))
46 | .forEach((k) => localStorage.removeItem(k));
47 | });
48 | await page.reload();
49 | await page.waitForLoadState('networkidle');
50 |
51 | // Navigate to Crafting tab
52 | await page.getByRole('tab', { name: /🔧 Craft/ }).click();
53 |
54 | // Click Enchant sub-tab
55 | const enchantBtn = page.getByRole('button', { hasText: 'Enchant' });
> 56 | await enchantBtn.click();
| ^ Error: locator.click: Error: strict mode violation: getByRole('button') resolved to 6 elements:
57 |
58 | // Should see the design stage UI
59 | const designBtn = page.getByRole('button', { hasText: 'Design' });
60 | const prepareBtn = page.getByRole('button', { hasText: 'Prepare' });
61 | const applyBtn = page.getByRole('button', { hasText: 'Apply' });
62 | await expect(designBtn).toBeVisible();
63 | await expect(prepareBtn).toBeVisible();
64 | await expect(applyBtn).toBeVisible();
65 | });
66 |
67 | test('can select equipment type and effect in Design stage', async ({ page }) => {
68 | await page.goto('/');
69 | await page.evaluate(() => {
70 | Object.keys(localStorage)
71 | .filter((k) => k.startsWith('mana-loop-'))
72 | .forEach((k) => localStorage.removeItem(k));
73 | });
74 | await page.reload();
75 | await page.waitForLoadState('networkidle');
76 |
77 | // Navigate to Crafting > Enchant > Design
78 | await page.getByRole('tab', { name: /🔧 Craft/ }).click();
79 | await page.getByRole('button', { hasText: 'Enchant' }).click();
80 |
81 | // The design section should show effect selectors once an equipment type is chosen
82 | // Look for any element matching equipment type buttons and effect-related content
83 | const equipmentButtons = page.locator('button:has-text("Basic Staff"), button:has-text("Apprentice Wand"), button:has-text("Oak Staff"), button:has-text("Crystal Wand")');
84 | const count = await equipmentButtons.count();
85 | expect(count).toBeGreaterThan(0);
86 | });
87 |
88 | test('can navigate through all 3 enchant stages', async ({ page }) => {
89 | await page.goto('/');
90 | await page.evaluate(() => {
91 | Object.keys(localStorage)
92 | .filter((k) => k.startsWith('mana-loop-'))
93 | .forEach((k) => localStorage.removeItem(k));
94 | });
95 | await page.reload();
96 | await page.waitForLoadState('networkidle');
97 |
98 | // Navigate to Crafting > Enchant
99 | await page.getByRole('tab', { name: /🔧 Craft/ }).click();
100 | await page.getByRole('button', { hasText: 'Enchant' }).click();
101 |
102 | // Verify Design stage is active
103 | const designBtn = page.getByRole('button', { hasText: 'Design' });
104 | await expect(designBtn).toBeVisible();
105 |
106 | // Switch to Prepare stage
107 | const prepareBtn = page.getByRole('button', { hasText: 'Prepare' });
108 | await prepareBtn.click();
109 |
110 | // Should see preparation UI
111 | const prepareHeading = page.locator('text=Select Equipment to Prepare');
112 | await expect(prepareHeading).toBeVisible({ timeout: 5000 });
113 |
114 | // Switch to Apply stage
115 | const applyBtn = page.getByRole('button', { hasText: 'Apply' });
116 | await applyBtn.click();
117 |
118 | // Should see application UI
119 | const applyHeading = page.locator('text=Select Equipment & Design');
120 | await expect(applyHeading).toBeVisible({ timeout: 5000 });
121 | });
122 | });
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

@@ -0,0 +1,375 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: equipment.spec.ts >> Equipment Management >> shows starting equipment already equipped
- Location: e2e/equipment.spec.ts:78:7
# Error details
```
Error: expect(locator).toBeVisible() failed
Locator: locator('text=Main Hand').locator('..').locator('text=Basic Staff')
Expected: visible
Timeout: 5000ms
Error: element(s) not found
Call log:
- Expect "toBeVisible" with timeout 5000ms
- waiting for locator('text=Main Hand').locator('..').locator('text=Basic Staff')
```
# Page snapshot
```yaml
- generic [ref=e1]:
- generic [ref=e2]:
- banner [ref=e3]:
- generic [ref=e4]:
- heading "MANA LOOP" [level=1] [ref=e5]
- generic [ref=e7]:
- generic [ref=e8]:
- generic [ref=e9]: Day 1
- generic [ref=e10]: 01:52
- generic [ref=e11]:
- generic [ref=e12]: "0"
- generic [ref=e13]: Insight
- main [ref=e14]:
- generic [ref=e15]:
- generic [ref=e17]:
- generic [ref=e18]:
- generic [ref=e19]:
- generic [ref=e20]: "14"
- generic [ref=e21]: / 100
- generic [ref=e22]:
- text: +2.7 mana/hr
- generic [ref=e23]: (1.4x med)
- progressbar [ref=e24]
- button "Gather +1 Mana" [ref=e26]:
- img
- text: Gather +1 Mana
- generic [ref=e27]:
- button "Elemental Mana (1)" [ref=e28]:
- generic [ref=e29]: Elemental Mana (1)
- img [ref=e30]
- generic [ref=e33]:
- generic [ref=e34]:
- generic [ref=e35]: 🔗
- generic [ref=e36]: Transference
- generic [ref=e39]: 0/10
- button "Climb the Spire" [ref=e40]:
- img
- text: Climb the Spire
- generic [ref=e42]:
- generic [ref=e43]:
- img [ref=e44]
- generic [ref=e46]: Current Activity
- generic [ref=e47]: Meditating
- generic [ref=e48]:
- generic [ref=e49]: "1"
- generic [ref=e50]: "2"
- generic [ref=e51]: "3"
- generic [ref=e52]: "4"
- generic [ref=e53]: "5"
- generic [ref=e54]: "6"
- generic [ref=e55]: "7"
- generic [ref=e56]: "8"
- generic [ref=e57]: "9"
- generic [ref=e58]: "10"
- generic [ref=e59]: "11"
- generic [ref=e60]: "12"
- generic [ref=e61]: "13"
- generic [ref=e62]: "14"
- generic [ref=e63]: "15"
- generic [ref=e64]: "16"
- generic [ref=e65]: "17"
- generic [ref=e66]: "18"
- generic [ref=e67]: "19"
- generic [ref=e68]: "20"
- generic [ref=e69]: "21"
- generic [ref=e70]: "22"
- generic [ref=e71]: "23"
- generic [ref=e72]: "24"
- generic [ref=e73]: "25"
- generic [ref=e74]: "26"
- generic [ref=e75]: "27"
- generic [ref=e76]: "28"
- generic [ref=e77]: "29"
- generic [ref=e78]: "30"
- generic [ref=e80]:
- tablist [ref=e81]:
- tab "⚔️ Spire" [ref=e82]
- tab "✨ Attune" [ref=e83]
- tab "🗿 Golems" [ref=e84]
- tab "📚 Skills" [ref=e85]
- tab "🔮 Spells" [ref=e86]
- tab "🛡️ Gear" [active] [selected] [ref=e87]
- tab "🔧 Craft" [ref=e88]
- tab "💎 Loot" [ref=e89]
- tab "🏆 Achieve" [ref=e90]
- tab "📊 Stats" [ref=e91]
- tab "🐛 Debug" [ref=e92]
- tab "📖 Grimoire" [ref=e93]
- tabpanel "🛡️ Gear" [ref=e94]:
- generic [ref=e95]:
- generic [ref=e96]:
- generic [ref=e97]:
- heading "Equipped Gear" [level=3] [ref=e98]
- generic [ref=e100]: 4 / 8 slots filled
- generic [ref=e101]:
- generic [ref=e102]:
- heading "Weapon & Shield" [level=4] [ref=e103]
- generic [ref=e104]:
- 'button "Main Hand slot: Basic Staff" [ref=e106]':
- generic [ref=e107]:
- generic [ref=e108]:
- img [ref=e109]
- generic [ref=e114]: Main Hand
- button "Unequip Basic Staff" [ref=e115]:
- img [ref=e116]
- generic [ref=e119]:
- generic [ref=e120]:
- text: Basic Staff
- generic [ref=e121]: 2-Handed
- generic [ref=e122]: "Enchantments: 1/50"
- generic [ref=e124]: Mana Bolt
- button "Off Hand slot (blocked by 2-handed weapon) (empty)" [ref=e125]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Off Hand
- generic [ref=e131]:
- img
- text: Occupied — 2H Weapon
- generic [ref=e132]:
- img [ref=e133]
- text: Blocked by 2-handed weapon
- generic [ref=e135]:
- heading "Armor" [level=4] [ref=e136]
- generic [ref=e137]:
- button "Head slot (empty)" [ref=e139]:
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e147]: Head
- generic [ref=e148]: Head
- 'button "Body slot: Civilian Shirt" [ref=e150]':
- generic [ref=e151]:
- generic [ref=e152]:
- img [ref=e153]
- generic [ref=e155]: Body
- button "Unequip Civilian Shirt" [ref=e156]:
- img [ref=e157]
- generic [ref=e160]:
- generic [ref=e161]: Civilian Shirt
- generic [ref=e162]: "Enchantments: 0/30"
- 'button "Hands slot: Civilian Gloves" [ref=e164]':
- generic [ref=e165]:
- generic [ref=e166]:
- img [ref=e167]
- generic [ref=e172]: Hands
- button "Unequip Civilian Gloves" [ref=e173]:
- img [ref=e174]
- generic [ref=e177]:
- generic [ref=e178]: Civilian Gloves
- generic [ref=e179]: "Enchantments: 0/20"
- 'button "Feet slot: Civilian Shoes" [ref=e181]':
- generic [ref=e182]:
- generic [ref=e183]:
- img [ref=e184]
- generic [ref=e187]: Feet
- button "Unequip Civilian Shoes" [ref=e188]:
- img [ref=e189]
- generic [ref=e192]:
- generic [ref=e193]: Civilian Shoes
- generic [ref=e194]: "Enchantments: 0/15"
- generic [ref=e195]:
- heading "Accessories" [level=4] [ref=e196]
- generic [ref=e197]:
- button "Accessory 1 slot (empty)" [ref=e199]:
- generic [ref=e201]:
- img [ref=e202]
- generic [ref=e205]: Accessory 1
- generic [ref=e206]: Accessory 1
- button "Accessory 2 slot (empty)" [ref=e208]:
- generic [ref=e210]:
- img [ref=e211]
- generic [ref=e214]: Accessory 2
- generic [ref=e215]: Accessory 2
- generic [ref=e216]:
- heading "Equipment Inventory (0 items)" [level=3] [ref=e218]
- status [ref=e219]: No unequipped items. Craft new gear in the Crafting tab.
- generic [ref=e220]:
- heading "Equipment Stats Summary" [level=3] [ref=e222]
- generic [ref=e223]:
- generic [ref=e224]:
- generic [ref=e225]: "4"
- generic [ref=e226]: Total Items
- generic [ref=e227]:
- generic [ref=e228]: "4"
- generic [ref=e229]: Equipped
- generic [ref=e230]:
- generic [ref=e231]: "0"
- generic [ref=e232]: In Inventory
- generic [ref=e233]:
- generic [ref=e234]: "1"
- generic [ref=e235]: Total Enchantments
- generic [ref=e236]:
- heading "✨ Enchantment Power" [level=3] [ref=e238]
- generic [ref=e239]:
- generic [ref=e240]:
- generic [ref=e241]: "Enchantment Power:"
- generic [ref=e242]: 1.00×
- paragraph [ref=e243]: Increases the power of all enchantments by 0%. Multiplier applied to all enchantment effects.
- generic [ref=e244]:
- generic [ref=e245]: "Active Effects from Equipment:"
- generic [ref=e247]: No active effects
- region "Notifications (F8)":
- list
- region "Notifications (F8)":
- list
- button "Open Next.js Dev Tools" [ref=e253] [cursor=pointer]:
- img [ref=e254]
- alert [ref=e257]
```
# Test source
```ts
1 | import { test, expect } from '@playwright/test';
2 |
3 | /**
4 | * E2E tests for equipment management:
5 | * - Equipping items to slots
6 | * - 2-handed weapon blocking offhand slot
7 | * - Unequipping items back to inventory
8 | */
9 |
10 | test.describe('Equipment Management', () => {
11 | test.beforeEach(async ({ page }) => {
12 | await page.goto('/');
13 | // Clear game state for a fresh start
14 | await page.evaluate(() => {
15 | Object.keys(localStorage)
16 | .filter((k) => k.startsWith('mana-loop-'))
17 | .forEach((k) => localStorage.removeItem(k));
18 | });
19 | await page.reload();
20 | await page.waitForLoadState('networkidle');
21 | });
22 |
23 | test('can navigate to Equipment tab', async ({ page }) => {
24 | // Use the tab with the shield icon to disambiguate
25 | const gearTab = page.getByRole('tab', { name: /🛡️ Gear/ });
26 | await expect(gearTab).toBeVisible();
27 | await gearTab.click();
28 |
29 | // Verify equipment UI elements
30 | const equippedGearHeading = page.locator('text="Equipped Gear"');
31 | await expect(equippedGearHeading).toBeVisible({ timeout: 5000 });
32 | });
33 |
34 | test('shows equipment slots with labels', async ({ page }) => {
35 | await page.goto('/');
36 | await page.evaluate(() => {
37 | Object.keys(localStorage)
38 | .filter((k) => k.startsWith('mana-loop-'))
39 | .forEach((k) => localStorage.removeItem(k));
40 | });
41 | await page.reload();
42 | await page.waitForLoadState('networkidle');
43 |
44 | await page.getByRole('tab', { name: /🛡️ Gear/ }).click();
45 |
46 | // Check for expected slot labels - use role=heading or more specific selectors
47 | // Main Hand slot
48 | const mainHandSection = page.locator('text=Main Hand');
49 | await expect(mainHandSection.first()).toBeVisible();
50 |
51 | // Off Hand
52 | const offHandSection = page.locator('text=Off Hand');
53 | await expect(offHandSection.first()).toBeVisible();
54 |
55 | // Head
56 | const headSection = page.locator('text=Head');
57 | await expect(headSection.first()).toBeVisible();
58 |
59 | // Body
60 | const bodySection = page.locator('text=Body');
61 | await expect(bodySection.first()).toBeVisible();
62 |
63 | // Hands
64 | const handsSection = page.locator('text=Hands');
65 | await expect(handsSection.first()).toBeVisible();
66 |
67 | // Feet
68 | const feetSection = page.locator('text=Feet');
69 | await expect(feetSection.first()).toBeVisible();
70 |
71 | // Accessory 1 and 2
72 | const acc1Section = page.locator('text=Accessory 1');
73 | await expect(acc1Section.first()).toBeVisible();
74 | const acc2Section = page.locator('text=Accessory 2');
75 | await expect(acc2Section.first()).toBeVisible();
76 | });
77 |
78 | test('shows starting equipment already equipped', async ({ page }) => {
79 | await page.goto('/');
80 | await page.evaluate(() => {
81 | Object.keys(localStorage)
82 | .filter((k) => k.startsWith('mana-loop-'))
83 | .forEach((k) => localStorage.removeItem(k));
84 | });
85 | await page.reload();
86 | await page.waitForLoadState('networkidle');
87 |
88 | await page.getByRole('tab', { name: /🛡️ Gear/ }).click();
89 |
90 | // The player starts with a Basic Staff in main hand (as an equipped item)
91 | const mainHandSlot = page.locator('text=Main Hand >> .. >> text=Basic Staff');
> 92 | await expect(mainHandSlot).toBeVisible({ timeout: 5000 });
| ^ Error: expect(locator).toBeVisible() failed
93 | });
94 |
95 | test('2-handed weapon blocks offhand slot', async ({ page }) => {
96 | await page.goto('/');
97 | await page.evaluate(() => {
98 | Object.keys(localStorage)
99 | .filter((k) => k.startsWith('mana-loop-'))
100 | .forEach((k) => localStorage.removeItem(k));
101 | });
102 | await page.reload();
103 | await page.waitForLoadState('networkidle');
104 |
105 | await page.getByRole('tab', { name: /🛡️ Gear/ }).click();
106 |
107 | // The starting basic staff is 2-handed
108 | // The offhand slot should show as blocked with "Occupied — 2H Weapon"
109 | const offHandBlocked = page.locator('text=Occupied').first();
110 | await expect(offHandBlocked).toBeVisible({ timeout: 5000 });
111 | });
112 |
113 | test('can unequip an item from a slot', async ({ page }) => {
114 | await page.goto('/');
115 | await page.evaluate(() => {
116 | Object.keys(localStorage)
117 | .filter((k) => k.startsWith('mana-loop-'))
118 | .forEach((k) => localStorage.removeItem(k));
119 | });
120 | await page.reload();
121 | await page.waitForLoadState('networkidle');
122 |
123 | await page.getByRole('tab', { name: /🛡️ Gear/ }).click();
124 |
125 | // Find an equiped slot with an unequip button (the X button)
126 | // The hands slot has civilian gloves equipped
127 | const handsSlot = page.locator('text=Hands >> .. >> button').first();
128 | await expect(handsSlot).toBeVisible({ timeout: 5000 });
129 | // Note: exact behavior of unequip depends on implementation state
130 | });
131 | });
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

@@ -0,0 +1,280 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: enchanting.spec.ts >> Enchanting Flow >> can select equipment type and effect in Design stage
- Location: e2e/enchanting.spec.ts:67:7
# Error details
```
Error: locator.click: Error: strict mode violation: getByRole('button') resolved to 6 elements:
1) <button data-slot="button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive text-primary-foreground shad…>…</button> aka getByRole('button', { name: 'Gather +1 Mana' })
2) <button class="flex items-center justify-between w-full text-xs text-gray-400 hover:text-gray-300 mb-2">…</button> aka getByRole('button', { name: 'Elemental Mana (1)' })
3) <button data-slot="button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive text-primary-foreground shadow-xs hover…>…</button> aka getByRole('button', { name: 'Climb the Spire' })
4) <button data-slot="action-button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[var(--radius)] text-sm font-medium transition-all duration-100 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-focus)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-base)] bg-[var(--interactive-primary)] text-white …>…</button> aka getByRole('button', { name: 'Fabricate' })
5) <button data-slot="action-button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[var(--radius)] text-sm font-medium transition-all duration-100 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-focus)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-base)] bg-[var(--interactive-secondary)] text-[var…>…</button> aka getByRole('button', { name: 'Enchant' })
6) <button id="next-logo" aria-haspopup="menu" data-next-mark="true" aria-expanded="false" aria-label="Open Next.js Dev Tools" data-nextjs-dev-tools-button="true" aria-controls="nextjs-dev-tools-menu">…</button> aka getByRole('button', { name: 'Open Next.js Dev Tools' })
Call log:
- waiting for getByRole('button')
```
# Page snapshot
```yaml
- generic [ref=e1]:
- generic [ref=e2]:
- banner [ref=e3]:
- generic [ref=e4]:
- heading "MANA LOOP" [level=1] [ref=e5]
- generic [ref=e7]:
- generic [ref=e8]:
- generic [ref=e9]: Day 1
- generic [ref=e10]: 01:02
- generic [ref=e11]:
- generic [ref=e12]: "0"
- generic [ref=e13]: Insight
- main [ref=e14]:
- generic [ref=e15]:
- generic [ref=e17]:
- generic [ref=e18]:
- generic [ref=e19]:
- generic [ref=e20]: "12"
- generic [ref=e21]: / 100
- generic [ref=e22]:
- text: +2.3 mana/hr
- generic [ref=e23]: (1.1x med)
- progressbar [ref=e24]
- button "Gather +1 Mana" [ref=e26]:
- img
- text: Gather +1 Mana
- generic [ref=e27]:
- button "Elemental Mana (1)" [ref=e28]:
- generic [ref=e29]: Elemental Mana (1)
- img [ref=e30]
- generic [ref=e33]:
- generic [ref=e34]:
- generic [ref=e35]: 🔗
- generic [ref=e36]: Transference
- generic [ref=e39]: 0/10
- button "Climb the Spire" [ref=e40]:
- img
- text: Climb the Spire
- generic [ref=e42]:
- generic [ref=e43]:
- img [ref=e44]
- generic [ref=e46]: Current Activity
- generic [ref=e47]: Meditating
- generic [ref=e48]:
- generic [ref=e49]: "1"
- generic [ref=e50]: "2"
- generic [ref=e51]: "3"
- generic [ref=e52]: "4"
- generic [ref=e53]: "5"
- generic [ref=e54]: "6"
- generic [ref=e55]: "7"
- generic [ref=e56]: "8"
- generic [ref=e57]: "9"
- generic [ref=e58]: "10"
- generic [ref=e59]: "11"
- generic [ref=e60]: "12"
- generic [ref=e61]: "13"
- generic [ref=e62]: "14"
- generic [ref=e63]: "15"
- generic [ref=e64]: "16"
- generic [ref=e65]: "17"
- generic [ref=e66]: "18"
- generic [ref=e67]: "19"
- generic [ref=e68]: "20"
- generic [ref=e69]: "21"
- generic [ref=e70]: "22"
- generic [ref=e71]: "23"
- generic [ref=e72]: "24"
- generic [ref=e73]: "25"
- generic [ref=e74]: "26"
- generic [ref=e75]: "27"
- generic [ref=e76]: "28"
- generic [ref=e77]: "29"
- generic [ref=e78]: "30"
- generic [ref=e80]:
- tablist [ref=e81]:
- tab "⚔️ Spire" [ref=e82]
- tab "✨ Attune" [ref=e83]
- tab "🗿 Golems" [ref=e84]
- tab "📚 Skills" [ref=e85]
- tab "🔮 Spells" [ref=e86]
- tab "🛡️ Gear" [ref=e87]
- tab "🔧 Craft" [active] [selected] [ref=e88]
- tab "💎 Loot" [ref=e89]
- tab "🏆 Achieve" [ref=e90]
- tab "📊 Stats" [ref=e91]
- tab "🐛 Debug" [ref=e92]
- tab "📖 Grimoire" [ref=e93]
- tabpanel "🔧 Craft" [ref=e94]:
- generic [ref=e95]:
- generic [ref=e97]:
- button "Fabricate" [ref=e98]:
- img
- text: Fabricate
- button "Enchant" [ref=e99]:
- img
- text: Enchant
- generic [ref=e100]:
- generic [ref=e101]:
- generic [ref=e103]:
- img [ref=e104]
- text: Available Blueprints
- generic [ref=e113]:
- img [ref=e114]
- paragraph [ref=e118]: No blueprints discovered yet.
- paragraph [ref=e119]: Defeat guardians to find blueprints!
- generic [ref=e120]:
- generic [ref=e122]:
- img [ref=e123]
- text: Materials (0)
- generic [ref=e131]:
- img [ref=e132]
- paragraph [ref=e134]: No materials collected yet.
- paragraph [ref=e135]: Defeat floors to gather materials!
- region "Notifications (F8)":
- list
- region "Notifications (F8)":
- list
- button "Open Next.js Dev Tools" [ref=e141] [cursor=pointer]:
- img [ref=e142]
- alert [ref=e145]
```
# Test source
```ts
1 | import { test, expect } from '@playwright/test';
2 |
3 | /**
4 | * E2E tests for the 3-step enchantment flow:
5 | * Design → Prepare → Apply
6 | *
7 | * These tests validate the core crafting loop works end-to-end.
8 | */
9 |
10 | test.describe('Enchanting Flow', () => {
11 | /**
12 | * Before each test, ensure we start with a clean state.
13 | * The game persists state in localStorage, so we clear it.
14 | */
15 | test.beforeEach(async ({ page }) => {
16 | await page.goto('/');
17 | // Clear game state to ensure a fresh start
18 | await page.evaluate(() => {
19 | Object.keys(localStorage)
20 | .filter((k) => k.startsWith('mana-loop-'))
21 | .forEach((k) => localStorage.removeItem(k));
22 | });
23 | await page.reload();
24 | // Wait for the game to initialize
25 | await page.waitForLoadState('networkidle');
26 | });
27 |
28 | test('can navigate to Crafting tab', async ({ page }) => {
29 | // The tab bar contains a "Craft" tab
30 | const craftTab = page.getByRole('tab', { name: /🔧 Craft/ });
31 | await expect(craftTab).toBeVisible();
32 | await craftTab.click();
33 |
34 | // Verify we're on the crafting tab by checking for sub-tabs
35 | const fabricateBtn = page.getByRole('button', { hasText: 'Fabricate' });
36 | const enchantBtn = page.getByRole('button', { hasText: 'Enchant' });
37 | await expect(fabricateBtn).toBeVisible();
38 | await expect(enchantBtn).toBeVisible();
39 | });
40 |
41 | test('can switch to Enchant sub-tab and see design UI', async ({ page }) => {
42 | await page.goto('/');
43 | await page.evaluate(() => {
44 | Object.keys(localStorage)
45 | .filter((k) => k.startsWith('mana-loop-'))
46 | .forEach((k) => localStorage.removeItem(k));
47 | });
48 | await page.reload();
49 | await page.waitForLoadState('networkidle');
50 |
51 | // Navigate to Crafting tab
52 | await page.getByRole('tab', { name: /🔧 Craft/ }).click();
53 |
54 | // Click Enchant sub-tab
55 | const enchantBtn = page.getByRole('button', { hasText: 'Enchant' });
56 | await enchantBtn.click();
57 |
58 | // Should see the design stage UI
59 | const designBtn = page.getByRole('button', { hasText: 'Design' });
60 | const prepareBtn = page.getByRole('button', { hasText: 'Prepare' });
61 | const applyBtn = page.getByRole('button', { hasText: 'Apply' });
62 | await expect(designBtn).toBeVisible();
63 | await expect(prepareBtn).toBeVisible();
64 | await expect(applyBtn).toBeVisible();
65 | });
66 |
67 | test('can select equipment type and effect in Design stage', async ({ page }) => {
68 | await page.goto('/');
69 | await page.evaluate(() => {
70 | Object.keys(localStorage)
71 | .filter((k) => k.startsWith('mana-loop-'))
72 | .forEach((k) => localStorage.removeItem(k));
73 | });
74 | await page.reload();
75 | await page.waitForLoadState('networkidle');
76 |
77 | // Navigate to Crafting > Enchant > Design
78 | await page.getByRole('tab', { name: /🔧 Craft/ }).click();
> 79 | await page.getByRole('button', { hasText: 'Enchant' }).click();
| ^ Error: locator.click: Error: strict mode violation: getByRole('button') resolved to 6 elements:
80 |
81 | // The design section should show effect selectors once an equipment type is chosen
82 | // Look for any element matching equipment type buttons and effect-related content
83 | const equipmentButtons = page.locator('button:has-text("Basic Staff"), button:has-text("Apprentice Wand"), button:has-text("Oak Staff"), button:has-text("Crystal Wand")');
84 | const count = await equipmentButtons.count();
85 | expect(count).toBeGreaterThan(0);
86 | });
87 |
88 | test('can navigate through all 3 enchant stages', async ({ page }) => {
89 | await page.goto('/');
90 | await page.evaluate(() => {
91 | Object.keys(localStorage)
92 | .filter((k) => k.startsWith('mana-loop-'))
93 | .forEach((k) => localStorage.removeItem(k));
94 | });
95 | await page.reload();
96 | await page.waitForLoadState('networkidle');
97 |
98 | // Navigate to Crafting > Enchant
99 | await page.getByRole('tab', { name: /🔧 Craft/ }).click();
100 | await page.getByRole('button', { hasText: 'Enchant' }).click();
101 |
102 | // Verify Design stage is active
103 | const designBtn = page.getByRole('button', { hasText: 'Design' });
104 | await expect(designBtn).toBeVisible();
105 |
106 | // Switch to Prepare stage
107 | const prepareBtn = page.getByRole('button', { hasText: 'Prepare' });
108 | await prepareBtn.click();
109 |
110 | // Should see preparation UI
111 | const prepareHeading = page.locator('text=Select Equipment to Prepare');
112 | await expect(prepareHeading).toBeVisible({ timeout: 5000 });
113 |
114 | // Switch to Apply stage
115 | const applyBtn = page.getByRole('button', { hasText: 'Apply' });
116 | await applyBtn.click();
117 |
118 | // Should see application UI
119 | const applyHeading = page.locator('text=Select Equipment & Design');
120 | await expect(applyHeading).toBeVisible({ timeout: 5000 });
121 | });
122 | });
```
@@ -0,0 +1,280 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: enchanting.spec.ts >> Enchanting Flow >> can navigate through all 3 enchant stages
- Location: e2e/enchanting.spec.ts:88:7
# Error details
```
Error: locator.click: Error: strict mode violation: getByRole('button') resolved to 6 elements:
1) <button data-slot="button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive text-primary-foreground shad…>…</button> aka getByRole('button', { name: 'Gather +1 Mana' })
2) <button class="flex items-center justify-between w-full text-xs text-gray-400 hover:text-gray-300 mb-2">…</button> aka getByRole('button', { name: 'Elemental Mana (1)' })
3) <button data-slot="button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive text-primary-foreground shadow-xs hover…>…</button> aka getByRole('button', { name: 'Climb the Spire' })
4) <button data-slot="action-button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[var(--radius)] text-sm font-medium transition-all duration-100 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-focus)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-base)] bg-[var(--interactive-primary)] text-white …>…</button> aka getByRole('button', { name: 'Fabricate' })
5) <button data-slot="action-button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[var(--radius)] text-sm font-medium transition-all duration-100 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-focus)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-base)] bg-[var(--interactive-secondary)] text-[var…>…</button> aka getByRole('button', { name: 'Enchant' })
6) <button id="next-logo" aria-haspopup="menu" data-next-mark="true" aria-expanded="false" aria-label="Open Next.js Dev Tools" data-nextjs-dev-tools-button="true" aria-controls="nextjs-dev-tools-menu">…</button> aka getByRole('button', { name: 'Open Next.js Dev Tools' })
Call log:
- waiting for getByRole('button')
```
# Page snapshot
```yaml
- generic [ref=e1]:
- generic [ref=e2]:
- banner [ref=e3]:
- generic [ref=e4]:
- heading "MANA LOOP" [level=1] [ref=e5]
- generic [ref=e7]:
- generic [ref=e8]:
- generic [ref=e9]: Day 1
- generic [ref=e10]: 00:55
- generic [ref=e11]:
- generic [ref=e12]: "0"
- generic [ref=e13]: Insight
- main [ref=e14]:
- generic [ref=e15]:
- generic [ref=e17]:
- generic [ref=e18]:
- generic [ref=e19]:
- generic [ref=e20]: "11"
- generic [ref=e21]: / 100
- generic [ref=e22]:
- text: +2.2 mana/hr
- generic [ref=e23]: (1.1x med)
- progressbar [ref=e24]
- button "Gather +1 Mana" [ref=e26]:
- img
- text: Gather +1 Mana
- generic [ref=e27]:
- button "Elemental Mana (1)" [ref=e28]:
- generic [ref=e29]: Elemental Mana (1)
- img [ref=e30]
- generic [ref=e33]:
- generic [ref=e34]:
- generic [ref=e35]: 🔗
- generic [ref=e36]: Transference
- generic [ref=e39]: 0/10
- button "Climb the Spire" [ref=e40]:
- img
- text: Climb the Spire
- generic [ref=e42]:
- generic [ref=e43]:
- img [ref=e44]
- generic [ref=e46]: Current Activity
- generic [ref=e47]: Meditating
- generic [ref=e48]:
- generic [ref=e49]: "1"
- generic [ref=e50]: "2"
- generic [ref=e51]: "3"
- generic [ref=e52]: "4"
- generic [ref=e53]: "5"
- generic [ref=e54]: "6"
- generic [ref=e55]: "7"
- generic [ref=e56]: "8"
- generic [ref=e57]: "9"
- generic [ref=e58]: "10"
- generic [ref=e59]: "11"
- generic [ref=e60]: "12"
- generic [ref=e61]: "13"
- generic [ref=e62]: "14"
- generic [ref=e63]: "15"
- generic [ref=e64]: "16"
- generic [ref=e65]: "17"
- generic [ref=e66]: "18"
- generic [ref=e67]: "19"
- generic [ref=e68]: "20"
- generic [ref=e69]: "21"
- generic [ref=e70]: "22"
- generic [ref=e71]: "23"
- generic [ref=e72]: "24"
- generic [ref=e73]: "25"
- generic [ref=e74]: "26"
- generic [ref=e75]: "27"
- generic [ref=e76]: "28"
- generic [ref=e77]: "29"
- generic [ref=e78]: "30"
- generic [ref=e80]:
- tablist [ref=e81]:
- tab "⚔️ Spire" [ref=e82]
- tab "✨ Attune" [ref=e83]
- tab "🗿 Golems" [ref=e84]
- tab "📚 Skills" [ref=e85]
- tab "🔮 Spells" [ref=e86]
- tab "🛡️ Gear" [ref=e87]
- tab "🔧 Craft" [active] [selected] [ref=e88]
- tab "💎 Loot" [ref=e89]
- tab "🏆 Achieve" [ref=e90]
- tab "📊 Stats" [ref=e91]
- tab "🐛 Debug" [ref=e92]
- tab "📖 Grimoire" [ref=e93]
- tabpanel "🔧 Craft" [ref=e94]:
- generic [ref=e95]:
- generic [ref=e97]:
- button "Fabricate" [ref=e98]:
- img
- text: Fabricate
- button "Enchant" [ref=e99]:
- img
- text: Enchant
- generic [ref=e100]:
- generic [ref=e101]:
- generic [ref=e103]:
- img [ref=e104]
- text: Available Blueprints
- generic [ref=e113]:
- img [ref=e114]
- paragraph [ref=e118]: No blueprints discovered yet.
- paragraph [ref=e119]: Defeat guardians to find blueprints!
- generic [ref=e120]:
- generic [ref=e122]:
- img [ref=e123]
- text: Materials (0)
- generic [ref=e131]:
- img [ref=e132]
- paragraph [ref=e134]: No materials collected yet.
- paragraph [ref=e135]: Defeat floors to gather materials!
- region "Notifications (F8)":
- list
- region "Notifications (F8)":
- list
- button "Open Next.js Dev Tools" [ref=e141] [cursor=pointer]:
- img [ref=e142]
- alert [ref=e145]
```
# Test source
```ts
1 | import { test, expect } from '@playwright/test';
2 |
3 | /**
4 | * E2E tests for the 3-step enchantment flow:
5 | * Design → Prepare → Apply
6 | *
7 | * These tests validate the core crafting loop works end-to-end.
8 | */
9 |
10 | test.describe('Enchanting Flow', () => {
11 | /**
12 | * Before each test, ensure we start with a clean state.
13 | * The game persists state in localStorage, so we clear it.
14 | */
15 | test.beforeEach(async ({ page }) => {
16 | await page.goto('/');
17 | // Clear game state to ensure a fresh start
18 | await page.evaluate(() => {
19 | Object.keys(localStorage)
20 | .filter((k) => k.startsWith('mana-loop-'))
21 | .forEach((k) => localStorage.removeItem(k));
22 | });
23 | await page.reload();
24 | // Wait for the game to initialize
25 | await page.waitForLoadState('networkidle');
26 | });
27 |
28 | test('can navigate to Crafting tab', async ({ page }) => {
29 | // The tab bar contains a "Craft" tab
30 | const craftTab = page.getByRole('tab', { name: /🔧 Craft/ });
31 | await expect(craftTab).toBeVisible();
32 | await craftTab.click();
33 |
34 | // Verify we're on the crafting tab by checking for sub-tabs
35 | const fabricateBtn = page.getByRole('button', { hasText: 'Fabricate' });
36 | const enchantBtn = page.getByRole('button', { hasText: 'Enchant' });
37 | await expect(fabricateBtn).toBeVisible();
38 | await expect(enchantBtn).toBeVisible();
39 | });
40 |
41 | test('can switch to Enchant sub-tab and see design UI', async ({ page }) => {
42 | await page.goto('/');
43 | await page.evaluate(() => {
44 | Object.keys(localStorage)
45 | .filter((k) => k.startsWith('mana-loop-'))
46 | .forEach((k) => localStorage.removeItem(k));
47 | });
48 | await page.reload();
49 | await page.waitForLoadState('networkidle');
50 |
51 | // Navigate to Crafting tab
52 | await page.getByRole('tab', { name: /🔧 Craft/ }).click();
53 |
54 | // Click Enchant sub-tab
55 | const enchantBtn = page.getByRole('button', { hasText: 'Enchant' });
56 | await enchantBtn.click();
57 |
58 | // Should see the design stage UI
59 | const designBtn = page.getByRole('button', { hasText: 'Design' });
60 | const prepareBtn = page.getByRole('button', { hasText: 'Prepare' });
61 | const applyBtn = page.getByRole('button', { hasText: 'Apply' });
62 | await expect(designBtn).toBeVisible();
63 | await expect(prepareBtn).toBeVisible();
64 | await expect(applyBtn).toBeVisible();
65 | });
66 |
67 | test('can select equipment type and effect in Design stage', async ({ page }) => {
68 | await page.goto('/');
69 | await page.evaluate(() => {
70 | Object.keys(localStorage)
71 | .filter((k) => k.startsWith('mana-loop-'))
72 | .forEach((k) => localStorage.removeItem(k));
73 | });
74 | await page.reload();
75 | await page.waitForLoadState('networkidle');
76 |
77 | // Navigate to Crafting > Enchant > Design
78 | await page.getByRole('tab', { name: /🔧 Craft/ }).click();
79 | await page.getByRole('button', { hasText: 'Enchant' }).click();
80 |
81 | // The design section should show effect selectors once an equipment type is chosen
82 | // Look for any element matching equipment type buttons and effect-related content
83 | const equipmentButtons = page.locator('button:has-text("Basic Staff"), button:has-text("Apprentice Wand"), button:has-text("Oak Staff"), button:has-text("Crystal Wand")');
84 | const count = await equipmentButtons.count();
85 | expect(count).toBeGreaterThan(0);
86 | });
87 |
88 | test('can navigate through all 3 enchant stages', async ({ page }) => {
89 | await page.goto('/');
90 | await page.evaluate(() => {
91 | Object.keys(localStorage)
92 | .filter((k) => k.startsWith('mana-loop-'))
93 | .forEach((k) => localStorage.removeItem(k));
94 | });
95 | await page.reload();
96 | await page.waitForLoadState('networkidle');
97 |
98 | // Navigate to Crafting > Enchant
99 | await page.getByRole('tab', { name: /🔧 Craft/ }).click();
> 100 | await page.getByRole('button', { hasText: 'Enchant' }).click();
| ^ Error: locator.click: Error: strict mode violation: getByRole('button') resolved to 6 elements:
101 |
102 | // Verify Design stage is active
103 | const designBtn = page.getByRole('button', { hasText: 'Design' });
104 | await expect(designBtn).toBeVisible();
105 |
106 | // Switch to Prepare stage
107 | const prepareBtn = page.getByRole('button', { hasText: 'Prepare' });
108 | await prepareBtn.click();
109 |
110 | // Should see preparation UI
111 | const prepareHeading = page.locator('text=Select Equipment to Prepare');
112 | await expect(prepareHeading).toBeVisible({ timeout: 5000 });
113 |
114 | // Switch to Apply stage
115 | const applyBtn = page.getByRole('button', { hasText: 'Apply' });
116 | await applyBtn.click();
117 |
118 | // Should see application UI
119 | const applyHeading = page.locator('text=Select Equipment & Design');
120 | await expect(applyHeading).toBeVisible({ timeout: 5000 });
121 | });
122 | });
```
File diff suppressed because one or more lines are too long
-32
View File
@@ -1,32 +0,0 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
authorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
-5
View File
@@ -1,5 +0,0 @@
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({ message: "Hello, world!" });
}
-13
View File
@@ -1,13 +0,0 @@
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const db =
globalForPrisma.prisma ??
new PrismaClient({
log: ['query'],
})
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db
@@ -5,8 +5,8 @@
*/
import { describe, it, expect } from 'vitest';
import { SKILLS_DEF } from '../constants';
import { calcInsight } from '../computed-stats';
import { SKILLS_DEF } from '@/lib/game/constants';
import { calcInsight } from '@/lib/game/computed-stats';
import type { GameState } from '../types';
function createMockState(overrides: Partial<GameState> = {}): GameState {
@@ -3,7 +3,7 @@
*/
import { describe, it, expect } from 'vitest';
import { SKILLS_DEF, SKILL_EVOLUTION_PATHS, getTierMultiplier, getNextTierSkill, generateTierSkillDef } from '../constants';
import { SKILLS_DEF, SKILL_EVOLUTION_PATHS, getTierMultiplier, getNextTierSkill, generateTierSkillDef } from '@/lib/game/constants';
import { SKILL_EVOLUTION_PATHS as EVOLUTION_PATHS, getUpgradesForSkillAtMilestone, getNextTierSkill as NextTier, getTierMultiplier as TierMultiplier, generateTierSkillDef as GenerateTier } from '../skill-evolution';
describe('Integration Tests', () => {
@@ -11,8 +11,8 @@ import {
computeElementMax,
computeRegen,
computeClickMana,
} from '../computed-stats';
import { SKILLS_DEF } from '../constants';
} from '@/lib/game/computed-stats';
import { SKILLS_DEF } from '@/lib/game/constants';
import type { GameState } from '../types';
// ─── Test Helpers ───────────────────────────────────────────────────────────
@@ -3,8 +3,8 @@
*/
import { describe, it, expect } from 'vitest';
import { PRESTIGE_DEF } from '../constants';
import { computeMaxMana, computeElementMax } from '../computed-stats';
import { PRESTIGE_DEF } from '@/lib/game/constants';
import { computeMaxMana, computeElementMax } from '@/lib/game/computed-stats';
import type { GameState } from '../types';
function createMockState(overrides: Partial<GameState> = {}): GameState {
@@ -3,7 +3,7 @@
*/
import { describe, it, expect } from 'vitest';
import { SKILLS_DEF } from '../constants';
import { SKILLS_DEF } from '@/lib/game/constants';
describe('Skill Prerequisites', () => {
it('Mana Overflow should require Mana Well 3', () => {
@@ -5,7 +5,7 @@
*/
import { describe, it, expect } from 'vitest';
import { SKILLS_DEF } from '../constants';
import { SKILLS_DEF } from '@/lib/game/constants';
describe('Enchanter Skills', () => {
describe('Enchanting (Unlock enchantment design)', () => {
@@ -10,8 +10,8 @@ import {
getStudySpeedMultiplier,
getStudyCostMultiplier,
getMeditationBonus,
} from '../computed-stats';
import { SKILLS_DEF } from '../constants';
} from '@/lib/game/computed-stats';
import { SKILLS_DEF } from '@/lib/game/constants';
describe('Study Skills', () => {
describe('Quick Learner (+10% study speed)', () => {
@@ -3,7 +3,7 @@
*/
import { describe, it, expect } from 'vitest';
import { SKILLS_DEF } from '../constants';
import { SKILLS_DEF } from '@/lib/game/constants';
describe('Study Times', () => {
it('all skills should have reasonable study times', () => {
@@ -3,8 +3,8 @@
*/
import { describe, it, expect } from 'vitest';
import { SKILLS_DEF } from './constants';
import { calcInsight } from './store';
import { SKILLS_DEF } from '@/lib/game/constants';
import { calcInsight } from '@/lib/game/store';
import type { GameState } from './types';
function createMockState(overrides: Partial<GameState> = {}): GameState {
@@ -10,8 +10,8 @@ import {
computeElementMax,
computeRegen,
computeClickMana,
} from './store';
import { SKILLS_DEF } from './constants';
} from '@/lib/game/store';
import { SKILLS_DEF } from '@/lib/game/constants';
import type { GameState } from './types';
// ─── Test Helpers ───────────────────────────────────────────────────────────
@@ -3,8 +3,8 @@
*/
import { describe, it, expect } from 'vitest';
import { SKILLS_DEF, PRESTIGE_DEF } from './constants';
import { computeMaxMana, computeElementMax } from './store';
import { SKILLS_DEF, PRESTIGE_DEF } from '@/lib/game/constants';
import { computeMaxMana, computeElementMax } from '@/lib/game/store';
import type { GameState } from './types';
function createMockState(overrides: Partial<GameState> = {}): GameState {
@@ -7,8 +7,8 @@ import {
getStudySpeedMultiplier,
getStudyCostMultiplier,
getMeditationBonus,
} from './store';
import { SKILLS_DEF } from './constants';
} from '@/lib/game/store';
import { SKILLS_DEF } from '@/lib/game/constants';
describe('Study Skills', () => {
describe('Quick Learner (+10% study speed)', () => {
@@ -3,7 +3,7 @@
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { useCombatStore } from './stores';
import { useCombatStore } from '@/lib/game/stores';
// ─── Test Fixtures ───────────────────────────────────────────────────
@@ -9,7 +9,7 @@ import {
usePrestigeStore,
useCombatStore,
computeMaxMana,
} from './stores';
} from '@/lib/game/stores';
// ─── Test Fixtures ───────────────────────────────────────────────────
@@ -9,8 +9,8 @@ import {
usePrestigeStore,
useCombatStore,
useUIStore,
} from './stores';
import { ELEMENTS } from './constants';
} from '@/lib/game/stores';
import { ELEMENTS } from '@/lib/game/constants';
// ─── Test Fixtures ───────────────────────────────────────────────────
@@ -6,7 +6,7 @@ import { describe, it, expect, beforeEach } from 'vitest';
import {
usePrestigeStore,
useManaStore,
} from './stores';
} from '@/lib/game/stores';
// ─── Test Fixtures ───────────────────────────────────────────────────
@@ -8,8 +8,8 @@ import {
useSkillStore,
usePrestigeStore,
getStudySpeedMultiplier,
} from './stores';
import { ELEMENTS } from './constants';
} from '@/lib/game/stores';
import { ELEMENTS } from '@/lib/game/constants';
// ─── Test Fixtures ───────────────────────────────────────────────────
@@ -3,7 +3,7 @@
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { useUIStore } from './stores';
import { useUIStore } from '@/lib/game/stores';
// ─── Test Fixtures ───────────────────────────────────────────────────
@@ -0,0 +1,492 @@
import { describe, it, expect } from 'vitest';
import {
computeStats,
BASE_STATS,
SKILLS_V2,
getBaseSkillId,
hasPrerequisites,
} from '../../constants/skills-v2';
import type { ComputedStats } from '../../constants/skills-v2-types';
// Helper to create a minimal prestige state
const emptyPrestige = {};
describe('computeStats()', () => {
describe('base stats with no skills', () => {
it('should return base stats when no skills are provided', () => {
const result = computeStats({}, emptyPrestige);
expect(result.maxMana).toBe(100);
expect(result.manaRegen).toBe(2);
expect(result.clickMana).toBe(1);
expect(result.baseDamage).toBe(5);
expect(result.elementCap).toBe(10);
});
it('should have all base values correct', () => {
const result = computeStats({}, emptyPrestige);
expect(result).toMatchObject({
maxMana: 100,
manaRegen: 2,
clickMana: 1,
elementCap: 10,
studySpeed: 1,
studyCostMult: 1,
meditationEfficiency: 1,
enchantCapacity: 100,
enchantSpeed: 1,
enchantPower: 1,
disenchantRecovery: 1,
baseDamage: 5,
damageMultiplier: 1,
attackSpeed: 1,
critChance: 0,
critMultiplier: 1.5,
armorPierce: 0,
insightGain: 1,
golemDamage: 1,
golemDuration: 1,
pactMultiplier: 1,
spellDamage: 1,
guardianDamage: 1,
craftSpeed: 1,
repairSpeed: 1,
elementalDamage: 1,
});
});
});
describe('Mana Well skill', () => {
it('should add 100 max mana per level', () => {
const result = computeStats({ manaWell: 5 }, emptyPrestige);
expect(result.maxMana).toBe(100 + 5 * 100);
});
it('should be 100 at level 0', () => {
const result = computeStats({ manaWell: 0 }, emptyPrestige);
expect(result.maxMana).toBe(100);
});
it('should be 1100 at max level 10', () => {
const result = computeStats({ manaWell: 10 }, emptyPrestige);
expect(result.maxMana).toBe(1100);
});
});
describe('Mana Flow skill', () => {
it('should add 1 mana regen per level', () => {
const result = computeStats({ manaFlow: 5 }, emptyPrestige);
expect(result.manaRegen).toBe(2 + 5);
});
it('should be 2 at level 0', () => {
const result = computeStats({ manaFlow: 0 }, emptyPrestige);
expect(result.manaRegen).toBe(2);
});
it('should be 12 at max level 10', () => {
const result = computeStats({ manaFlow: 10 }, emptyPrestige);
expect(result.manaRegen).toBe(12);
});
});
describe('Mana Tap skill', () => {
it('should add 1 click mana at level 1', () => {
const result = computeStats({ manaTap: 1 }, emptyPrestige);
expect(result.clickMana).toBe(2);
});
it('should be 1 at level 0', () => {
const result = computeStats({ manaTap: 0 }, emptyPrestige);
expect(result.clickMana).toBe(1);
});
});
describe('Mana Surge skill', () => {
it('should add 3 click mana per level', () => {
const result = computeStats({ manaSurge: 1 }, emptyPrestige);
expect(result.clickMana).toBe(1 + 3);
});
it('should stack with manaTap', () => {
const result = computeStats({ manaTap: 1, manaSurge: 1 }, emptyPrestige);
expect(result.clickMana).toBe(1 + 1 + 3);
});
});
describe('Mana Spring skill', () => {
it('should add 2 mana regen at level 1', () => {
const result = computeStats({ manaSpring: 1 }, emptyPrestige);
expect(result.manaRegen).toBe(2 + 2);
});
it('should stack with manaFlow', () => {
const result = computeStats({ manaFlow: 5, manaSpring: 1 }, emptyPrestige);
expect(result.manaRegen).toBe(2 + 5 + 2);
});
});
describe('Mana Overflow skill', () => {
it('should multiply click mana by compounding 1.25 per level', () => {
// multiply effects compound per level: 1 * 1.25^2 = 1.5625
const result = computeStats({ manaOverflow: 2 }, emptyPrestige);
expect(result.clickMana).toBeCloseTo(1.5625, 2);
});
it('should be 1 at level 0', () => {
const result = computeStats({ manaOverflow: 0 }, emptyPrestige);
expect(result.clickMana).toBe(1);
});
});
describe('Quick Learner skill', () => {
it('should multiply study speed by compounding 1.10 per level', () => {
// 1.1^5 = 1.61051
const result = computeStats({ quickLearner: 5 }, emptyPrestige);
expect(result.studySpeed).toBeCloseTo(1.61051, 2);
});
it('should be ~2.5937 at max level 10', () => {
const result = computeStats({ quickLearner: 10 }, emptyPrestige);
expect(result.studySpeed).toBeCloseTo(2.593742, 2);
});
});
describe('Focused Mind skill', () => {
it('should multiply study cost by compounding 0.95 per level', () => {
// 0.95^2 = 0.9025
const result = computeStats({ focusedMind: 2 }, emptyPrestige);
expect(result.studyCostMult).toBeCloseTo(0.9025, 2);
});
});
describe('Meditation Focus skill', () => {
it('should multiply meditation efficiency at level 1', () => {
// BASE (1) * (1 + 1.5) = 2.5
const result = computeStats({ meditation: 1 }, emptyPrestige);
expect(result.meditationEfficiency).toBe(2.5);
});
});
describe('Deep Trance skill', () => {
it('should multiply meditation efficiency further', () => {
// meditation: 1 * (1+1.5) = 2.5, deepTrance: 2.5 * (1+1.8) = 7.0
const result = computeStats({ meditation: 1, deepTrance: 1 }, emptyPrestige);
expect(result.meditationEfficiency).toBe(7.0);
});
});
describe('Void Meditation skill', () => {
it('should multiply meditation efficiency to max', () => {
// 1 * 2.5 * 2.8 * 3.5 = 24.5
const result = computeStats({ meditation: 1, deepTrance: 1, voidMeditation: 1 }, emptyPrestige);
expect(result.meditationEfficiency).toBe(24.5);
});
});
describe('Combat skills', () => {
it('Arcane Fury should multiply damage', () => {
const result = computeStats({ arcaneFury: 5 }, emptyPrestige);
expect(result.damageMultiplier).toBeCloseTo(1.61051, 2);
});
it('Combat Training should add base damage', () => {
const result = computeStats({ combatTraining: 3 }, emptyPrestige);
expect(result.baseDamage).toBe(5 + 3 * 5);
});
it('Precision should add crit chance', () => {
const result = computeStats({ precision: 4 }, emptyPrestige);
expect(result.critChance).toBe(0.2);
});
it('Precision should cap at 1.0', () => {
const result = computeStats({ precision: 25 }, emptyPrestige);
expect(result.critChance).toBe(1.0);
});
it('Elemental Mastery should multiply elemental damage', () => {
// 1 * 1.15^4 = ~1.749
const result = computeStats({ elementalMastery: 4 }, emptyPrestige);
expect(result.elementalDamage).toBeCloseTo(1.749, 2);
});
it('Attack Speed should multiply attack speed (compounding)', () => {
// 1 * 0.9^3 = 0.729
const result = computeStats({ attackSpeed: 3 }, emptyPrestige);
expect(result.attackSpeed).toBeCloseTo(0.729, 2);
});
it('Armor Piercing should add armor pierce', () => {
const result = computeStats({ armorPiercing: 4 }, emptyPrestige);
expect(result.armorPierce).toBe(0.2);
});
});
describe('Enchanting skills', () => {
it('Enchanting should affect enchantCapacity and enchantSpeed', () => {
const result = computeStats({ enchanting: 5 }, emptyPrestige);
expect(result.enchantCapacity).toBe(100 + 5 * 10); // add 10 per level
expect(result.enchantSpeed).toBeCloseTo(0.9, 2); // multiply by 0.98^5
});
it('Essence Refining should multiply enchantPower', () => {
const result = computeStats({ enchanting: 4, essenceRefining: 1 }, emptyPrestige);
expect(result.enchantPower).toBeCloseTo(1.1, 2);
});
});
describe('Golemancy skills', () => {
it('Golem Mastery should multiply golemDamage', () => {
const result = computeStats({ golemMastery: 5 }, emptyPrestige);
expect(result.golemDamage).toBeCloseTo(1.61051, 2);
});
it('Golem Longevity should add golemDuration', () => {
const result = computeStats({ golemLongevity: 3 }, emptyPrestige);
expect(result.golemDuration).toBe(1 + 3);
});
it('Golem Efficiency should multiply attackSpeed', () => {
const result = computeStats({ golemEfficiency: 2 }, emptyPrestige);
expect(result.attackSpeed).toBeCloseTo(0.9025, 2); // 0.95^2
});
});
describe('Invocation / Pact skills', () => {
it('Invocation should multiply spellDamage', () => {
const result = computeStats({ invocation: 5 }, emptyPrestige);
expect(result.spellDamage).toBeCloseTo(1.27628, 2); // 1.05^5
});
it('Pact Mastery should multiply pactMultiplier', () => {
const result = computeStats({ pactMastery: 5 }, emptyPrestige);
expect(result.pactMultiplier).toBeCloseTo(1.61051, 2); // 1.1^5
});
it('Guardian Lore should multiply guardianDamage', () => {
const result = computeStats({ guardianLore: 3 }, emptyPrestige);
expect(result.guardianDamage).toBeCloseTo(1.728, 2); // 1.2^3
});
});
describe('Element capacity skills', () => {
it('Fire Mana Cap should increase fireCap', () => {
const result = computeStats({ fireManaCap: 5 }, emptyPrestige);
expect(result.fireCap).toBe(5 * 10);
});
it('Multiple element caps should contribute to elementCap', () => {
const result = computeStats({ fireManaCap: 3, waterManaCap: 2 }, emptyPrestige);
// fireCap=30, waterCap=20 -> elementCap = 10 + 30 + 20 = 60
expect(result.elementCap).toBe(60);
});
it('All base element caps should contribute', () => {
const result = computeStats({
fireManaCap: 10, waterManaCap: 10, airManaCap: 10, earthManaCap: 10,
}, emptyPrestige);
// Each adds 10*10=100, total 400 + base 10 = 410
expect(result.elementCap).toBe(410);
});
});
describe('Hybrid skills', () => {
it('Pact-Weaving should multiply enchantPower', () => {
const result = computeStats({ pactWeaving: 3 }, emptyPrestige);
expect(result.enchantPower).toBeCloseTo(1.331, 2); // 1.1^3
});
it('Guardian Constructs should affect golemDamage and golemDuration', () => {
const result = computeStats({ guardianConstructs: 2 }, emptyPrestige);
expect(result.golemDamage).toBeCloseTo(1.3225, 2); // 1.15^2
expect(result.golemDuration).toBe(1.5); // add 0.25*2=0.5, but cap floor is 1, so 1 + 0.5 = 1.5
});
it('Enchanted Golemancy should affect enchantPower and golemDamage', () => {
const result = computeStats({ enchantedGolemancy: 3 }, emptyPrestige);
expect(result.enchantPower).toBeCloseTo(1.157625, 2); // 1.05^3
expect(result.golemDamage).toBeCloseTo(1.331, 2); // 1.1^3
});
});
describe('Prestige upgrades', () => {
it('manaWell prestige should increase maxMana', () => {
const result = computeStats({}, { manaWell: 3 });
expect(result.maxMana).toBe(100 + 3 * 500);
});
it('manaFlow prestige should increase manaRegen', () => {
const result = computeStats({}, { manaFlow: 2 });
expect(result.manaRegen).toBe(2 + 2 * 0.5);
});
it('elementalAttune prestige should increase elementCap', () => {
const result = computeStats({}, { elementalAttune: 4 });
expect(result.elementCap).toBe(10 + 4 * 25);
});
it('pactBinding prestige should increase pactMultiplier', () => {
const result = computeStats({}, { pactBinding: 2 });
expect(result.pactMultiplier).toBe(1 + 2 * 0.1);
});
it('insightAmp prestige should multiply insightGain', () => {
const result = computeStats({}, { insightAmp: 2 });
expect(result.insightGain).toBe(1 + 1 * 0.25 * 2);
});
it('prestige should stack with skills', () => {
const result = computeStats({ manaWell: 5 }, { manaWell: 3 });
expect(result.maxMana).toBe(100 + 5 * 100 + 3 * 500);
});
});
describe('Skill stacking', () => {
it('should correctly stack multiple skills', () => {
const result = computeStats({
manaWell: 3,
manaFlow: 2,
manaTap: 1,
precision: 4,
}, emptyPrestige);
expect(result.maxMana).toBe(100 + 300);
expect(result.manaRegen).toBe(2 + 2);
expect(result.clickMana).toBe(1 + 1);
expect(result.critChance).toBe(0.2);
});
it('should handle all skills with effects at once without interference', () => {
// Only test skills that have effects defined (skip research skills with empty effects)
const allSkillsWithEffects: Record<string, number> = {};
for (const [id, def] of Object.entries(SKILLS_V2)) {
if (def.effects.length > 0) {
allSkillsWithEffects[id] = 1;
}
}
const result = computeStats(allSkillsWithEffects, emptyPrestige);
// Should not throw and should produce reasonable values
expect(result.maxMana).toBeGreaterThan(0);
expect(result.manaRegen).toBeGreaterThan(0);
expect(result.baseDamage).toBeGreaterThan(0);
expect(result.elementCap).toBeGreaterThanOrEqual(10);
});
});
describe('edge cases', () => {
it('should ignore negative levels (no effect applied)', () => {
const result = computeStats({}, emptyPrestige);
expect(result.maxMana).toBe(100);
});
it('should ignore unknown skill IDs', () => {
const result = computeStats({ unknownSkill: 5 } as any, emptyPrestige);
expect(result.maxMana).toBe(100);
});
it('should clamp critChance to 1.0', () => {
const result = computeStats({ precision: 100 } as any, emptyPrestige);
expect(result.critChance).toBe(1.0);
});
it('should clamp armorPierce to 1.0', () => {
const result = computeStats({ armorPiercing: 100 } as any, emptyPrestige);
expect(result.armorPierce).toBe(1.0);
});
it('should clamp attackSpeed minimum to 0.1', () => {
const result = computeStats({ attackSpeed: 100 } as any, emptyPrestige);
expect(result.attackSpeed).toBe(0.1);
});
it('should clamp maxMana minimum to 1', () => {
const result = computeStats({}, emptyPrestige);
expect(result.maxMana).toBeGreaterThanOrEqual(1);
});
it('should clamp baseDamage minimum to 1', () => {
const result = computeStats({}, emptyPrestige);
expect(result.baseDamage).toBeGreaterThanOrEqual(1);
});
});
});
describe('getBaseSkillId()', () => {
it('should strip _tN suffix for tiered skills', () => {
expect(getBaseSkillId('manaWell_t2')).toBe('manaWell');
expect(getBaseSkillId('manaWell_t5')).toBe('manaWell');
expect(getBaseSkillId('quickLearner_t3')).toBe('quickLearner');
});
it('should return same ID for non-tiered skills', () => {
expect(getBaseSkillId('manaWell')).toBe('manaWell');
expect(getBaseSkillId('fireManaCap')).toBe('fireManaCap');
});
});
describe('hasPrerequisites()', () => {
it('should return true when no prerequisites', () => {
expect(hasPrerequisites({}, undefined)).toBe(true);
expect(hasPrerequisites({}, {})).toBe(true);
});
it('should return true when prerequisites are met', () => {
expect(hasPrerequisites({ manaWell: 5 }, { manaWell: 3 })).toBe(true);
});
it('should return false when prerequisites are not met', () => {
expect(hasPrerequisites({ manaWell: 2 }, { manaWell: 3 })).toBe(false);
expect(hasPrerequisites({}, { manaWell: 1 })).toBe(false);
});
it('should check multiple prerequisites', () => {
expect(hasPrerequisites({ a: 2, b: 3 }, { a: 1, b: 2 })).toBe(true);
expect(hasPrerequisites({ a: 2, b: 1 }, { a: 1, b: 2 })).toBe(false);
});
});
describe('SKILLS_V2', () => {
it('should have manaWell defined', () => {
expect(SKILLS_V2.manaWell).toBeDefined();
expect(SKILLS_V2.manaWell.id).toBe('manaWell');
expect(SKILLS_V2.manaWell.maxLevel).toBe(10);
expect(SKILLS_V2.manaWell.effects).toHaveLength(1);
});
it('should have all core skills defined', () => {
const coreSkills = ['manaWell', 'manaFlow', 'quickLearner', 'focusedMind', 'meditation'];
for (const id of coreSkills) {
expect(SKILLS_V2[id]).toBeDefined();
expect(SKILLS_V2[id].effects.length).toBeGreaterThan(0);
}
});
it('should have correct manaWell effect', () => {
const effect = SKILLS_V2.manaWell.effects[0];
expect(effect.stat).toBe('maxMana');
expect(effect.mode).toBe('add');
expect(effect.valuePerLevel).toBe(100);
});
it('should have correct manaFlow effect', () => {
const effect = SKILLS_V2.manaFlow.effects[0];
expect(effect.stat).toBe('manaRegen');
expect(effect.mode).toBe('add');
expect(effect.valuePerLevel).toBe(1);
});
it('should handle prerequisite fields', () => {
expect(SKILLS_V2.manaOverflow.prerequisites).toEqual({ manaWell: 3 });
expect(SKILLS_V2.deepTrance.prerequisites).toEqual({ meditation: 1 });
expect(SKILLS_V2.manaTap.prerequisites).toBeUndefined();
});
it('should handle attunementRequired fields', () => {
expect(SKILLS_V2.enchanting.attunementRequired).toBe('enchanter');
expect(SKILLS_V2.invocation.attunementRequired).toBe('invoker');
expect(SKILLS_V2.golemMastery.attunementRequired).toBe('fabricator');
expect(SKILLS_V2.manaWell.attunementRequired).toBeUndefined();
});
});
console.log('✅ computeStats() and skill v2 tests defined.');
@@ -3,7 +3,7 @@
*/
import { describe, it, expect } from 'vitest';
import { calcDamage, getFloorMaxHP, getFloorElement } from '../index';
import { calcDamage, getFloorMaxHP, getFloorElement } from '@/lib/game/stores/index';
import type { GameState } from '../types';
function createMockState(overrides: Partial<GameState> = {}): GameState {
@@ -112,7 +112,7 @@ describe('Combat Calculations', () => {
describe('getFloorMaxHP', () => {
it('should return guardian HP for guardian floors', () => {
// Import GUARDIANS from constants
import { GUARDIANS } from '../../constants';
import { GUARDIANS } from '@/lib/game/constants';
expect(getFloorMaxHP(10)).toBe(GUARDIANS[10].hp);
expect(getFloorMaxHP(100)).toBe(GUARDIANS[100].hp);
});
@@ -3,7 +3,7 @@
*/
import { describe, it, expect } from 'vitest';
import { SKILLS_DEF, PRESTIGE_DEF, GUARDIANS } from '../../constants';
import { SKILLS_DEF, PRESTIGE_DEF, GUARDIANS } from '@/lib/game/constants';
describe('Skill Definitions', () => {
it('all skills should have valid categories', () => {
@@ -3,9 +3,9 @@
*/
import { describe, it, expect } from 'vitest';
import { computeMaxMana, computeRegen, computeClickMana, computeElementMax } from '../index';
import { computeMaxMana, computeRegen, computeClickMana, computeElementMax } from '@/lib/game/stores/index';
import type { GameState } from '../types';
import { ELEMENTS } from '../../constants';
import { ELEMENTS } from '@/lib/game/constants';
function createMockState(overrides: Partial<GameState> = {}): GameState {
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
@@ -3,8 +3,8 @@
*/
import { describe, it, expect } from 'vitest';
import { getMeditationBonus, calcInsight, getIncursionStrength } from '../index';
import { MAX_DAY, INCURSION_START_DAY } from '../../constants';
import { getMeditationBonus, calcInsight, getIncursionStrength } from '@/lib/game/stores/index';
import { MAX_DAY, INCURSION_START_DAY } from '@/lib/game/constants';
import type { GameState } from '../types';
function createMockState(overrides: Partial<GameState> = {}): GameState {
@@ -3,8 +3,8 @@
*/
import { describe, it, expect } from 'vitest';
import { canAffordSpellCost } from '../index';
import { rawCost, elemCost } from '../../constants';
import { canAffordSpellCost } from '@/lib/game/stores/index';
import { rawCost, elemCost } from '@/lib/game/constants';
describe('Spell Cost System', () => {
describe('rawCost', () => {
@@ -3,7 +3,7 @@
*/
import { describe, it, expect } from 'vitest';
import { getStudySpeedMultiplier, getStudyCostMultiplier } from '../index';
import { getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/stores/index';
describe('Study Speed Functions', () => {
describe('getStudySpeedMultiplier', () => {
@@ -3,7 +3,7 @@
*/
import { describe, it, expect } from 'vitest';
import { fmt, fmtDec } from '../index';
import { fmt, fmtDec } from '@/lib/game/stores/index';
describe('Utility Functions', () => {
describe('fmt', () => {
@@ -3,7 +3,7 @@
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { useCombatStore } from '../combatStore';
import { useCombatStore } from '@/lib/game/stores/combatStore';
// Reset stores before each test
beforeEach(() => {
@@ -3,7 +3,7 @@
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { useManaStore } from '../manaStore';
import { useManaStore } from '@/lib/game/stores/manaStore';
// Reset stores before each test
beforeEach(() => {
@@ -3,7 +3,7 @@
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { usePrestigeStore } from '../prestigeStore';
import { usePrestigeStore } from '@/lib/game/stores/prestigeStore';
import { useManaStore } from '../manaStore';
// Reset stores before each test
@@ -3,7 +3,7 @@
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { useSkillStore } from '../skillStore';
import { useSkillStore } from '@/lib/game/stores/skillStore';
// Reset stores before each test
beforeEach(() => {
@@ -3,7 +3,7 @@
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { useUIStore } from '../uiStore';
import { useUIStore } from '@/lib/game/stores/uiStore';
// Reset stores before each test
beforeEach(() => {
@@ -3,9 +3,9 @@
*/
import { describe, it, expect } from 'vitest';
import { calcDamage } from '../../utils';
import { calcDamage } from '@/lib/game/utils';
import type { GameState } from '../../types';
import { ELEMENTS } from '../../constants';
import { ELEMENTS } from '@/lib/game/constants';
function createMockState(overrides: Partial<GameState> = {}): GameState {
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
@@ -3,8 +3,8 @@
*/
import { describe, it, expect } from 'vitest';
import { getFloorMaxHP, getFloorElement } from '../../utils';
import { GUARDIANS } from '../../constants';
import { getFloorMaxHP, getFloorElement } from '@/lib/game/utils';
import { GUARDIANS } from '@/lib/game/constants';
describe('Floor Functions', () => {
describe('getFloorMaxHP', () => {
@@ -3,7 +3,7 @@
*/
import { describe, it, expect } from 'vitest';
import { fmt, fmtDec } from '../../utils';
import { fmt, fmtDec } from '@/lib/game/utils';
describe('Formatting Functions', () => {
describe('fmt (format number)', () => {
@@ -3,7 +3,7 @@
*/
import { describe, it, expect } from 'vitest';
import { GUARDIANS } from '../../constants';
import { GUARDIANS } from '@/lib/game/constants';
describe('Guardians', () => {
it('should have guardians on expected floors', () => {
@@ -3,8 +3,8 @@
*/
import { describe, it, expect } from 'vitest';
import { getIncursionStrength } from '../../utils';
import { MAX_DAY, INCURSION_START_DAY } from '../../constants';
import { getIncursionStrength } from '@/lib/game/utils';
import { MAX_DAY, INCURSION_START_DAY } from '@/lib/game/constants';
describe('Incursion Strength', () => {
describe('getIncursionStrength', () => {
@@ -3,9 +3,9 @@
*/
import { describe, it, expect } from 'vitest';
import { calcInsight } from '../../utils';
import { calcInsight } from '@/lib/game/utils';
import type { GameState } from '../../types';
import { ELEMENTS } from '../../constants';
import { ELEMENTS } from '@/lib/game/constants';
function createMockState(overrides: Partial<GameState> = {}): GameState {
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
@@ -3,9 +3,9 @@
*/
import { describe, it, expect } from 'vitest';
import { computeMaxMana, computeElementMax, computeRegen, computeClickMana } from '../../utils';
import { computeMaxMana, computeElementMax, computeRegen, computeClickMana } from '@/lib/game/utils';
import type { GameState } from '../../types';
import { ELEMENTS } from '../../constants';
import { ELEMENTS } from '@/lib/game/constants';
function createMockState(overrides: Partial<GameState> = {}): GameState {
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
@@ -104,22 +104,43 @@ describe('Mana Calculation Functions', () => {
const state = createMockState({ prestigeUpgrades: { manaWell: 3 } });
expect(computeMaxMana(state)).toBe(100 + 3 * 500);
});
it('should stack manaWell skill and prestige', () => {
const state = createMockState({
skills: { manaWell: 5 },
prestigeUpgrades: { manaWell: 2 },
});
expect(computeMaxMana(state)).toBe(100 + 5 * 100 + 2 * 500);
});
});
describe('computeRegen', () => {
it('should return base regen with no upgrades', () => {
// Base regen is 2, but computeRegen now includes attunement regen
// Enchanter (active, level 1) adds rawManaRegen * 1.5^0 = rawManaRegen
// Default enchanter rawManaRegen is 0.5, so base with enchanter = 2 + 0.5 = 2.5
it('should return base regen with no upgrades (includes attunement regen)', () => {
const state = createMockState();
expect(computeRegen(state)).toBe(2);
// Base 2 + enchanter regen (0.5 * 1 = 0.5) = 2.5
expect(computeRegen(state)).toBeCloseTo(2.5, 1);
});
it('should add regen from manaFlow skill', () => {
const state = createMockState({ skills: { manaFlow: 5 } });
expect(computeRegen(state)).toBe(2 + 5 * 1);
// Base 2 + manaFlow 5 + enchanter 0.5 = 7.5
expect(computeRegen(state)).toBeCloseTo(2 + 5 * 1 + 0.5, 1);
});
it('should add regen from manaSpring skill', () => {
const state = createMockState({ skills: { manaSpring: 1 } });
expect(computeRegen(state)).toBe(2 + 2);
// Base 2 + manaSpring 2 + enchanter 0.5 = 4.5
expect(computeRegen(state)).toBeCloseTo(2 + 2 + 0.5, 1);
});
it('should multiply by temporal echo prestige', () => {
const state = createMockState({ prestigeUpgrades: { temporalEcho: 2 } });
// (2 + 0.5 enchanter) * 1.2 = 3.0
expect(computeRegen(state)).toBeCloseTo(2.5 * 1.2, 1);
});
});
@@ -138,6 +159,28 @@ describe('Mana Calculation Functions', () => {
const state = createMockState({ skills: { manaSurge: 1 } });
expect(computeClickMana(state)).toBe(1 + 3);
});
it('should stack manaTap and manaSurge', () => {
const state = createMockState({ skills: { manaTap: 1, manaSurge: 1 } });
expect(computeClickMana(state)).toBe(1 + 1 + 3);
});
});
describe('computeElementMax', () => {
it('should return base element cap with no upgrades', () => {
const state = createMockState();
expect(computeElementMax(state)).toBe(10);
});
it('should add cap from elemAttune skill', () => {
const state = createMockState({ skills: { elemAttune: 5 } });
expect(computeElementMax(state)).toBe(10 + 5 * 50);
});
it('should add cap from prestige upgrades', () => {
const state = createMockState({ prestigeUpgrades: { elementalAttune: 3 } });
expect(computeElementMax(state)).toBe(10 + 3 * 25);
});
});
});
@@ -3,7 +3,7 @@
*/
import { describe, it, expect } from 'vitest';
import { getMeditationBonus } from '../../utils';
import { getMeditationBonus } from '@/lib/game/utils';
describe('Meditation Bonus', () => {
describe('getMeditationBonus', () => {
@@ -3,7 +3,7 @@
*/
import { describe, it, expect } from 'vitest';
import { PRESTIGE_DEF } from '../../constants';
import { PRESTIGE_DEF } from '@/lib/game/constants';
describe('Prestige Upgrades', () => {
it('should have prestige upgrades with valid costs', () => {
@@ -3,7 +3,7 @@
*/
import { describe, it, expect } from 'vitest';
import { SKILLS_DEF } from '../../constants';
import { SKILLS_DEF } from '@/lib/game/constants';
describe('Skill Definitions', () => {
it('should have skills with valid categories', () => {

Some files were not shown because too many files have changed in this diff Show More