diff --git a/.accesslog b/.accesslog deleted file mode 100755 index e69de29..0000000 diff --git a/.gitignore b/.gitignore index c05605a..583f01b 100755 --- a/.gitignore +++ b/.gitignore @@ -48,5 +48,3 @@ prompt server.log # Skills directory -/.zscripts/ -.gitnexus diff --git a/.gitnexus/lbug b/.gitnexus/lbug new file mode 100644 index 0000000..ae78bee Binary files /dev/null and b/.gitnexus/lbug differ diff --git a/.gitnexus/meta.json b/.gitnexus/meta.json new file mode 100644 index 0000000..ea74d76 --- /dev/null +++ b/.gitnexus/meta.json @@ -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 + } +} \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit index 858555d..992f9b4 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -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 diff --git a/.husky/scripts/check-file-size.js b/.husky/scripts/check-file-size.js index ba20b69..f2b5d9c 100644 --- a/.husky/scripts/check-file-size.js +++ b/.husky/scripts/check-file-size.js @@ -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) { diff --git a/.husky/scripts/generate-dependency-graph.js b/.husky/scripts/generate-dependency-graph.js new file mode 100644 index 0000000..113a629 --- /dev/null +++ b/.husky/scripts/generate-dependency-graph.js @@ -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`); +} \ No newline at end of file diff --git a/3001 b/3001 deleted file mode 100644 index 82adecc..0000000 --- a/3001 +++ /dev/null @@ -1,11 +0,0 @@ -โจฏ Failed to start server -Error: listen EADDRINUSE: address already in use :::3000 - at (Error: listen EADDRINUSE: address already in use :::3000) - at new Promise () { - code: 'EADDRINUSE', - errno: -98, - syscall: 'listen', - address: '::', - port: 3000 -} -[?25h diff --git a/AGENTS.md b/AGENTS.md index 5b9d05f..0ddb81f 100755 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,15 @@ -# Mana Loop - Project Architecture Guide +# Mana Loop โ€” Project Architecture Guide for AI Agents -This document provides a comprehensive overview of the project architecture for AI agents working on this codebase. +This document provides everything an AI agent needs to work on this codebase safely and effectively. **Read it in full before writing a single line of code.** + + +## โš ๏ธ TERMINAL TOOL PROTOCOL +After calling `run_command`, you MUST immediately call `get_process_status` +with `wait: 30` in the SAME response. Never stop generating after a +`status: "running"` result โ€” always follow up immediately. + +After ANY Edit tool call, immediately call read_file to verify +the change is present. If it's not, use write_file instead. --- @@ -26,7 +35,7 @@ git config --global user.email "n8n-gitea@anexim.local" --- -## โš ๏ธ MANDATORY GIT WORKFLOW - MUST BE FOLLOWED +## โš ๏ธ MANDATORY GIT WORKFLOW โ€” MUST BE FOLLOWED **Before starting ANY work, you MUST:** @@ -35,7 +44,7 @@ git config --global user.email "n8n-gitea@anexim.local" cd /home/user/repos/Mana-Loop && git pull origin master ``` -2. **Do your task** - Make all necessary code changes +2. **Do your task** โ€” Make all necessary code changes 3. **Before finishing, commit and push:** ```bash @@ -45,631 +54,558 @@ git config --global user.email "n8n-gitea@anexim.local" git push origin master ``` -**This workflow is ENFORCED and NON-NEGOTIABLE.** Every agent session must: -- Start with `git pull` -- End with `git add`, `git commit`, `git push` - -**Git Remote:** `git@gitea.tailf367e3.ts.net:Anexim/Mana-Loop.git` +This workflow is **enforced and non-negotiable**. Every session must start with `git pull` and end with `git push`. --- -## Project Overview +## ๐ŸŽฏ CURRENT PRIORITY โ€” READ THIS FIRST -**Mana Loop** is an incremental/idle game built with: -- **Framework**: Next.js 16 with App Router -- **Language**: TypeScript 5 -- **Styling**: Tailwind CSS 4 with shadcn/ui components -- **State Management**: Zustand with persist middleware (modular store architecture) -- **Database**: Prisma ORM with SQLite (for persistence features) +Every session must start by reading the active strategy document to know what to work on: + +```bash +cat docs/strategy/overall-remediation-plan.md +``` + +The strategy defines the current phase and task queue. **Do not pick tasks on your own.** Always work on the highest-priority incomplete task from the strategy. + +Current active task queue (update this when the strategy changes): + +| # | Task | Status | +|---|------|--------| +| TASK-001 | Playwright setup + baseline E2E tests | โฌœ | +| TASK-002 | Enchanting E2E tests (3-step flow) | โฌœ | +| TASK-003 | Equipment E2E tests (equip, 2H block, unequip) | โฌœ | +| TASK-004 | Combat E2E tests (cast, HP, floor advance) | โฌœ | +| TASK-005 | `globals.css` design tokens | โฌœ | +| TASK-006 | Left panel redesign | โฌœ | +| TASK-007 | Skill system v2 (`computeStats` + migration) | โฌœ | +| TASK-008 | Enchanting UI fix (store as source of truth) | โฌœ | +| TASK-009 | Attunement expansion (new attunements + path choice) | โฌœ | +| TASK-010 | Prestige rework (path bonuses) | โฌœ | +| TASK-011 | Full UI redesign (remaining tabs) | โฌœ | + +--- + +## ๐Ÿ”„ Crash Recovery Protocol + +If the agent's session ends mid-task, a fresh session **must** resume from where it left off, not restart. + +### Saving progress (MANDATORY after every significant step): +``` +Call save_progress with: + task_id: "TASK-XXX-task-name" + status: "in_progress" + completed_steps: ["step 1 description", "step 2 description"] + next_step: "what to do next" + notes: "any context for the next session" + project_path: "/home/user/repos/Mana-Loop" +``` + +### On session start โ€” check for existing progress: +```bash +cat docs/.workflow/TASK-XXX-task-name.json 2>/dev/null +``` + +If a Work-in-Progress entry exists in the active-task-log: +- **Resume from the last recorded step.** Do not restart the task. +- Read the saved notes to understand context. + +If no Work-in-Progress entry exists: +- Initialize the task using the Task Initialization Protocol below. + +--- + +## ๐Ÿ“– MANDATORY CONTEXT FILES โ€” READ BEFORE EVERY TASK + +**You must read these files at the start of every session before writing any code:** + +```bash +# 1. Auto-generated project file tree +cat docs/project-structure.txt + +# 2. Module dependency graph โ€” who imports whom +cat docs/dependency-graph.json + +# 3. Circular dependency report โ€” red alert if non-empty +cat docs/circular-deps.txt + +# 4. The strategy document (active priorities) +cat docs/strategy/overall-remediation-plan.md + +# 5. Any existing progress checkpoint for current task +cat docs/.workflow/TASK-*.json 2>/dev/null +``` + +--- + +### How to use `docs/dependency-graph.json` + +The graph is a JSON object where **keys are files** and **values are arrays of files they import**. + +**To find the blast radius of changing a file** (what will break): +```bash +node -e " +const d = require('./docs/dependency-graph.json').graph; +const target = 'stores/skillStore.ts'; +const affected = Object.entries(d).filter(([,deps]) => deps.some(dep => dep.includes(target))); +affected.forEach(([f]) => console.log(f)); +" +``` + +**To find what a file depends on** (its imports): +```bash +node -e " +const d = require('./docs/dependency-graph.json').graph; +const key = Object.keys(d).find(k => k.includes('skillStore')); +console.log(JSON.stringify(d[key], null, 2)); +" +``` + +### How to use `docs/circular-deps.txt` + +If this file contains anything other than "No circular dependencies found", **stop and fix the circulars before touching any of the involved files**. Circular imports are the #1 cause of silent runtime failures in this codebase. + +### When to re-generate the dependency graph manually + +The graph is auto-generated on commit via husky, but if you need fresh data mid-session: +```bash +node .husky/scripts/generate-dependency-graph.js +``` + +--- + +## ๐Ÿงฐ AVAILABLE TOOLS IN THIS ENVIRONMENT + +The following OpenWebUI tools are available in this agent session. Use them โ€” they exist to make your work safer and faster. + +--- + +### 1. Sub Agent (`run_sub_agent`, `run_parallel_sub_agents`) + +**What it does:** Delegates a tool-heavy task to a fresh isolated context. The sub-agent runs its own tool loop and returns only the final result, keeping the main conversation context clean. + +**When to use it:** +- Any investigation that requires 3+ sequential tool calls (file reads, git operations, analysis) +- Parallel research tasks that don't depend on each other +- Heavy grep/search operations that would pollute the main context + +**How to invoke:** +``` +Call run_sub_agent with: + description: "Brief task summary shown as status" + prompt: "Full detailed instructions โ€” include ALL context the sub-agent needs, + it has NO access to this conversation history" +``` + +**Critical:** The sub-agent starts with zero context. Paste the relevant file contents, task spec, and constraints directly into the `prompt` field. Do not assume it knows anything. + +**Parallel tasks:** +``` +Call run_parallel_sub_agents with: + tasks: [ + { description: "...", prompt: "..." }, + { description: "...", prompt: "..." } + ] +``` +Use this when tasks are independent (e.g. investigating combat while also reading skill files). + +--- + +### 2. Dev Workflow Pro (`save_progress`, `run_checks`) + +**What it does:** Runs quality checks (`typecheck`, `lint`, `test`) from within the agent session, and saves task progress as JSON checkpoints in `docs/.workflow/`. + +**โš ๏ธ Configuration required:** The `project_path` valve must be set to `/home/user/repos/Mana-Loop` for this tool to work. If it returns "Project path is missing", the valve isn't configured โ€” fall back to `run_sub_agent` with a shell command. + +**Verify it works:** +``` +Call run_checks with: + checks: ["lint"] + project_path: "/home/user/repos/Mana-Loop" +``` +If this returns โœ… lint output, it's working. If it errors, use the sub-agent fallback below. + +**Sub-agent fallback (always works):** +``` +Call run_sub_agent with: + description: "Run lint and typecheck" + prompt: "cd /home/user/repos/Mana-Loop && bun run lint && bun run typecheck. + Return the full output including any errors." +``` + +**Saving progress between sessions:** +``` +Call save_progress with: + task_id: "TASK-001-playwright-setup" + status: "in_progress" + completed_steps: ["installed playwright", "configured playwright.config.ts"] + next_step: "write first E2E test for enchanting flow" + notes: "need to verify test:ui script works before writing tests" + project_path: "/home/user/repos/Mana-Loop" +``` +Progress is saved to `docs/.workflow/TASK-001-playwright-setup.json`. Read it at the start of a resumed task. + +--- + +### 3. Advisor (`ask_advisor`, `debate`) + +**What it does:** Calls a specialist LLM in complete isolation to get a second opinion before making a decision. Zero context pollution โ€” only your question and the context you paste reaches it. + +**When to use it โ€” this is mandatory for:** +- Splitting or refactoring any file โ†’ use `advisor_type: "refactor"` +- Any new game mechanic that touches more than 2 systems โ†’ use `advisor_type: "code"` +- Design decisions that could affect balance or lore โ†’ use `advisor_type: "lore"` +- Before committing a significant implementation โ†’ use `advisor_type: "review"` + +**Advisor types:** +| Type | When to use | +|------|-------------| +| `refactor` | Before splitting any file. Paste the FULL file content in `context`. | +| `code` | TypeScript / React / Zustand / game architecture decisions | +| `lore` | Game mechanic consistency, attunement balance, prestige design | +| `review` | Bug-finding pass on a plan or code snippet before committing | +| `challenge` | Argues AGAINST your approach โ€” run before major architectural decisions | +| `general` | Anything else | + +**Example โ€” checking a refactor plan:** +``` +Call ask_advisor with: + question: "I want to split skillStore.ts into skillStore.ts and skillEvolution.ts. + Is this safe? What are the circular import risks?" + advisor_type: "refactor" + context: "[paste the full skillStore.ts content here]" +``` + +--- + +### 4. Game Dev Agent Skills (Inlet Filter) + +**What it does:** Automatically injects engineering skill workflows (spec, TDD, debug, refactor, perf, review, security) into the system prompt based on keywords in your message. + +**โš ๏ธ This is a Filter, not a Tool โ€” it requires specific setup to work:** + +1. It must be added as a **Filter** in OpenWebUI under Workspace โ†’ Functions โ†’ Filters (NOT under Tools) +2. It must be toggled **ON** for the current model or workspace +3. Keyword detection is automatic โ€” the workflow injects based on what words appear in the user's message + +**Trigger keywords:** +| Skill | Keywords that activate it | +|-------|--------------------------| +| `spec` | spec, specifications, prd, scope, requirements | +| `debug` | bug, error, crash, broken, fix, not working | +| `plan` | plan, breakdown, tasks, todo, roadmap, milestone | +| `implement` | implement, build, write, create, add feature | +| `tdd` | test, tdd, unit test, failing test, coverage, vitest | +| `perf` | slow, lag, fps, frame time, memory leak, optimise | +| `review` | review, code quality, refactor, clean up, PR | +| `security` | auth, login, token, exploit, injection, hack | + +**If it's not injecting:** Check that it's installed as a Filter (not a Tool) and is enabled for the active model. In forced mode, set the `forced_skill` valve to the skill you want instead of `"auto"`. + +--- + +## ๐Ÿ”ด BEFORE EDITING ANY FILE โ€” Impact Analysis Protocol + +Before modifying a function, store, or component, always answer these questions: + +```bash +# 1. Who imports this file? +node -e " +const d = require('./docs/dependency-graph.json').graph; +const me = 'stores/skillStore.ts'; // change this +Object.entries(d).filter(([,v]) => v.some(dep => dep.includes(me))) + .forEach(([f]) => console.log('imports me:', f)); +" + +# 2. Are there circular deps involving this file? +grep -i "skillStore" docs/circular-deps.txt + +# 3. What tests cover this file? +find src -name "*.test.ts" | xargs grep -l "skillStore" 2>/dev/null +``` + +If the impact is HIGH (5+ files import it, or it's in a circular chain) โ€” call the Advisor with `advisor_type: "review"` or `advisor_type: "challenge"` before making changes. + +--- + +## ๐Ÿ—‚๏ธ Project Overview + +**Mana Loop** is a browser-based incremental/idle game built with: +- **Framework:** Next.js 16 with App Router +- **Language:** TypeScript 5 +- **Styling:** Tailwind CSS 4 with shadcn/ui components +- **State Management:** Zustand with persist middleware (modular store architecture) +- **Database:** Prisma ORM with SQLite +- **Test Runner:** Vitest +- **Package Manager:** Bun ## Core Game Loop -1. **Mana Gathering**: Click or auto-generate mana over time -2. **Studying**: Spend mana to learn skills and spells -3. **Combat**: Climb the Spire, defeat guardians, sign pacts -4. **Crafting**: Enchant equipment with spell effects -5. **Prestige**: Reset progress for permanent bonuses (Insight) - -## Directory Structure - -``` -src/ -โ”œโ”€โ”€ app/ -โ”‚ โ”œโ”€โ”€ page.tsx # Main game UI (~reduced via component extraction) -โ”‚ โ”œโ”€โ”€ layout.tsx # Root layout with providers -โ”‚ โ”œโ”€โ”€ api/ # API routes (minimal use) -โ”‚ โ””โ”€โ”€ components/ # App-level components -โ”‚ โ”œโ”€โ”€ GameOverScreen.tsx -โ”‚ โ”œโ”€โ”€ LeftPanel.tsx -โ”‚ โ””โ”€โ”€ ... -โ”œโ”€โ”€ components/ -โ”‚ โ”œโ”€โ”€ ui/ # shadcn/ui components (auto-generated) -โ”‚ โ””โ”€โ”€ game/ # All game components (modular structure) -โ”‚ โ”œโ”€โ”€ index.ts # Barrel exports -โ”‚ โ”œโ”€โ”€ GameContext.tsx -โ”‚ โ”œโ”€โ”€ ActionButtons.tsx -โ”‚ โ”œโ”€โ”€ CalendarDisplay.tsx -โ”‚ โ”œโ”€โ”€ CraftingProgress.tsx -โ”‚ โ”œโ”€โ”€ ManaDisplay.tsx -โ”‚ โ”œโ”€โ”€ TimeDisplay.tsx -โ”‚ โ”œโ”€โ”€ tabs/ # Tab-specific components -โ”‚ โ”‚ โ”œโ”€โ”€ index.ts -โ”‚ โ”‚ โ”œโ”€โ”€ AchievementsTab.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ AttunementsTab.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ CraftingTab.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ DebugTab.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ EquipmentTab.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ GolemancyTab.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ LabTab.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ LootTab.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ PrestigeTab.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ SkillsTab.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ SpellsTab.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ SpireTab.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ StatsTab.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ EquipmentSlotGrid.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ EquipmentControls.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ EnchantmentsPanel.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ EquipmentInventory.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ SkillRow.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ SkillCategoryHeader.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ MilestoneProgress.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ SkillMultipliers.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ SpireHeader.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ GuardianPanel.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ RoomDisplay.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ FloorControls.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ CombatStatsPanel.tsx -โ”‚ โ”‚ โ””โ”€โ”€ ActivityLog.tsx -โ”‚ โ”œโ”€โ”€ crafting/ # Crafting-specific components -โ”‚ โ”‚ โ”œโ”€โ”€ index.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ EnchantmentDesigner/ -โ”‚ โ”‚ โ”œโ”€โ”€ EnchantmentApplier.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ EnchantmentPreparer.tsx -โ”‚ โ”‚ โ””โ”€โ”€ EquipmentCrafter.tsx -โ”‚ โ”œโ”€โ”€ stats/ # Stats display components -โ”‚ โ”‚ โ”œโ”€โ”€ index.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ CombatStatsSection.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ ManaStatsSection.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ ManaTypeBreakdown.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ StudyStatsSection.tsx -โ”‚ โ”‚ โ””โ”€โ”€ UpgradeEffectsSection.tsx -โ”‚ โ”œโ”€โ”€ debug/ # Debug tools -โ”‚ โ”‚ โ”œโ”€โ”€ index.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ SkillDebug.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ ElementDebug.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ AttunementDebug.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ GolemDebug.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ PactDebug.tsx -โ”‚ โ”‚ โ””โ”€โ”€ GameStateDebug.tsx -โ”‚ โ”œโ”€โ”€ shared/ # Shared sub-components -โ”‚ โ”‚ โ”œโ”€โ”€ MemorySlotPicker.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ StudyProgress.tsx -โ”‚ โ”‚ โ””โ”€โ”€ UpgradeDialog.tsx -โ”‚ โ””โ”€โ”€ LootInventory/ # Loot display components -โ”‚ โ”œโ”€โ”€ index.tsx -โ”‚ โ”œโ”€โ”€ MaterialItem.tsx -โ”‚ โ”œโ”€โ”€ EssenceItem.tsx -โ”‚ โ”œโ”€โ”€ BlueprintsSection.tsx -โ”‚ โ”œโ”€โ”€ EquipmentItem.tsx -โ”‚ โ””โ”€โ”€ LootInventoryDisplay.tsx -โ””โ”€โ”€ lib/ - โ”œโ”€โ”€ game/ - โ”‚ โ”œโ”€โ”€ stores/ # Modular Zustand stores (NEW) - โ”‚ โ”‚ โ”œโ”€โ”€ index.ts # Combined store exports - โ”‚ โ”‚ โ”œโ”€โ”€ gameStore.ts # Main store (~11KB, core state + tick) - โ”‚ โ”‚ โ”œโ”€โ”€ manaStore.ts # Mana state and actions (~9KB) - โ”‚ โ”‚ โ”œโ”€โ”€ combatStore.ts # Combat system (~9KB) - โ”‚ โ”‚ โ”œโ”€โ”€ prestigeStore.ts # Prestige/loop system (~8KB) - โ”‚ โ”‚ โ”œโ”€โ”€ skillStore.ts # Skill state and actions (~11KB) - โ”‚ โ”‚ โ”œโ”€โ”€ uiStore.ts # UI state (~2KB) - โ”‚ โ”‚ โ”œโ”€โ”€ gameLoopActions.ts # Game loop logic - โ”‚ โ”‚ โ”œโ”€โ”€ gameActions.ts # Generic game actions - โ”‚ โ”‚ โ””โ”€โ”€ gameHooks.ts # Store hooks - โ”‚ โ”œโ”€โ”€ store/ # Legacy store slices (migration in progress) - โ”‚ โ”‚ โ”œโ”€โ”€ index.ts # Re-exports from store.ts + computed utils - โ”‚ โ”‚ โ”œโ”€โ”€ combatSlice.ts # Combat state slice - โ”‚ โ”‚ โ”œโ”€โ”€ manaSlice.ts # Mana state slice - โ”‚ โ”‚ โ”œโ”€โ”€ skillSlice.ts # Skill state slice - โ”‚ โ”‚ โ”œโ”€โ”€ craftingSlice.ts # Crafting state slice - โ”‚ โ”‚ โ””โ”€โ”€ computed.ts # Computed stats - โ”‚ โ”œโ”€โ”€ store-modules/ # Legacy store utilities - โ”‚ โ”œโ”€โ”€ crafting-actions/ # Modular crafting system (NEW) - โ”‚ โ”‚ โ”œโ”€โ”€ index.ts - โ”‚ โ”‚ โ”œโ”€โ”€ application-actions.ts - โ”‚ โ”‚ โ”œโ”€โ”€ design-actions.ts - โ”‚ โ”‚ โ”œโ”€โ”€ preparation-actions.ts - โ”‚ โ”‚ โ”œโ”€โ”€ equipment-actions.ts - โ”‚ โ”‚ โ”œโ”€โ”€ crafting-equipment-actions.ts - โ”‚ โ”‚ โ”œโ”€โ”€ disenchant-actions.ts - โ”‚ โ”‚ โ””โ”€โ”€ computed-getters.ts - โ”‚ โ”œโ”€โ”€ skill-evolution-modules/ # Modular skill evolution (NEW) - โ”‚ โ”‚ โ”œโ”€โ”€ index.ts # Main export (~11KB) - โ”‚ โ”‚ โ”œโ”€โ”€ mana-well-flow.ts # Mana Well/Flow skills (~15KB) - โ”‚ โ”‚ โ”œโ”€โ”€ quick-learner.ts # Quick Learner skill (~7KB) - โ”‚ โ”‚ โ”œโ”€โ”€ focused-mind.ts # Focused Mind skill (~6KB) - โ”‚ โ”‚ โ”œโ”€โ”€ enchanting-skills.ts # Enchanting skills (~15KB) - โ”‚ โ”‚ โ”œโ”€โ”€ invocation-skills.ts # Invocation skills (~15KB) - โ”‚ โ”‚ โ”œโ”€โ”€ hybrid-skills.ts # Hybrid skills (~22KB) - โ”‚ โ”‚ โ”œโ”€โ”€ guardian-skills.ts # Guardian-related skills (~4KB) - โ”‚ โ”‚ โ”œโ”€โ”€ insight-harvest.ts # Insight Harvest skill (~7KB) - โ”‚ โ”‚ โ”œโ”€โ”€ mana-utility-skills.ts # Mana utility skills (~7KB) - โ”‚ โ”‚ โ”œโ”€โ”€ elemental-attunement.ts # Elemental Attunement (~7KB) - โ”‚ โ”‚ โ”œโ”€โ”€ knowledge-retention.ts # Knowledge Retention (~4KB) - โ”‚ โ”‚ โ”œโ”€โ”€ learning-skills.ts # Learning skills (~1KB) - โ”‚ โ”‚ โ”œโ”€โ”€ magic-skills.ts # Magic skills (~1KB) - โ”‚ โ”‚ โ”œโ”€โ”€ utils.ts # Skill evolution utilities - โ”‚ โ”‚ โ””โ”€โ”€ types.ts # TypeScript interfaces - โ”‚ โ”œโ”€โ”€ constants/ # Modular constants (NEW) - โ”‚ โ”‚ โ”œโ”€โ”€ index.ts - โ”‚ โ”‚ โ”œโ”€โ”€ core.ts # Core game constants - โ”‚ โ”‚ โ”œโ”€โ”€ elements.ts # Element definitions - โ”‚ โ”‚ โ”œโ”€โ”€ guardians.ts # Guardian definitions - โ”‚ โ”‚ โ”œโ”€โ”€ prestige.ts # Prestige upgrade definitions - โ”‚ โ”‚ โ”œโ”€โ”€ rooms.ts # Room type definitions - โ”‚ โ”‚ โ”œโ”€โ”€ skills.ts # Skill definitions (~30KB) - โ”‚ โ”‚ โ”œโ”€โ”€ spells.ts # Spell definitions - โ”‚ โ”‚ โ””โ”€โ”€ spells-modules/ # Spell sub-modules - โ”‚ โ”œโ”€โ”€ data/ # Game data definitions (NEW) - โ”‚ โ”‚ โ”œโ”€โ”€ enchantment-effects.ts - โ”‚ โ”‚ โ”œโ”€โ”€ enchantments/ # Enchantment definitions - โ”‚ โ”‚ โ”œโ”€โ”€ equipment/ # Equipment definitions - โ”‚ โ”‚ โ”œโ”€โ”€ golems/ # Golem definitions - โ”‚ โ”‚ โ”œโ”€โ”€ achievements.ts - โ”‚ โ”‚ โ”œโ”€โ”€ crafting-recipes.ts - โ”‚ โ”‚ โ””โ”€โ”€ loot-drops.ts - โ”‚ โ”œโ”€โ”€ crafting-slice.ts # Legacy crafting (being modularized) - โ”‚ โ”œโ”€โ”€ skill-evolution.ts # Legacy skill evolution (reduced, ~1.5KB) - โ”‚ โ”œโ”€โ”€ constants.ts # Legacy constants (reduced, ~1KB) - โ”‚ โ”œโ”€โ”€ store.ts # Legacy store (reduced, ~14KB) - โ”‚ โ”œโ”€โ”€ computed-stats.ts # Computed stats functions - โ”‚ โ”œโ”€โ”€ navigation-slice.ts # Floor navigation actions - โ”‚ โ”œโ”€โ”€ study-slice.ts # Study system actions - โ”‚ โ”œโ”€โ”€ effects.ts # Unified effect computation - โ”‚ โ”œโ”€โ”€ upgrade-effects.ts # Skill upgrade effect definitions - โ”‚ โ”œโ”€โ”€ types.ts # TypeScript interfaces - โ”‚ โ”œโ”€โ”€ formatting.ts # Display formatters - โ”‚ โ””โ”€โ”€ utils/ # Utility functions - โ””โ”€โ”€ utils.ts # General utilities (cn function) -``` - -*For a complete, up-to-date project tree, see `docs/project-structure.txt` (auto-generated on each commit).* - -### Quick Reference -- **Modular Stores**: `src/lib/game/stores/` (USE THESE) -- **Legacy Store**: `src/lib/game/store.ts` and `src/lib/game/store-modules/` (DEPRECATED - being migrated) -- **Utilities**: `src/lib/game/utils/` (new home for migrated utilities) -- **Project Structure**: `docs/project-structure.txt` (full tree, auto-generated) - -*Note: The project tree in `docs/project-structure.txt` is automatically generated on each commit by the pre-commit hook using `.husky/scripts/generate-project-tree.js` and respects `.gitignore` rules.* - -## Key Systems - -### 1. State Management (Modular Store Architecture) - -The game uses a **modular Zustand store architecture** with multiple specialized stores: - -#### Store Modules (`src/lib/game/stores/`) -- **gameStore.ts**: Core state, tick logic, and main actions (~11KB) -- **manaStore.ts**: Mana gathering, elements, conversion (~9KB) -- **combatStore.ts**: Combat system, spells, floor progression (~9KB) -- **prestigeStore.ts**: Prestige/loop system, insight, upgrades (~8KB) -- **skillStore.ts**: Skill state, studying, evolution (~11KB) -- **uiStore.ts**: UI state, modals, debug settings (~2KB) - -#### Legacy Store Files (Being Migrated/Deprecated) -- **store.ts**: Reduced from ~2812 lines to ~14KB (core logic moved to stores/) -- **store-modules/**: Legacy utility functions being moved to `src/lib/game/utils/` - - `room-utils.ts` โ†’ `utils/room-utils.ts` - - `enemy-utils.ts` โ†’ `utils/enemy-utils.ts` - - `activity-log.ts` โ†’ `utils/activity-log.ts` -- **crafting-slice.ts**: Reduced, being replaced by crafting-actions/ - -#### Store Interaction Pattern -```typescript -// Each store can interact with other stores via get() and custom hooks -// Example from combatStore.ts: -import { useManaStore } from './manaStore'; - -// Access other store state -const manaState = useManaStore.getState(); -``` - -### 2. Crafting System (Modular Architecture) - -The crafting system has been split into focused action modules: - -#### Crafting Action Modules (`src/lib/game/crafting-actions/`) -- **design-actions.ts**: Enchantment design creation (~3KB) -- **preparation-actions.ts**: Equipment preparation (~1KB) -- **application-actions.ts**: Enchantment application (~2KB) -- **equipment-actions.ts**: Equipment management (~2.5KB) -- **crafting-equipment-actions.ts**: Equipment crafting (~2.5KB) -- **disenchant-actions.ts**: Disenchanting logic (~1KB) -- **computed-getters.ts**: Crafting computed values (~2KB) -- **index.ts**: Barrel exports - -### 3. Skill Evolution System (Modular Architecture) - -The massive ~3400-line `skill-evolution.ts` has been split into focused modules: - -#### Skill Evolution Modules (`src/lib/game/skill-evolution-modules/`) -- **index.ts**: Main export combining all skill trees (~11KB) -- **mana-well-flow.ts**: Mana Well and Mana Flow skills (~15KB) -- **quick-learner.ts**: Quick Learner and related skills (~7KB) -- **focused-mind.ts**: Focused Mind and study skills (~6KB) -- **enchanting-skills.ts**: Enchanting skill tree (~15KB) -- **invocation-skills.ts**: Invocation and Pact Mastery (~15KB) -- **hybrid-skills.ts**: Cross-attunement hybrid skills (~22KB) -- **guardian-skills.ts**: Guardian-related skills (~4KB) -- **insight-harvest.ts**: Insight and prestige skills (~7KB) -- **mana-utility-skills.ts**: Utility mana skills (~7KB) -- **elemental-attunement.ts**: Elemental skills (~7KB) -- **knowledge-retention.ts**: Knowledge retention skill (~4KB) -- **learning-skills.ts**: Basic learning skills (~1KB) -- **magic-skills.ts**: Magic-related skills (~1KB) -- **utils.ts**: Shared utilities -- **types.ts**: TypeScript interfaces - -### 4. Constants System (Modular Architecture) - -Game constants have been organized into domain-specific modules: - -#### Constants Modules (`src/lib/game/constants/`) -- **core.ts**: Core game constants (timing, limits) -- **elements.ts**: Element definitions and hierarchies -- **guardians.ts**: Guardian definitions and stats -- **prestige.ts**: Prestige upgrade definitions -- **rooms.ts**: Room type definitions -- **skills.ts**: Complete skill definitions (~30KB) -- **spells.ts**: Spell definitions -- **spells-modules/**: Organized spell sub-modules -- **index.ts**: Barrel exports - -### 5. Game Data (New Structure) - -#### Data Directory (`src/lib/game/data/`) -- **enchantment-effects.ts**: Enchantment effect catalog -- **enchantments/**: Enchantment definitions by category -- **equipment/**: Equipment type definitions -- **golems/**: Golem type definitions -- **achievements.ts**: Achievement definitions -- **crafting-recipes.ts**: Crafting recipe definitions -- **loot-drops.ts**: Loot table definitions - -### Computed Stats (`computed-stats.ts`) -Extracted utility functions for stat calculations: -- `computeMaxMana()`, `computeRegen()`, `computeEffectiveRegen()` -- `calcDamage()`, `calcInsight()`, `getElementalBonus()` -- `getFloorMaxHP()`, `getFloorElement()`, `getMeditationBonus()` -- `canAffordSpellCost()`, `deductSpellCost()` - -```typescript -interface GameState { - // Time - day: number; - hour: number; - paused: boolean; - - // Mana (now in manaStore.ts) - rawMana: number; - elements: Record; - - // Combat (now in combatStore.ts) - currentFloor: number; - floorHP: number; - activeSpell: string; - castProgress: number; - - // Progression (now in skillStore.ts) - skills: Record; - spells: Record; - skillUpgrades: Record; - skillTiers: Record; - - // Equipment - equipmentInstances: Record; - equippedInstances: Record; - enchantmentDesigns: EnchantmentDesign[]; - - // Prestige (now in prestigeStore.ts) - insight: number; - prestigeUpgrades: Record; - signedPacts: number[]; -} -``` - -### Effect System (`effects.ts`) - -**CRITICAL**: All stat modifications flow through the unified effect system. - -```typescript -// Effects come from two sources: -// 1. Skill Upgrades (milestone bonuses) -// 2. Equipment Enchantments (crafted bonuses) - -getUnifiedEffects(state) => UnifiedEffects { - maxManaBonus, maxManaMultiplier, - regenBonus, regenMultiplier, - clickManaBonus, clickManaMultiplier, - baseDamageBonus, baseDamageMultiplier, - attackSpeedMultiplier, - critChanceBonus, critDamageMultiplier, - studySpeedMultiplier, - specials: Set, // Special effect IDs -} -``` - -**When adding new stats**: -1. Add to `ComputedEffects` interface in `upgrade-effects.ts` -2. Add mapping in `computeEquipmentEffects()` in `effects.ts` -3. Apply in the relevant game logic (tick, damage calc, etc.) - -## Important Patterns - -### Adding a New Effect - -1. **Define in `enchantment-effects.ts`** (now in `data/enchantment-effects.ts`): -```typescript -my_new_effect: { - id: 'my_new_effect', - name: 'Effect Name', - description: '+10% something', - category: 'combat', - baseCapacityCost: 30, - maxStacks: 3, - allowedEquipmentCategories: ['caster', 'hands'], - effect: { type: 'multiplier', stat: 'attackSpeed', value: 1.10 } -} -``` - -2. **Add stat mapping in `effects.ts`** (if new stat): -```typescript -// In computeEquipmentEffects() -if (effect.stat === 'myNewStat') { - bonuses.myNewStat = (bonuses.myNewStat || 0) + effect.value; -} -``` - -3. **Apply in game logic**: -```typescript -const effects = getUnifiedEffects(state); -damage *= effects.myNewStatMultiplier; -``` - -### Adding a New Skill - -1. **Define in `constants/skills.ts`** (NEW location) -2. **Add evolution path in `skill-evolution-modules/`** (NEW location) - - Create new module or add to existing module -3. **Export from `skill-evolution-modules/index.ts`** -4. **Update UI in `components/game/tabs/SkillsTab.tsx`** - -### Adding a New Spell - -1. **Define in `constants/spells.ts`** (NEW location) -2. **Add to `constants/spells-modules/`** if categorized -3. **Add spell enchantment in `data/enchantment-effects.ts`** -4. **Add research skill in `constants/skills.ts`** -5. **Map research to effect in `EFFECT_RESEARCH_MAPPING`** - -## Git Hooks (Husky) - -This project uses **Husky** to manage git hooks for automated checks and agent assistance: - -### Pre-Commit Hook (`.husky/pre-commit`) -Runs automatically before each commit: -1. **File Size Check**: Ensures no staged file exceeds 400 lines (improves AI agent readability) -2. **Project Structure Generation**: Updates `docs/project-structure.txt` with current tree (respects `.gitignore`) - -### Post-Merge Hook (`.husky/post-merge`) -Runs after merging branches: -- Checks if `package.json` or `package-lock.json` changed -- Automatically runs `npm install` to sync dependencies - -### Implementation Files -- Hook scripts: `.husky/` directory -- File size check: `.husky/scripts/check-file-size.js` -- Tree generator: `.husky/scripts/generate-project-tree.js` +1. **Mana Gathering** โ€” Click or auto-generate mana over time +2. **Studying** โ€” Spend mana to learn skills and spells +3. **Combat** โ€” Climb the Spire, defeat guardians, sign pacts +4. **Crafting** โ€” Enchant equipment with spell effects via 3-step process +5. **Prestige** โ€” Reset progress for permanent bonuses (Insight) --- -## Common Pitfalls +## Directory Structure -1. **Forgetting to call `getUnifiedEffects()`**: Always use unified effects for stat calculations -2. **Direct stat modification**: Never modify stats directly; use effect system -3. **Missing tier multiplier**: Use `getTierMultiplier(skillId)` for tiered skills -4. **Ignoring special effects**: Check `hasSpecial(effects, SPECIAL_EFFECTS.X)` for special abilities -5. **Not updating modular stores**: Check all stores in `stores/` directory for related state -6. **Bypassing crafting-actions**: Use the modular actions in `crafting-actions/` for new crafting features +**See `docs/project-structure.txt` for the full auto-generated tree.** The key directories: -## Testing +``` +src/lib/game/stores/ # โœ… USE THESE โ€” Zustand store modules +src/lib/game/crafting-actions/ # Modular crafting system +src/lib/game/data/ # Game data definitions +src/lib/game/constants/ # Game constants by domain +src/lib/game/utils/ # Utility functions +src/lib/game/store/ # โš ๏ธ LEGACY โ€” migration in progress +src/lib/game/store-modules/ # โš ๏ธ LEGACY โ€” being migrated to utils/ +src/lib/game/skill-evolution-modules/ # โš ๏ธ LEGACY โ€” being replaced by computeStats() +src/components/game/ # All game UI components (modular structure) +src/lib/game/stores/__tests__/ # Store-level unit tests +e2e/ # Playwright E2E tests (added in Phase 0) +``` -Run `npm run test` before every commit. Tests must pass. +--- -When fixing a bug, write a test that would have caught it first. -Test files live in src/lib/game/stores/__tests__/. +## Key Systems -Critical paths that must always have tests: -- Any store action that modifies rawMana -- Any store action that modifies equippedInstances -- computeRegen, computeMaxMana, calcDamage -- canAffordSpellCost, spendRawMana, deductSpellCost +### State Management -## Testing Guidelines +| Store | File | Responsibility | +|-------|------|---------------| +| Game Store | `stores/gameStore.ts` | Core state, tick loop, equip/unequip | +| Mana Store | `stores/manaStore.ts` | Raw mana, elements, conversion | +| Combat Store | `stores/combatStore.ts` | Spire, spells, floor progression | +| Prestige Store | `stores/prestigeStore.ts` | Insight, upgrades, loop end | +| Skill Store | `stores/skillStore.ts` | Skill levels, studying | +| UI Store | `stores/uiStore.ts` | Modal state, debug flags | -- Run `npm run lint` after changes -- Run `npm run test` to execute unit tests -- Check dev server logs -- Test with fresh game state (clear localStorage) -- **New**: Tests are organized alongside their modules (e.g., `stores/__tests__/`, `store-tests/`) +Cross-store access pattern: +```typescript +// In combatStore.ts, reading mana state: +const manaState = useManaStore.getState(); +``` -## Modular Architecture Pattern +### Effect System (`effects.ts`) โ€” CRITICAL -The codebase has been refactored from large monolithic files into focused, modular components. This improves: -- **Maintainability**: Each module has a single responsibility -- **Readability**: Files are under 400 lines (pre-commit hook enforces this) -- **AI Agent Efficiency**: Smaller files are easier to understand and modify +All stat modifications flow through `getUnifiedEffects(state)`. **Never read skill levels directly to compute a stat** โ€” always use the unified effects object. -### Key Modular Directories +When adding a new stat: +1. Add to `ComputedEffects` interface in `upgrade-effects.ts` +2. Add mapping in `computeEquipmentEffects()` in `effects.ts` +3. Apply in the relevant game logic using `getUnifiedEffects(state)` -| Directory | Purpose | Line Count Target | -|-----------|---------|-------------------| -| `stores/` | Zustand store modules | < 400 lines each | -| `crafting-actions/` | Crafting system actions | < 400 lines each | -| `skill-evolution-modules/` | Skill trees by category | < 400 lines each | -| `constants/` | Game constants by domain | < 400 lines each | -| `data/` | Game data definitions | < 400 lines each | -| `components/game/tabs/` | UI tab components | < 400 lines each | -| `components/game/crafting/` | Crafting UI components | < 400 lines each | -| `components/game/stats/` | Stats display components | < 400 lines each | +### Crafting System (3-Step Enchantment) -### Creating a New Module +The enchantment flow is **Design โ†’ Prepare โ†’ Apply**. Each step is in its own action module: +- `crafting-actions/design-actions.ts` +- `crafting-actions/preparation-actions.ts` +- `crafting-actions/application-actions.ts` -1. **Identify the domain**: Which system does it belong to? -2. **Create focused file**: Keep under 400 lines -3. **Export from index**: Add to barrel export file -4. **Update imports**: Fix all references to old location -5. **Test**: Run lint and tests before committing +The store is the **single source of truth** for enchantment selection state โ€” never use local component state for selected effects. + +### Skill System (v2 โ€” `computeStats()`) + +The old `skill-evolution-modules/` system is being replaced by a flat `computeStats()` function (see strategy doc Phase 1). Until migration is complete: +- Old skills still exist in `skill-evolution-modules/` +- New skills will be defined in `constants/skills-v2.ts` +- `computeStats()` in `constants/skills-v2.ts` is the authoritative stat calculator + +--- + +## โš ๏ธ Common Pitfalls + +1. **Forgetting `getUnifiedEffects()`** โ€” never modify stats directly; use the effect system +2. **Using legacy store** โ€” always use `stores/` (new), not `store/` (legacy) +3. **Not checking circular deps** โ€” read `docs/circular-deps.txt` before modifying shared files +4. **Bypassing crafting-actions** โ€” use modular actions for any crafting feature +5. **Using local state for enchantment selection** โ€” always use the crafting store +6. **Adding skills to `skill-evolution-modules/`** โ€” use `constants/skills-v2.ts` for new skills + +--- + +## ๐Ÿงช Testing Priorities + +Tests are non-negotiable. Run `npm run test` before every commit. All tests must pass. + +**Priority order (if time is short, protect these first):** + +| Priority | Test Type | What | +|----------|-----------|------| +| ๐Ÿ”ด Critical | E2E | Enchantment flow (Design โ†’ Prepare โ†’ Apply) | +| ๐Ÿ”ด Critical | E2E | Gear equipping + 2H weapon blocking | +| ๐Ÿ”ด Critical | E2E | Combat tick and floor advancement | +| ๐ŸŸก High | E2E | Locked effects cannot be selected | +| ๐ŸŸก High | Unit | Store actions that modify `rawMana` | +| ๐ŸŸก High | Unit | Store actions that modify `equippedInstances` | +| ๐ŸŸก High | Unit | `computeStats()` returns correct values per skill level | +| ๐ŸŸข Medium | Unit | `computeRegen`, `computeMaxMana`, `calcDamage` | +| ๐ŸŸข Medium | Unit | `canAffordSpellCost`, `spendRawMana`, `deductSpellCost` | + +**When fixing a bug:** write the failing test FIRST, then fix until it passes. + +Test locations: +- E2E: `e2e/*.spec.ts` +- Store tests: `src/lib/game/stores/__tests__/` +- Store-level tests: `src/lib/game/store-tests/` +- Split tests: `src/lib/game/stores-split-tests/` + +--- + +## Git Hooks (Husky) + +### Pre-Commit Hook (`.husky/pre-commit`) + +Runs automatically before each commit: +1. **File Size Check** โ€” no staged file may exceed 400 lines +2. **Project Structure** โ€” regenerates `docs/project-structure.txt` +3. **Dependency Graph** โ€” regenerates `docs/dependency-graph.json` and `docs/circular-deps.txt` via madge +4. Auto-stages all generated docs files + +### Post-Merge Hook (`.husky/post-merge`) + +Runs after merge โ€” auto-installs if `package.json` changed. + +### Madge Setup + +If `docs/dependency-graph.json` is missing or stale, install madge and regenerate: +```bash +bun add -d madge +node .husky/scripts/generate-dependency-graph.js +``` --- ## File Size Guidelines -### Current File Sizes (After Modular Refactoring) +**400 lines maximum** per file (enforced by pre-commit hook). -| File | Lines | Size (bytes) | Notes | -|------|-------|--------------|-------| -| `stores/gameStore.ts` | ~300 | ~11KB | Core state + tick logic | -| `stores/manaStore.ts` | ~250 | ~9KB | Mana system | -| `stores/combatStore.ts` | ~250 | ~9KB | Combat system | -| `stores/prestigeStore.ts` | ~200 | ~8KB | Prestige system | -| `stores/skillStore.ts` | ~300 | ~11KB | Skill system | -| `stores/uiStore.ts` | ~50 | ~2KB | UI state | -| `crafting-actions/*.ts` | ~50-150 | ~1-3KB each | Modular crafting | -| `skill-evolution-modules/*.ts` | ~100-600 | ~4-22KB each | Modular skills | -| `constants/*.ts` | ~50-1000 | ~1-30KB each | Modular constants | -| `page.tsx` | ~100 | ~4KB | Main UI (heavily reduced) | -| `components/game/tabs/*.tsx` | ~50-400 | ~2-15KB each | Tab components | - -### Guidelines -- **400 lines maximum** per file (enforced by pre-commit hook) -- Extract to modules when approaching 300 lines -- Use barrel exports (`index.ts`) for clean imports -- Keep related functionality together in modules -- **Modular architecture** is now the standard - all new code should follow this pattern - -### Automated File Size Check -A pre-commit hook automatically checks all staged files. Files exceeding **400 lines** will be rejected. The hook runs via Husky and uses `.husky/scripts/check-file-size.js`. If your file is too large, refactor it into smaller modules before committing. +| Directory | Purpose | Target | +|-----------|---------|--------| +| `stores/` | Zustand store modules | < 400 lines | +| `crafting-actions/` | Crafting system actions | < 400 lines | +| `constants/` | Game constants by domain | < 400 lines | +| `data/` | Game data definitions | < 400 lines | +| `components/game/tabs/` | UI tab components | < 400 lines | --- -## ๐Ÿšซ BANNED CONTENT - NEVER ADD THESE +## ๐Ÿšซ Banned Content -### Lifesteal and Healing are BANNED -**DO NOT add lifesteal or healing mechanics to player abilities.** +### Never Add These -This includes: -- `lifesteal` spell effects -- `heal` or `regeneration` abilities for the player -- Any mechanic that restores player HP or mana based on damage dealt -- Life-stealing weapons or enchantments - -**Rationale**: The game's core design is that the player cannot take damage - only floors can. Healing/lifesteal mechanics are unnecessary and would create confusing gameplay. +- **Lifesteal / Healing mechanics** โ€” the player cannot take damage. Healing is unnecessary. +- **Scroll Crafting** โ€” violates NO INSTANT FINISHING design pillar. +- **Ascension Skills** โ€” deleted. ### Banned Mana Types -The following mana types have been **removed** and should **never be re-added**: -- `life` - Healing/lifesteal themed (banned) -- `blood` - Life + Water compound (banned due to lifesteal theme) -- `wood` - Life + Earth compound (banned due to life connection) -- `mental` - Mind/psionic themed (removed for design consistency) -- `force` - Telekinetic themed (removed for design consistency) + +Never re-add: `life`, `blood`, `wood`, `mental`, `force` ### Removed Features -- LabTab โ€” permanently removed. Do not re-add under any name. - The lab element conversion UI was replaced by attunements. + +- **LabTab** โ€” permanently removed. Do not re-add under any name. +- **Pause button** โ€” does not exist. Do not add one. --- -## ๐Ÿ”ฎ Mana Types Overview +## ๐Ÿ”ฎ Mana Types Reference -### Base Mana Types (7) -| Element | Symbol | Color | Theme | -|---------|--------|-------|-------| -| Fire | ๐Ÿ”ฅ | #FF6B35 | Destruction, burn damage | -| Water | ๐Ÿ’ง | #4ECDC4 | Flow, freeze effects | -| Air | ๐ŸŒฌ๏ธ | #00D4FF | Speed, wind damage | -| Earth | โ›ฐ๏ธ | #F4A261 | Stability, armor pierce | -| Light | โ˜€๏ธ | #FFD700 | Radiance, holy damage | -| Dark | ๐ŸŒ‘ | #9B59B6 | Shadows, void damage | -| Death | ๐Ÿ’€ | #778CA3 | Decay, rot damage | - -### Utility Mana Types (1) -| Element | Symbol | Color | Theme | -|---------|--------|-------|-------| -| Transference | ๐Ÿ”— | #1ABC9C | Mana transfer, Enchanter attunement | - -### Compound Mana Types (3) -| Element | Recipe | Theme | +### Base (7) +| Element | Symbol | Color | |---------|--------|-------| -| Metal | Fire + Earth | Armor piercing, forged weapons | -| Sand | Earth + Water | AOE damage, desert winds | -| Lightning | Fire + Air | Fast damage, armor pierce, chain effects | +| Fire | ๐Ÿ”ฅ | #FF6B35 | +| Water | ๐Ÿ’ง | #4ECDC4 | +| Air | ๐ŸŒฌ๏ธ | #00D4FF | +| Earth | โ›ฐ๏ธ | #F4A261 | +| Light | โ˜€๏ธ | #FFD700 | +| Dark | ๐ŸŒ‘ | #9B59B6 | +| Death | ๐Ÿ’€ | #778CA3 | -### Exotic Mana Types (3) -| Element | Recipe | Theme | +### Utility (1) +| Element | Symbol | Color | |---------|--------|-------| -| Crystal | Sand + Sand + Light | Prismatic, high damage | -| Stellar | Fire + Fire + Light | Cosmic, ultimate fire/light | -| Void | Dark + Dark + Death | Oblivion, ultimate dark/death | +| Transference | ๐Ÿ”— | #1ABC9C | -### Mana Type Hierarchy -``` -Base Elements (7) โ†’ Compound (3) โ†’ Exotic (3) - โ†“ - Utility (1) โ† Special attunement-based -``` +### Compound (3) โ€” Fire+Earth=Metal, Earth+Water=Sand, Fire+Air=Lightning - -# GitNexus โ€” Code Intelligence +### Exotic (3) โ€” Sand+Sand+Light=Crystal, Fire+Fire+Light=Stellar, Dark+Dark+Death=Void -This project is indexed by GitNexus as **Mana-Loop** (3795 symbols, 6409 relationships, 146 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +--- -> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. +## Adding New Things โ€” Quick Reference -## Always Do +### New Effect +1. Define in `data/enchantment-effects.ts` +2. Add stat mapping in `effects.ts` โ†’ `computeEquipmentEffects()` +3. Apply in game logic using `getUnifiedEffects(state)` -- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user. -- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows. -- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits. -- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. -- When you need full context on a specific symbol โ€” callers, callees, which execution flows it participates in โ€” use `gitnexus_context({name: "symbolName"})`. +### New Skill +1. Define in `constants/skills-v2.ts` (NOT `skill-evolution-modules/`) +2. Add to `computeStats()` effect mapping +3. UI auto-reads from structure -## Never Do +### New Spell +1. Define in `constants/spells.ts` +2. Add enchantment in `data/enchantment-effects.ts` +3. Add research skill in `constants/skills-v2.ts` +4. Map research to effect in `EFFECT_RESEARCH_MAPPING` -- NEVER edit a function, class, or method without first running `gitnexus_impact` on it. -- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis. -- NEVER rename symbols with find-and-replace โ€” use `gitnexus_rename` which understands the call graph. -- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope. +--- -## Resources +## ๐Ÿ”„ Agent Operational Protocol -| Resource | Use for | -|----------|---------| -| `gitnexus://repo/Mana-Loop/context` | Codebase overview, check index freshness | -| `gitnexus://repo/Mana-Loop/clusters` | All functional areas | -| `gitnexus://repo/Mana-Loop/processes` | All execution flows | -| `gitnexus://repo/Mana-Loop/process/{name}` | Step-by-step execution trace | +### 1. State & Resumption Protocol -## CLI +Before starting any implementation work, determine the current project state: -| Task | Read this skill file | -|------|---------------------| -| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` | -| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` | -| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` | -| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` | -| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` | -| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` | +1. Read: + - `docs/strategy/overall-remediation-plan.md` + - `docs/active-task-log.md` (if it exists) + - `docs/.workflow/TASK-*.json` (any saved progress) - +2. Identify the first roadmap item marked `[ ]` in the **Current Priority** table at the top of this file. + +3. If a Work-in-Progress entry exists for that task, **resume from the last recorded step**. Do not restart. + +4. If no Work-in-Progress entry exists, initialize using the Task Initialization Protocol. + +### 2. Task Initialization Protocol + +Before modifying code for a new task: + +1. Update `docs/active-task-log.md` +2. Record: task ID, objective, files expected to change, current status, planned tests +3. Run baseline validation **before** editing: + ```bash + bun run lint && bun run test + ``` +4. Record any failing baseline tests before implementation begins +5. Call `save_progress` after completing each significant step + +### 3. Definition of Done + +A task is only complete when ALL of the following are true: + +- [ ] Implementation is complete +- [ ] Lint passes +- [ ] All relevant tests pass (see Testing Priorities above) +- [ ] New tests added for bug fixes or new systems +- [ ] `docs/strategy/overall-remediation-plan.md` updated: `[ ]` โ†’ `[x]` +- [ ] `docs/active-task-log.md` updated (marked `ARCHIVED` or cleared) +- [ ] Changes committed and pushed: + ```bash + git add -A + git commit -m "feat: [task] โ€” [summary]" + git push origin master + ``` + +### 4. Roadmap Execution Rules + +- Work on **ONE** roadmap task at a time unless explicitly told otherwise +- Do not perform unrelated refactors +- Do not expand scope beyond current task acceptance criteria +- Large redesigns โ†’ split into `docs/tasks/` +- Every task maps back to a strategy item diff --git a/Dockerfile b/Dockerfile index 884312e..1bc529f 100755 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/add_debugname.py b/add_debugname.py deleted file mode 100644 index cd0823a..0000000 --- a/add_debugname.py +++ /dev/null @@ -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 ( ) where the jsx starts with < - # We need to find: return ( followed by newline and then < - - # Find the pattern: return ( \n \n \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}\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 - # The return ends with: ); followed by } (end of function) - # We need to find the matching closing parenthesis and then add before it - - # Let's find the last 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 before the closing ); - # We need to find the right place - it should be after the last 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}") - 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!") diff --git a/bun.lock b/bun.lock index 272fc30..42b1e81 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], } } diff --git a/db/custom.db b/db/custom.db deleted file mode 100755 index fa70c81..0000000 Binary files a/db/custom.db and /dev/null differ diff --git a/docs/.workflow/TASK-001-playwright-setup.json b/docs/.workflow/TASK-001-playwright-setup.json deleted file mode 100644 index 6fc4f11..0000000 --- a/docs/.workflow/TASK-001-playwright-setup.json +++ /dev/null @@ -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`. \ No newline at end of file diff --git a/docs/.workflow/TASK-006-left-panel-redesign.json b/docs/.workflow/TASK-006-left-panel-redesign.json deleted file mode 100644 index b6f25f1..0000000 --- a/docs/.workflow/TASK-006-left-panel-redesign.json +++ /dev/null @@ -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." -} diff --git a/docs/PLAN-SpireTab-refresh.md b/docs/PLAN-SpireTab-refresh.md deleted file mode 100644 index 5153c3e..0000000 --- a/docs/PLAN-SpireTab-refresh.md +++ /dev/null @@ -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 diff --git a/docs/SPEC-SpireTab-refresh.md b/docs/SPEC-SpireTab-refresh.md deleted file mode 100644 index 81a98f6..0000000 --- a/docs/SPEC-SpireTab-refresh.md +++ /dev/null @@ -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. diff --git a/docs/TASKS-SpireTab-refresh.md b/docs/TASKS-SpireTab-refresh.md deleted file mode 100644 index f20d894..0000000 --- a/docs/TASKS-SpireTab-refresh.md +++ /dev/null @@ -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 diff --git a/docs/active-task-log.md b/docs/active-task-log.md deleted file mode 100644 index a376103..0000000 --- a/docs/active-task-log.md +++ /dev/null @@ -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 | \ No newline at end of file diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index 3bbb8e5..af72f31 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -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 diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index 8440501..7c79b0e 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -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": [ diff --git a/docs/strategy/overall-remediation-plan.md b/docs/strategy/overall-remediation-plan.md new file mode 100644 index 0000000..fbd2001 --- /dev/null +++ b/docs/strategy/overall-remediation-plan.md @@ -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([]); +// ... + 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); +// ... + 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. diff --git a/docs/tasks/TASK-001-playwright-setup.md b/docs/tasks/TASK-001-playwright-setup.md deleted file mode 100644 index 6f036d0..0000000 --- a/docs/tasks/TASK-001-playwright-setup.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/download/README.md b/download/README.md deleted file mode 100755 index 10906f8..0000000 --- a/download/README.md +++ /dev/null @@ -1 +0,0 @@ -Here are all the generated files. \ No newline at end of file diff --git a/examples/websocket/frontend.tsx b/examples/websocket/frontend.tsx deleted file mode 100755 index dcc2aff..0000000 --- a/examples/websocket/frontend.tsx +++ /dev/null @@ -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([]); - const [inputMessage, setInputMessage] = useState(''); - const [username, setUsername] = useState(''); - const [isUsernameSet, setIsUsernameSet] = useState(false); - const [socket, setSocket] = useState(null); - const [isConnected, setIsConnected] = useState(false); - const [users, setUsers] = useState([]); - - 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 ( -
- - - - WebSocket Demo - - {isConnected ? 'Connected' : 'Disconnected'} - - - - - {!isUsernameSet ? ( -
- setUsername(e.target.value)} - onKeyPress={(e) => { - if (e.key === 'Enter') { - handleJoin(); - } - }} - placeholder="Enter your username..." - disabled={!isConnected} - className="flex-1" - /> - -
- ) : ( - <> - -
- {messages.length === 0 ? ( -

No messages yet

- ) : ( - messages.map((msg) => ( -
-
-
-

- {msg.username} -

-

- {msg.content} -

-
- - {new Date(msg.timestamp).toLocaleTimeString()} - -
-
- )) - )} -
-
- -
- setInputMessage(e.target.value)} - onKeyPress={handleKeyPress} - placeholder="Type a message..." - disabled={!isConnected} - className="flex-1" - /> - -
- - )} -
-
-
- ); -} diff --git a/examples/websocket/server.ts b/examples/websocket/server.ts deleted file mode 100755 index 4db70df..0000000 --- a/examples/websocket/server.ts +++ /dev/null @@ -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() - -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) - }) -}) \ No newline at end of file diff --git a/fix_remaining.py b/fix_remaining.py deleted file mode 100644 index b49731f..0000000 --- a/fix_remaining.py +++ /dev/null @@ -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 - # 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 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}\n" - modified = before_return + opening_tag + after_return - - # Now find the closing ); for the return statement and add 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 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}" - 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!") diff --git a/fix_tabs.py b/fix_tabs.py deleted file mode 100644 index 3be9e24..0000000 --- a/fix_tabs.py +++ /dev/null @@ -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 after the return ( - modified = before_return + f'{indent}\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 before ); - lines_before.insert(close_paren_line, f"{close_paren_indent}") - 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!") diff --git a/fix_tabs2.py b/fix_tabs2.py deleted file mode 100644 index eb3496b..0000000 --- a/fix_tabs2.py +++ /dev/null @@ -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 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 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 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 " - - # 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 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}" - 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!") diff --git a/mini-services/.gitkeep b/mini-services/.gitkeep deleted file mode 100755 index e69de29..0000000 diff --git a/package.json b/package.json index 012fc15..e3d7585 100755 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/playwright-report/data/1513ea5b9ea5985996f67ca36f2bc4d34add51f1.webm b/playwright-report/data/1513ea5b9ea5985996f67ca36f2bc4d34add51f1.webm new file mode 100644 index 0000000..725bb3c Binary files /dev/null and b/playwright-report/data/1513ea5b9ea5985996f67ca36f2bc4d34add51f1.webm differ diff --git a/playwright-report/data/23eb0c541b68af33d962c3ac20ba74eb9ba477b3.md b/playwright-report/data/23eb0c541b68af33d962c3ac20ba74eb9ba477b3.md new file mode 100644 index 0000000..51893bc --- /dev/null +++ b/playwright-report/data/23eb0c541b68af33d962c3ac20ba74eb9ba477b3.md @@ -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 | }); +``` \ No newline at end of file diff --git a/playwright-report/data/25af666b2659e25b596f1eb58ca5629f38f0fa74.png b/playwright-report/data/25af666b2659e25b596f1eb58ca5629f38f0fa74.png new file mode 100644 index 0000000..c743d2e Binary files /dev/null and b/playwright-report/data/25af666b2659e25b596f1eb58ca5629f38f0fa74.png differ diff --git a/playwright-report/data/294ed85dfd5fbd79486f5274129a1d8b83cfa676.png b/playwright-report/data/294ed85dfd5fbd79486f5274129a1d8b83cfa676.png new file mode 100644 index 0000000..9e289d4 Binary files /dev/null and b/playwright-report/data/294ed85dfd5fbd79486f5274129a1d8b83cfa676.png differ diff --git a/playwright-report/data/37c584c77b029af648d58a063f9724538662c6d0.webm b/playwright-report/data/37c584c77b029af648d58a063f9724538662c6d0.webm new file mode 100644 index 0000000..456fe1c Binary files /dev/null and b/playwright-report/data/37c584c77b029af648d58a063f9724538662c6d0.webm differ diff --git a/playwright-report/data/4d1229974e5326e2351c32921095bff6e989005e.png b/playwright-report/data/4d1229974e5326e2351c32921095bff6e989005e.png new file mode 100644 index 0000000..5bc4454 Binary files /dev/null and b/playwright-report/data/4d1229974e5326e2351c32921095bff6e989005e.png differ diff --git a/playwright-report/data/4f22caa1a2b454f813b4c68c510a2ef0b340a248.md b/playwright-report/data/4f22caa1a2b454f813b4c68c510a2ef0b340a248.md new file mode 100644 index 0000000..d252514 --- /dev/null +++ b/playwright-report/data/4f22caa1a2b454f813b4c68c510a2ef0b340a248.md @@ -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 | }); +``` \ No newline at end of file diff --git a/playwright-report/data/6408809a17a0a92b06e5cc75fcee95e9778138c4.md b/playwright-report/data/6408809a17a0a92b06e5cc75fcee95e9778138c4.md new file mode 100644 index 0000000..f08917f --- /dev/null +++ b/playwright-report/data/6408809a17a0a92b06e5cc75fcee95e9778138c4.md @@ -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) aka getByRole('button', { name: 'Elemental Mana (1)' }) + 3) 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 | }); +``` \ No newline at end of file diff --git a/playwright-report/data/66a1f85e1e6a655dfb90f10bd1a60887cffa87da.md b/playwright-report/data/66a1f85e1e6a655dfb90f10bd1a60887cffa87da.md new file mode 100644 index 0000000..500ff88 --- /dev/null +++ b/playwright-report/data/66a1f85e1e6a655dfb90f10bd1a60887cffa87da.md @@ -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 | }); +``` \ No newline at end of file diff --git a/playwright-report/data/6b97a6c84cfda4c717249f240d0a80e1b195498a.png b/playwright-report/data/6b97a6c84cfda4c717249f240d0a80e1b195498a.png new file mode 100644 index 0000000..dbbd62a Binary files /dev/null and b/playwright-report/data/6b97a6c84cfda4c717249f240d0a80e1b195498a.png differ diff --git a/playwright-report/data/6c1c7d873c0c5262ffca286974649ec3bf1eb3f4.md b/playwright-report/data/6c1c7d873c0c5262ffca286974649ec3bf1eb3f4.md new file mode 100644 index 0000000..3eaf7a4 --- /dev/null +++ b/playwright-report/data/6c1c7d873c0c5262ffca286974649ec3bf1eb3f4.md @@ -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) aka getByRole('button', { name: 'Elemental Mana (1)' }) + 3) 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 | }); +``` \ No newline at end of file diff --git a/playwright-report/data/72280c2048aa77a6b58afc7bba8f9db3dfd1c68b.webm b/playwright-report/data/72280c2048aa77a6b58afc7bba8f9db3dfd1c68b.webm new file mode 100644 index 0000000..a066bf2 Binary files /dev/null and b/playwright-report/data/72280c2048aa77a6b58afc7bba8f9db3dfd1c68b.webm differ diff --git a/playwright-report/data/8035d8abad1bfb2166374e25b55f52324fef1275.png b/playwright-report/data/8035d8abad1bfb2166374e25b55f52324fef1275.png new file mode 100644 index 0000000..9359a40 Binary files /dev/null and b/playwright-report/data/8035d8abad1bfb2166374e25b55f52324fef1275.png differ diff --git a/playwright-report/data/8396039272c615989307eaf4113a77b0d77cfbdd.webm b/playwright-report/data/8396039272c615989307eaf4113a77b0d77cfbdd.webm new file mode 100644 index 0000000..c43e317 Binary files /dev/null and b/playwright-report/data/8396039272c615989307eaf4113a77b0d77cfbdd.webm differ diff --git a/playwright-report/data/a69b7491fd34ee0580bc0153a90dc146b509aac3.md b/playwright-report/data/a69b7491fd34ee0580bc0153a90dc146b509aac3.md new file mode 100644 index 0000000..bb73e33 --- /dev/null +++ b/playwright-report/data/a69b7491fd34ee0580bc0153a90dc146b509aac3.md @@ -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 | }); +``` \ No newline at end of file diff --git a/playwright-report/data/bb3c9d51cafcb654c796b093c72c5b702f52faed.webm b/playwright-report/data/bb3c9d51cafcb654c796b093c72c5b702f52faed.webm new file mode 100644 index 0000000..53b4a5d Binary files /dev/null and b/playwright-report/data/bb3c9d51cafcb654c796b093c72c5b702f52faed.webm differ diff --git a/playwright-report/data/bee318a3f485bd3e98088a4735e02181585e431b.png b/playwright-report/data/bee318a3f485bd3e98088a4735e02181585e431b.png new file mode 100644 index 0000000..4be1783 Binary files /dev/null and b/playwright-report/data/bee318a3f485bd3e98088a4735e02181585e431b.png differ diff --git a/playwright-report/data/c0f44af041cac0f5d5efaec8a9a9e5d165c8d26a.png b/playwright-report/data/c0f44af041cac0f5d5efaec8a9a9e5d165c8d26a.png new file mode 100644 index 0000000..8745af0 Binary files /dev/null and b/playwright-report/data/c0f44af041cac0f5d5efaec8a9a9e5d165c8d26a.png differ diff --git a/playwright-report/data/cf49b56fde3bacf27d842ef4bfeed4887d97f01e.webm b/playwright-report/data/cf49b56fde3bacf27d842ef4bfeed4887d97f01e.webm new file mode 100644 index 0000000..0884b64 Binary files /dev/null and b/playwright-report/data/cf49b56fde3bacf27d842ef4bfeed4887d97f01e.webm differ diff --git a/playwright-report/data/dbea283cbcf6aaed195161609c68ab7de0c6adfa.png b/playwright-report/data/dbea283cbcf6aaed195161609c68ab7de0c6adfa.png new file mode 100644 index 0000000..e626b41 Binary files /dev/null and b/playwright-report/data/dbea283cbcf6aaed195161609c68ab7de0c6adfa.png differ diff --git a/playwright-report/data/dc2d9fe97c08dd61f42a27ead0829c2d74322ccc.webm b/playwright-report/data/dc2d9fe97c08dd61f42a27ead0829c2d74322ccc.webm new file mode 100644 index 0000000..7c8f37e Binary files /dev/null and b/playwright-report/data/dc2d9fe97c08dd61f42a27ead0829c2d74322ccc.webm differ diff --git a/playwright-report/data/e3d1abb209771785e7247c38fd372d8fd61b7ea4.md b/playwright-report/data/e3d1abb209771785e7247c38fd372d8fd61b7ea4.md new file mode 100644 index 0000000..ec9b2c4 --- /dev/null +++ b/playwright-report/data/e3d1abb209771785e7247c38fd372d8fd61b7ea4.md @@ -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) aka getByRole('button', { name: 'Elemental Mana (1)' }) + 3) 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 | }); +``` \ No newline at end of file diff --git a/playwright-report/data/e59720b989841926cc856d6a00be0a6f8365cf49.webm b/playwright-report/data/e59720b989841926cc856d6a00be0a6f8365cf49.webm new file mode 100644 index 0000000..2e72a6a Binary files /dev/null and b/playwright-report/data/e59720b989841926cc856d6a00be0a6f8365cf49.webm differ diff --git a/playwright-report/data/f5ba77f8b20c452bd2c31718b44897276882a465.md b/playwright-report/data/f5ba77f8b20c452bd2c31718b44897276882a465.md new file mode 100644 index 0000000..0d20546 --- /dev/null +++ b/playwright-report/data/f5ba77f8b20c452bd2c31718b44897276882a465.md @@ -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) aka getByRole('button', { name: 'Elemental Mana (1)' }) + 3) 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 | }); +``` \ No newline at end of file diff --git a/playwright-report/index.html b/playwright-report/index.html new file mode 100644 index 0000000..71ca158 --- /dev/null +++ b/playwright-report/index.html @@ -0,0 +1,90 @@ + + + + + + + + + Playwright Test Report + + + + +
+ + + \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma deleted file mode 100755 index f72ac32..0000000 --- a/prisma/schema.prisma +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/src/app/api/route.ts b/src/app/api/route.ts deleted file mode 100755 index 0277710..0000000 --- a/src/app/api/route.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { NextResponse } from "next/server"; - -export async function GET() { - return NextResponse.json({ message: "Hello, world!" }); -} \ No newline at end of file diff --git a/src/lib/db.ts b/src/lib/db.ts deleted file mode 100755 index 7dfb80c..0000000 --- a/src/lib/db.ts +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/lib/game/__tests__/skills-tests/ascension-skills.test.ts b/src/lib/game/__tests__/skills-tests/ascension-skills.test.ts index f85cc3e..3c74975 100644 --- a/src/lib/game/__tests__/skills-tests/ascension-skills.test.ts +++ b/src/lib/game/__tests__/skills-tests/ascension-skills.test.ts @@ -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 { diff --git a/src/lib/game/__tests__/skills-tests/integration-and-evolution.test.ts b/src/lib/game/__tests__/skills-tests/integration-and-evolution.test.ts index d521c38..e63fda6 100644 --- a/src/lib/game/__tests__/skills-tests/integration-and-evolution.test.ts +++ b/src/lib/game/__tests__/skills-tests/integration-and-evolution.test.ts @@ -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', () => { diff --git a/src/lib/game/__tests__/skills-tests/mana-skills.test.ts b/src/lib/game/__tests__/skills-tests/mana-skills.test.ts index dc67b90..3db6e32 100644 --- a/src/lib/game/__tests__/skills-tests/mana-skills.test.ts +++ b/src/lib/game/__tests__/skills-tests/mana-skills.test.ts @@ -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 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/src/lib/game/__tests__/skills-tests/prestige-upgrades.test.ts b/src/lib/game/__tests__/skills-tests/prestige-upgrades.test.ts index fa916ca..c717f2e 100644 --- a/src/lib/game/__tests__/skills-tests/prestige-upgrades.test.ts +++ b/src/lib/game/__tests__/skills-tests/prestige-upgrades.test.ts @@ -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 { diff --git a/src/lib/game/__tests__/skills-tests/skill-prerequisites.test.ts b/src/lib/game/__tests__/skills-tests/skill-prerequisites.test.ts index ed33e33..01cb23a 100644 --- a/src/lib/game/__tests__/skills-tests/skill-prerequisites.test.ts +++ b/src/lib/game/__tests__/skills-tests/skill-prerequisites.test.ts @@ -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', () => { diff --git a/src/lib/game/__tests__/skills-tests/specialized-skills.test.ts b/src/lib/game/__tests__/skills-tests/specialized-skills.test.ts index 18dc320..8f1c7b0 100644 --- a/src/lib/game/__tests__/skills-tests/specialized-skills.test.ts +++ b/src/lib/game/__tests__/skills-tests/specialized-skills.test.ts @@ -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)', () => { diff --git a/src/lib/game/__tests__/skills-tests/study-skills.test.ts b/src/lib/game/__tests__/skills-tests/study-skills.test.ts index 6afdb3c..c29db67 100644 --- a/src/lib/game/__tests__/skills-tests/study-skills.test.ts +++ b/src/lib/game/__tests__/skills-tests/study-skills.test.ts @@ -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)', () => { diff --git a/src/lib/game/__tests__/skills-tests/study-times.test.ts b/src/lib/game/__tests__/skills-tests/study-times.test.ts index 1644178..dbdc381 100644 --- a/src/lib/game/__tests__/skills-tests/study-times.test.ts +++ b/src/lib/game/__tests__/skills-tests/study-times.test.ts @@ -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', () => { diff --git a/src/lib/game/skills-split-tests/ascension-specialized-skills.test.ts b/src/lib/game/skills-split-tests/ascension-specialized-skills.test.ts index b643721..0c31edc 100644 --- a/src/lib/game/skills-split-tests/ascension-specialized-skills.test.ts +++ b/src/lib/game/skills-split-tests/ascension-specialized-skills.test.ts @@ -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 { diff --git a/src/lib/game/skills-split-tests/mana-skills.test.ts b/src/lib/game/skills-split-tests/mana-skills.test.ts index c21a773..432889f 100644 --- a/src/lib/game/skills-split-tests/mana-skills.test.ts +++ b/src/lib/game/skills-split-tests/mana-skills.test.ts @@ -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 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/src/lib/game/skills-split-tests/prerequisites-studytimes-prestige-integration.test.ts b/src/lib/game/skills-split-tests/prerequisites-studytimes-prestige-integration.test.ts index 46b847f..1f810ed 100644 --- a/src/lib/game/skills-split-tests/prerequisites-studytimes-prestige-integration.test.ts +++ b/src/lib/game/skills-split-tests/prerequisites-studytimes-prestige-integration.test.ts @@ -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 { diff --git a/src/lib/game/skills-split-tests/study-skills.test.ts b/src/lib/game/skills-split-tests/study-skills.test.ts index 5fec1c9..e3e8e90 100644 --- a/src/lib/game/skills-split-tests/study-skills.test.ts +++ b/src/lib/game/skills-split-tests/study-skills.test.ts @@ -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)', () => { diff --git a/src/lib/game/stores-split-tests/combat-store.test.ts b/src/lib/game/stores-split-tests/combat-store.test.ts index fe02450..8789d4d 100644 --- a/src/lib/game/stores-split-tests/combat-store.test.ts +++ b/src/lib/game/stores-split-tests/combat-store.test.ts @@ -3,7 +3,7 @@ */ import { describe, it, expect, beforeEach } from 'vitest'; -import { useCombatStore } from './stores'; +import { useCombatStore } from '@/lib/game/stores'; // โ”€โ”€โ”€ Test Fixtures โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/src/lib/game/stores-split-tests/integration.test.ts b/src/lib/game/stores-split-tests/integration.test.ts index bb11373..438130e 100644 --- a/src/lib/game/stores-split-tests/integration.test.ts +++ b/src/lib/game/stores-split-tests/integration.test.ts @@ -9,7 +9,7 @@ import { usePrestigeStore, useCombatStore, computeMaxMana, -} from './stores'; +} from '@/lib/game/stores'; // โ”€โ”€โ”€ Test Fixtures โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/src/lib/game/stores-split-tests/mana-store.test.ts b/src/lib/game/stores-split-tests/mana-store.test.ts index 7be4881..059622c 100644 --- a/src/lib/game/stores-split-tests/mana-store.test.ts +++ b/src/lib/game/stores-split-tests/mana-store.test.ts @@ -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 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/src/lib/game/stores-split-tests/prestige-store.test.ts b/src/lib/game/stores-split-tests/prestige-store.test.ts index 6eaa768..a9999c3 100644 --- a/src/lib/game/stores-split-tests/prestige-store.test.ts +++ b/src/lib/game/stores-split-tests/prestige-store.test.ts @@ -6,7 +6,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { usePrestigeStore, useManaStore, -} from './stores'; +} from '@/lib/game/stores'; // โ”€โ”€โ”€ Test Fixtures โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/src/lib/game/stores-split-tests/skill-store.test.ts b/src/lib/game/stores-split-tests/skill-store.test.ts index 1de346c..1f77bf1 100644 --- a/src/lib/game/stores-split-tests/skill-store.test.ts +++ b/src/lib/game/stores-split-tests/skill-store.test.ts @@ -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 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/src/lib/game/stores-split-tests/ui-store.test.ts b/src/lib/game/stores-split-tests/ui-store.test.ts index 8fb3f20..55afdba 100644 --- a/src/lib/game/stores-split-tests/ui-store.test.ts +++ b/src/lib/game/stores-split-tests/ui-store.test.ts @@ -3,7 +3,7 @@ */ import { describe, it, expect, beforeEach } from 'vitest'; -import { useUIStore } from './stores'; +import { useUIStore } from '@/lib/game/stores'; // โ”€โ”€โ”€ Test Fixtures โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/src/lib/game/stores/__tests__/computed-stats.test.ts b/src/lib/game/stores/__tests__/computed-stats.test.ts new file mode 100644 index 0000000..9fa8db4 --- /dev/null +++ b/src/lib/game/stores/__tests__/computed-stats.test.ts @@ -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 = {}; + 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.'); \ No newline at end of file diff --git a/src/lib/game/stores/__tests__/index-tests/combat-calculations.test.ts b/src/lib/game/stores/__tests__/index-tests/combat-calculations.test.ts index ab8823f..f019979 100644 --- a/src/lib/game/stores/__tests__/index-tests/combat-calculations.test.ts +++ b/src/lib/game/stores/__tests__/index-tests/combat-calculations.test.ts @@ -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 { @@ -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); }); diff --git a/src/lib/game/stores/__tests__/index-tests/definitions.test.ts b/src/lib/game/stores/__tests__/index-tests/definitions.test.ts index 84c8e40..02588e5 100644 --- a/src/lib/game/stores/__tests__/index-tests/definitions.test.ts +++ b/src/lib/game/stores/__tests__/index-tests/definitions.test.ts @@ -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', () => { diff --git a/src/lib/game/stores/__tests__/index-tests/mana-calculations.test.ts b/src/lib/game/stores/__tests__/index-tests/mana-calculations.test.ts index d4c9952..48d8c5c 100644 --- a/src/lib/game/stores/__tests__/index-tests/mana-calculations.test.ts +++ b/src/lib/game/stores/__tests__/index-tests/mana-calculations.test.ts @@ -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 { const elements: Record = {}; diff --git a/src/lib/game/stores/__tests__/index-tests/meditation-insight-incursion.test.ts b/src/lib/game/stores/__tests__/index-tests/meditation-insight-incursion.test.ts index c7a454a..29049c4 100644 --- a/src/lib/game/stores/__tests__/index-tests/meditation-insight-incursion.test.ts +++ b/src/lib/game/stores/__tests__/index-tests/meditation-insight-incursion.test.ts @@ -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 { diff --git a/src/lib/game/stores/__tests__/index-tests/spell-cost.test.ts b/src/lib/game/stores/__tests__/index-tests/spell-cost.test.ts index 5792f61..bacafa6 100644 --- a/src/lib/game/stores/__tests__/index-tests/spell-cost.test.ts +++ b/src/lib/game/stores/__tests__/index-tests/spell-cost.test.ts @@ -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', () => { diff --git a/src/lib/game/stores/__tests__/index-tests/study-speed.test.ts b/src/lib/game/stores/__tests__/index-tests/study-speed.test.ts index c1db539..dafe49f 100644 --- a/src/lib/game/stores/__tests__/index-tests/study-speed.test.ts +++ b/src/lib/game/stores/__tests__/index-tests/study-speed.test.ts @@ -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', () => { diff --git a/src/lib/game/stores/__tests__/index-tests/utility-functions.test.ts b/src/lib/game/stores/__tests__/index-tests/utility-functions.test.ts index 4355bee..0449fa3 100644 --- a/src/lib/game/stores/__tests__/index-tests/utility-functions.test.ts +++ b/src/lib/game/stores/__tests__/index-tests/utility-functions.test.ts @@ -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', () => { diff --git a/src/lib/game/stores/__tests__/store-method-tests/combat-store.test.ts b/src/lib/game/stores/__tests__/store-method-tests/combat-store.test.ts index 51d6542..8aaa082 100644 --- a/src/lib/game/stores/__tests__/store-method-tests/combat-store.test.ts +++ b/src/lib/game/stores/__tests__/store-method-tests/combat-store.test.ts @@ -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(() => { diff --git a/src/lib/game/stores/__tests__/store-method-tests/mana-store.test.ts b/src/lib/game/stores/__tests__/store-method-tests/mana-store.test.ts index e1c2404..690abbb 100644 --- a/src/lib/game/stores/__tests__/store-method-tests/mana-store.test.ts +++ b/src/lib/game/stores/__tests__/store-method-tests/mana-store.test.ts @@ -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(() => { diff --git a/src/lib/game/stores/__tests__/store-method-tests/prestige-store.test.ts b/src/lib/game/stores/__tests__/store-method-tests/prestige-store.test.ts index edb2cb8..28ac2c2 100644 --- a/src/lib/game/stores/__tests__/store-method-tests/prestige-store.test.ts +++ b/src/lib/game/stores/__tests__/store-method-tests/prestige-store.test.ts @@ -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 diff --git a/src/lib/game/stores/__tests__/store-method-tests/skill-store.test.ts b/src/lib/game/stores/__tests__/store-method-tests/skill-store.test.ts index aa3c782..593239f 100644 --- a/src/lib/game/stores/__tests__/store-method-tests/skill-store.test.ts +++ b/src/lib/game/stores/__tests__/store-method-tests/skill-store.test.ts @@ -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(() => { diff --git a/src/lib/game/stores/__tests__/store-method-tests/ui-store.test.ts b/src/lib/game/stores/__tests__/store-method-tests/ui-store.test.ts index d622557..d8789d8 100644 --- a/src/lib/game/stores/__tests__/store-method-tests/ui-store.test.ts +++ b/src/lib/game/stores/__tests__/store-method-tests/ui-store.test.ts @@ -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(() => { diff --git a/src/lib/game/stores/__tests__/stores-tests/damage-calculation.test.ts b/src/lib/game/stores/__tests__/stores-tests/damage-calculation.test.ts index fd39d17..98da5e0 100644 --- a/src/lib/game/stores/__tests__/stores-tests/damage-calculation.test.ts +++ b/src/lib/game/stores/__tests__/stores-tests/damage-calculation.test.ts @@ -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 { const elements: Record = {}; diff --git a/src/lib/game/stores/__tests__/stores-tests/floor.test.ts b/src/lib/game/stores/__tests__/stores-tests/floor.test.ts index 3a57d3a..3ab8312 100644 --- a/src/lib/game/stores/__tests__/stores-tests/floor.test.ts +++ b/src/lib/game/stores/__tests__/stores-tests/floor.test.ts @@ -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', () => { diff --git a/src/lib/game/stores/__tests__/stores-tests/formatting.test.ts b/src/lib/game/stores/__tests__/stores-tests/formatting.test.ts index 092d7f9..579edf2 100644 --- a/src/lib/game/stores/__tests__/stores-tests/formatting.test.ts +++ b/src/lib/game/stores/__tests__/stores-tests/formatting.test.ts @@ -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)', () => { diff --git a/src/lib/game/stores/__tests__/stores-tests/guardians.test.ts b/src/lib/game/stores/__tests__/stores-tests/guardians.test.ts index a8f6a81..8a5c546 100644 --- a/src/lib/game/stores/__tests__/stores-tests/guardians.test.ts +++ b/src/lib/game/stores/__tests__/stores-tests/guardians.test.ts @@ -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', () => { diff --git a/src/lib/game/stores/__tests__/stores-tests/incursion.test.ts b/src/lib/game/stores/__tests__/stores-tests/incursion.test.ts index d52c80e..b91536f 100644 --- a/src/lib/game/stores/__tests__/stores-tests/incursion.test.ts +++ b/src/lib/game/stores/__tests__/stores-tests/incursion.test.ts @@ -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', () => { diff --git a/src/lib/game/stores/__tests__/stores-tests/insight-calculation.test.ts b/src/lib/game/stores/__tests__/stores-tests/insight-calculation.test.ts index 051a664..edf7b04 100644 --- a/src/lib/game/stores/__tests__/stores-tests/insight-calculation.test.ts +++ b/src/lib/game/stores/__tests__/stores-tests/insight-calculation.test.ts @@ -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 { const elements: Record = {}; diff --git a/src/lib/game/stores/__tests__/stores-tests/mana-calculation.test.ts b/src/lib/game/stores/__tests__/stores-tests/mana-calculation.test.ts index 3ec5e6c..30fce92 100644 --- a/src/lib/game/stores/__tests__/stores-tests/mana-calculation.test.ts +++ b/src/lib/game/stores/__tests__/stores-tests/mana-calculation.test.ts @@ -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 { const elements: Record = {}; @@ -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); + }); }); }); diff --git a/src/lib/game/stores/__tests__/stores-tests/meditation.test.ts b/src/lib/game/stores/__tests__/stores-tests/meditation.test.ts index 60e0946..fff1284 100644 --- a/src/lib/game/stores/__tests__/stores-tests/meditation.test.ts +++ b/src/lib/game/stores/__tests__/stores-tests/meditation.test.ts @@ -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', () => { diff --git a/src/lib/game/stores/__tests__/stores-tests/prestige-upgrades.test.ts b/src/lib/game/stores/__tests__/stores-tests/prestige-upgrades.test.ts index 5f30d17..99799ce 100644 --- a/src/lib/game/stores/__tests__/stores-tests/prestige-upgrades.test.ts +++ b/src/lib/game/stores/__tests__/stores-tests/prestige-upgrades.test.ts @@ -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', () => { diff --git a/src/lib/game/stores/__tests__/stores-tests/skill-definitions.test.ts b/src/lib/game/stores/__tests__/stores-tests/skill-definitions.test.ts index c098beb..7499516 100644 --- a/src/lib/game/stores/__tests__/stores-tests/skill-definitions.test.ts +++ b/src/lib/game/stores/__tests__/stores-tests/skill-definitions.test.ts @@ -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', () => { diff --git a/src/lib/game/stores/__tests__/stores-tests/spell-cost.test.ts b/src/lib/game/stores/__tests__/stores-tests/spell-cost.test.ts index 28c5e55..8b8bb3f 100644 --- a/src/lib/game/stores/__tests__/stores-tests/spell-cost.test.ts +++ b/src/lib/game/stores/__tests__/stores-tests/spell-cost.test.ts @@ -3,8 +3,8 @@ */ import { describe, it, expect } from 'vitest'; -import { canAffordSpellCost } from '../../utils'; -import { rawCost, elemCost } from '../../constants'; +import { canAffordSpellCost } from '@/lib/game/utils'; +import { rawCost, elemCost } from '@/lib/game/constants'; describe('Spell Cost System', () => { describe('rawCost', () => { diff --git a/src/lib/game/stores/__tests__/stores-tests/spell-definitions.test.ts b/src/lib/game/stores/__tests__/stores-tests/spell-definitions.test.ts index 29bddae..7092150 100644 --- a/src/lib/game/stores/__tests__/stores-tests/spell-definitions.test.ts +++ b/src/lib/game/stores/__tests__/stores-tests/spell-definitions.test.ts @@ -3,7 +3,7 @@ */ import { describe, it, expect } from 'vitest'; -import { SPELLS_DEF } from '../../constants'; +import { SPELLS_DEF } from '@/lib/game/constants'; describe('Spell Definitions', () => { it('should have manaBolt as a basic spell', () => { diff --git a/src/lib/game/stores/__tests__/stores-tests/study-speed.test.ts b/src/lib/game/stores/__tests__/stores-tests/study-speed.test.ts index 1a45a32..86c3bea 100644 --- a/src/lib/game/stores/__tests__/stores-tests/study-speed.test.ts +++ b/src/lib/game/stores/__tests__/stores-tests/study-speed.test.ts @@ -3,7 +3,7 @@ */ import { describe, it, expect } from 'vitest'; -import { getStudySpeedMultiplier, getStudyCostMultiplier } from '../../utils'; +import { getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants'; describe('Study Speed Functions', () => { describe('getStudySpeedMultiplier', () => { @@ -15,6 +15,10 @@ describe('Study Speed Functions', () => { expect(getStudySpeedMultiplier({ quickLearner: 1 })).toBe(1.1); expect(getStudySpeedMultiplier({ quickLearner: 5 })).toBe(1.5); }); + + it('should increase by 10% per level up to max', () => { + expect(getStudySpeedMultiplier({ quickLearner: 10 })).toBe(2.0); + }); }); describe('getStudyCostMultiplier', () => { @@ -26,6 +30,10 @@ describe('Study Speed Functions', () => { expect(getStudyCostMultiplier({ focusedMind: 1 })).toBe(0.95); expect(getStudyCostMultiplier({ focusedMind: 5 })).toBe(0.75); }); + + it('should decrease by 5% per level up to max', () => { + expect(getStudyCostMultiplier({ focusedMind: 10 })).toBe(0.5); + }); }); }); diff --git a/src/lib/game/stores/craftingStore.ts b/src/lib/game/stores/craftingStore.ts index aec0ae4..4e9c02d 100644 --- a/src/lib/game/stores/craftingStore.ts +++ b/src/lib/game/stores/craftingStore.ts @@ -47,6 +47,15 @@ export interface CraftingState { materials: Record; blueprints: string[]; }; + + // Enchantment selection state (single source of truth for enchanting UI) + enchantmentSelection: { + selectedEquipmentType: string | null; + selectedEffects: DesignEffect[]; + designName: string; + selectedDesign: string | null; + selectedEquipmentInstance: string | null; + }; } export interface CraftingActions { @@ -86,6 +95,14 @@ export interface CraftingActions { // Equipment crafting actions startCraftingEquipment: (blueprintId: string) => boolean; cancelEquipmentCrafting: () => void; + + // Enchantment selection actions (store as source of truth) + setSelectedEquipmentType: (type: string | null) => void; + setSelectedEffects: (effects: DesignEffect[]) => void; + setDesignName: (name: string) => void; + setSelectedDesign: (id: string | null) => void; + setSelectedEquipmentInstance: (id: string | null) => void; + resetEnchantmentSelection: () => void; } export type CraftingStore = CraftingState & CraftingActions; @@ -108,6 +125,13 @@ export const useCraftingStore = create()( materials: {}, blueprints: [], }, + enchantmentSelection: { + selectedEquipmentType: null, + selectedEffects: [], + designName: '', + selectedDesign: null, + selectedEquipmentInstance: null, + }, // Actions setDesignProgress: (progress) => set({ designProgress: progress }), @@ -311,6 +335,34 @@ export const useCraftingStore = create()( useUIStore.getState().addLog(cancelResult.logMessage); }, + // Enchantment selection actions + setSelectedEquipmentType: (type) => { + set((s) => ({ enchantmentSelection: { ...s.enchantmentSelection, selectedEquipmentType: type }})); + }, + setSelectedEffects: (effects) => { + set((s) => ({ enchantmentSelection: { ...s.enchantmentSelection, selectedEffects: effects } })); + }, + setDesignName: (name) => { + set((s) => ({ enchantmentSelection: { ...s.enchantmentSelection, designName: name } })); + }, + setSelectedDesign: (id) => { + set((s) => ({ enchantmentSelection: { ...s.enchantmentSelection, selectedDesign: id } })); + }, + setSelectedEquipmentInstance: (id) => { + set((s) => ({ enchantmentSelection: { ...s.enchantmentSelection, selectedEquipmentInstance: id } })); + }, + resetEnchantmentSelection: () => { + set((s) => ({ + enchantmentSelection: { + selectedEquipmentType: null, + selectedEffects: [], + designName: '', + selectedDesign: null, + selectedEquipmentInstance: null, + }, + })); + }, + // Loot inventory actions deleteMaterial: (materialId: string, amount: number) => { set((state) => { @@ -366,6 +418,7 @@ export const useCraftingStore = create()( equipmentInstances: state.equipmentInstances, equippedInstances: state.equippedInstances, lootInventory: state.lootInventory, + enchantmentSelection: state.enchantmentSelection, }), } ) diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..cbcc1fb --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "passed", + "failedTests": [] +} \ No newline at end of file diff --git a/tsconfig-check.json b/tsconfig-check.json deleted file mode 100644 index 74cd89c..0000000 --- a/tsconfig-check.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "./tsconfig.json", - "include": ["src/app/components/LeftPanel.tsx"], - "exclude": ["node_modules", "src/components"] -} diff --git a/tsconfig-leftpanel.json b/tsconfig-leftpanel.json deleted file mode 100644 index 74cd89c..0000000 --- a/tsconfig-leftpanel.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "./tsconfig.json", - "include": ["src/app/components/LeftPanel.tsx"], - "exclude": ["node_modules", "src/components"] -} diff --git a/tsconfig-lp.json b/tsconfig-lp.json deleted file mode 100644 index 74cd89c..0000000 --- a/tsconfig-lp.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "./tsconfig.json", - "include": ["src/app/components/LeftPanel.tsx"], - "exclude": ["node_modules", "src/components"] -}