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
@@ -48,5 +48,3 @@ prompt
|
||||
|
||||
server.log
|
||||
# Skills directory
|
||||
/.zscripts/
|
||||
.gitnexus
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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!")
|
||||
@@ -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=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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 |
|
||||
@@ -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
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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: **8–10 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.
|
||||
@@ -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 +0,0 @@
|
||||
Here are all the generated files.
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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!")
|
||||
@@ -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!")
|
||||
@@ -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!")
|
||||
@@ -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 | });
|
||||
```
|
||||
|
After Width: | Height: | Size: 252 KiB |
|
After Width: | Height: | Size: 243 KiB |
|
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 | });
|
||||
```
|
||||
|
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 | });
|
||||
```
|
||||
|
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 | });
|
||||
```
|
||||
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 243 KiB |
|
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 | });
|
||||
```
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({ message: "Hello, world!" });
|
||||
}
|
||||
@@ -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', () => {
|
||||
|
||||