diff --git a/.zscripts/dev.out.log b/.zscripts/dev.out.log index 238332b..53bf133 100755 --- a/.zscripts/dev.out.log +++ b/.zscripts/dev.out.log @@ -1205,3 +1205,23 @@ Checked 846 installs across 915 packages (no changes) [45.00ms] $ prisma db push Failed to load config file "/home/z/my-project" as a TypeScript/JavaScript module. Error: Error: ENOTDIR: not a directory, lstat '/home/z/my-project/.config/prisma' error: script "db:push" exited with code 1 +========================================== +[2026-03-25 07:22:32] Starting: bun install +========================================== +[BUN] Installing dependencies... +[0.10ms] ".env" +bun install v1.3.10 (30e609e0) + +Checked 846 installs across 915 packages (no changes) [60.00ms] +========================================== +[2026-03-25 07:22:33] Completed: bun install +[LOG] Step: bun install | Duration: 1s +========================================== + +========================================== +[2026-03-25 07:22:33] Starting: bun run db:push +========================================== +[BUN] Setting up database... +$ prisma db push +Failed to load config file "/home/z/my-project" as a TypeScript/JavaScript module. Error: Error: ENOTDIR: not a directory, lstat '/home/z/my-project/.config/prisma' +error: script "db:push" exited with code 1 diff --git a/.zscripts/dev.pid b/.zscripts/dev.pid index a16667d..e5a0177 100755 --- a/.zscripts/dev.pid +++ b/.zscripts/dev.pid @@ -1 +1 @@ -548 +488 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4d84d35 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,217 @@ +# Mana Loop - Project Architecture Guide + +This document provides a comprehensive overview of the project architecture for AI agents working on this codebase. + +## Project Overview + +**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 +- **Database**: Prisma ORM with SQLite (for persistence features) + +## 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 (single page application) +│ ├── layout.tsx # Root layout with providers +│ └── api/ # API routes (minimal use) +├── components/ +│ ├── ui/ # shadcn/ui components (auto-generated) +│ └── game/ +│ ├── index.ts # Barrel exports +│ └── tabs/ # Tab-specific components +│ ├── CraftingTab.tsx +│ ├── LabTab.tsx +│ ├── SpellsTab.tsx +│ └── SpireTab.tsx +└── lib/ + ├── game/ + │ ├── store.ts # Zustand store (state + actions) + │ ├── effects.ts # Unified effect computation + │ ├── upgrade-effects.ts # Skill upgrade effect definitions + │ ├── constants.ts # Game definitions (spells, skills, etc.) + │ ├── skill-evolution.ts # Skill tier progression paths + │ ├── crafting-slice.ts # Equipment/enchantment logic + │ ├── types.ts # TypeScript interfaces + │ ├── formatting.ts # Display formatters + │ ├── utils.ts # Utility functions + │ └── data/ + │ ├── equipment.ts # Equipment type definitions + │ └── enchantment-effects.ts # Enchantment effect catalog + └── utils.ts # General utilities (cn function) +``` + +## Key Systems + +### 1. State Management (`store.ts`) + +The game uses a single Zustand store with the following key slices: + +```typescript +interface GameState { + // Time + day: number; + hour: number; + paused: boolean; + + // Mana + rawMana: number; + elements: Record; + + // Combat + currentFloor: number; + floorHP: number; + activeSpell: string; + castProgress: number; + + // Progression + skills: Record; + spells: Record; + skillUpgrades: Record; + skillTiers: Record; + + // Equipment + equipmentInstances: Record; + equippedInstances: Record; + enchantmentDesigns: EnchantmentDesign[]; + + // Prestige + insight: number; + prestigeUpgrades: Record; + signedPacts: number[]; +} +``` + +### 2. 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.) + +### 3. Combat System + +Combat uses a **cast speed** system: +- Each spell has `castSpeed` (casts per hour) +- Cast progress accumulates: `progress += castSpeed * attackSpeedMultiplier * HOURS_PER_TICK` +- When `progress >= 1`, spell is cast (cost deducted, damage dealt) +- DPS = `damagePerCast * castsPerSecond` + +Damage calculation order: +1. Base spell damage +2. Skill bonuses (combatTrain, arcaneFury, etc.) +3. Upgrade effects (multipliers, bonuses) +4. Special effects (Overpower, Berserker, etc.) +5. Elemental modifiers (same element +25%, super effective +50%) + +### 4. Crafting/Enchantment System + +Three-stage process: +1. **Design**: Select effects, takes time based on complexity +2. **Prepare**: Pay mana to prepare equipment, takes time +3. **Apply**: Apply design to equipment, costs mana per hour + +Equipment has **capacity** that limits total enchantment power. + +### 5. Skill Evolution System + +Skills have 5 tiers of evolution: +- At level 5: Choose 2 of 4 milestone upgrades +- At level 10: Choose 2 more upgrades, then tier up +- Each tier multiplies the skill's base effect by 10x + +## Important Patterns + +### Adding a New Effect + +1. **Define in `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.ts` SKILLS_DEF** +2. **Add evolution path in `skill-evolution.ts`** +3. **Add prerequisite checks in `store.ts`** +4. **Update UI in `page.tsx`** + +### Adding a New Spell + +1. **Define in `constants.ts` SPELLS_DEF** +2. **Add spell enchantment in `enchantment-effects.ts`** +3. **Add research skill in `constants.ts`** +4. **Map research to effect in `EFFECT_RESEARCH_MAPPING`** + +## Common Pitfalls + +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 + +## Testing Guidelines + +- Run `bun run lint` after changes +- Check dev server logs at `/home/z/my-project/dev.log` +- Test with fresh game state (clear localStorage) + +## File Size Guidelines + +- Keep `page.tsx` under 2000 lines by extracting to tab components +- Keep store functions focused; extract to helper files when >50 lines +- Use barrel exports (`index.ts`) for clean imports diff --git a/src/app/page.tsx b/src/app/page.tsx index b5e0c84..09ec43e 100755 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'; import { useGameStore, useGameLoop, fmt, fmtDec, getFloorElement, computeMaxMana, computeRegen, computeClickMana, calcDamage, getMeditationBonus, getIncursionStrength, canAffordSpellCost } from '@/lib/game/store'; import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF, PRESTIGE_DEF, MAX_DAY, INCURSION_START_DAY, MANA_PER_ELEMENT, SKILL_CATEGORIES, getStudySpeedMultiplier, getStudyCostMultiplier, ELEMENT_OPPOSITES, HOURS_PER_TICK, TICK_MS } from '@/lib/game/constants'; import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects'; +import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment'; import { SKILL_EVOLUTION_PATHS, getUpgradesForSkillAtMilestone, getNextTierSkill, getTierMultiplier } from '@/lib/game/skill-evolution'; import { getUnifiedEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects'; import type { SkillUpgradeChoice } from '@/lib/game/types'; @@ -21,6 +22,38 @@ import { Sparkles, Swords, BookOpen, FlaskConical, Trophy, RotateCcw, Pause, Pla import type { GameAction } from '@/lib/game/types'; import { CraftingTab } from '@/components/game/tabs/CraftingTab'; +// Helper to get active spells from equipped caster weapons +function getActiveEquipmentSpells( + equippedInstances: Record, + equipmentInstances: Record }> +): Array<{ spellId: string; equipmentId: string }> { + const spells: Array<{ spellId: string; equipmentId: string }> = []; + const weaponSlots = ['mainHand', 'offHand'] as const; + + for (const slot of weaponSlots) { + const instanceId = equippedInstances[slot]; + if (!instanceId) continue; + + const instance = equipmentInstances[instanceId]; + if (!instance) continue; + + const equipType = EQUIPMENT_TYPES[instance.typeId]; + if (!equipType || equipType.category !== 'caster') continue; + + for (const ench of instance.enchantments) { + const effectDef = ENCHANTMENT_EFFECTS[ench.effectId]; + if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) { + spells.push({ + spellId: effectDef.effect.spellId, + equipmentId: instanceId, + }); + } + } + } + + return spells; +} + // Element icon mapping const ELEMENT_ICONS: Record = { fire: Flame, @@ -157,29 +190,33 @@ export default function ManaLoopGame() { const damageBreakdown = getDamageBreakdown(); - // Compute DPS based on cast speed - const getDPS = () => { - const spell = SPELLS_DEF[store.activeSpell]; - if (!spell) return 0; - - const spellCastSpeed = spell.castSpeed || 1; + // Get all active spells from equipment + const activeEquipmentSpells = getActiveEquipmentSpells(store.equippedInstances, store.equipmentInstances); + + // Compute DPS based on cast speed for ALL active spells + const getTotalDPS = () => { const quickCastBonus = 1 + (store.skills.quickCast || 0) * 0.05; const attackSpeedMult = upgradeEffects.attackSpeedMultiplier; - const totalCastSpeed = spellCastSpeed * quickCastBonus * attackSpeedMult; + const castsPerSecondMult = HOURS_PER_TICK / (TICK_MS / 1000); - // Damage per cast - const damagePerCast = calcDamage(store, store.activeSpell, floorElem); + let totalDPS = 0; - // Casts per second = castSpeed / hours per second - // HOURS_PER_TICK = 0.04 hours per tick, TICK_MS = 200ms - // So castSpeed casts/hour * 0.04 hours/tick = casts per tick - // casts per tick / 0.2 seconds per tick = casts per second - const castsPerSecond = totalCastSpeed * HOURS_PER_TICK / (TICK_MS / 1000); + for (const { spellId } of activeEquipmentSpells) { + const spell = SPELLS_DEF[spellId]; + if (!spell) continue; + + const spellCastSpeed = spell.castSpeed || 1; + const totalCastSpeed = spellCastSpeed * quickCastBonus * attackSpeedMult; + const damagePerCast = calcDamage(store, spellId, floorElem); + const castsPerSecond = totalCastSpeed * castsPerSecondMult; + + totalDPS += damagePerCast * castsPerSecond; + } - return damagePerCast * castsPerSecond; + return totalDPS; }; - const dps = getDPS(); + const totalDPS = getTotalDPS(); // Effective regen (with meditation, incursion, cascade) // Note: baseRegen already includes upgradeEffects multipliers and bonuses @@ -516,7 +553,7 @@ export default function ManaLoopGame() {
{fmt(store.floorHP)} / {fmt(store.floorMaxHP)} HP - DPS: {store.currentAction === 'climb' && canCastSpell(store.activeSpell) ? fmtDec(dps) : '—'} + DPS: {store.currentAction === 'climb' && activeEquipmentSpells.length > 0 ? fmtDec(totalDPS) : '—'}
@@ -529,62 +566,74 @@ export default function ManaLoopGame() { - {/* Active Spell Card */} + {/* Active Spells Card - Shows all spells from equipped weapons */} - Active Spell + + Active Spells ({activeEquipmentSpells.length}) + - {activeSpellDef ? ( - <> -
- {activeSpellDef.name} - {activeSpellDef.tier === 0 && Basic} - {activeSpellDef.tier >= 4 && Legendary} -
-
- ⚔️ {fmt(calcDamage(store, store.activeSpell))} dmg • - - {' '}{formatSpellCost(activeSpellDef.cost)} - - {' '}• ⚡ {(activeSpellDef.castSpeed || 1).toFixed(1)} casts/hr -
- - {/* Cast progress bar when climbing */} - {store.currentAction === 'climb' && ( -
-
- Cast Progress - {((store.castProgress || 0) * 100).toFixed(0)}% + {activeEquipmentSpells.length > 0 ? ( +
+ {activeEquipmentSpells.map(({ spellId, equipmentId }) => { + const spellDef = SPELLS_DEF[spellId]; + if (!spellDef) return null; + + const spellState = store.equipmentSpellStates?.find( + s => s.spellId === spellId && s.sourceEquipment === equipmentId + ); + const progress = spellState?.castProgress || 0; + const canCast = canAffordSpellCost(spellDef.cost, store.rawMana, store.elements); + + return ( +
+
+
+ {spellDef.name} + {spellDef.tier === 0 && Basic} + {spellDef.tier >= 4 && Legendary} +
+ + {canCast ? '✓' : '✗'} + +
+
+ ⚔️ {fmt(calcDamage(store, spellId, floorElem))} dmg • + + {' '}{formatSpellCost(spellDef.cost)} + + {' '}• ⚡ {(spellDef.castSpeed || 1).toFixed(1)}/hr +
+ + {/* Cast progress bar when climbing */} + {store.currentAction === 'climb' && ( +
+
+ Cast + {(progress * 100).toFixed(0)}% +
+ +
+ )} + + {spellDef.effects && spellDef.effects.length > 0 && ( +
+ {spellDef.effects.map((eff, i) => ( + + {eff.type === 'lifesteal' && `🩸 ${Math.round(eff.value * 100)}%`} + {eff.type === 'burn' && `🔥 Burn`} + {eff.type === 'freeze' && `❄️ Freeze`} + + ))} +
+ )}
- -
- )} - - {activeSpellDef.desc && ( -
{activeSpellDef.desc}
- )} - {activeSpellDef.effects && activeSpellDef.effects.length > 0 && ( -
- {activeSpellDef.effects.map((eff, i) => ( - - {eff.type === 'lifesteal' && `🩸 ${Math.round(eff.value * 100)}% lifesteal`} - {eff.type === 'burn' && `🔥 Burn`} - {eff.type === 'freeze' && `❄️ Freeze`} - - ))} -
- )} - - ) : ( -
No spell selected
- )} - - {/* Can cast indicator */} - {activeSpellDef && ( -
- {canCastSpell(store.activeSpell) ? '✓ Can cast' : '✗ Insufficient mana'} + ); + })}
+ ) : ( +
No spells on equipped weapons. Enchant a staff with spell effects.
)} {incursionStrength > 0 && ( @@ -1444,10 +1493,6 @@ export default function ManaLoopGame() { })()}
-
- Deep Reservoir Bonus: - +{fmt((store.skills.deepReservoir || 0) * 500)} -
Prestige Mana Well: +{fmt((store.prestigeUpgrades.manaWell || 0) * 500)} diff --git a/src/lib/game/constants.ts b/src/lib/game/constants.ts index d4db475..5906a3b 100755 --- a/src/lib/game/constants.ts +++ b/src/lib/game/constants.ts @@ -714,7 +714,7 @@ export const SKILLS_DEF: Record = { // Special Effect Research researchSpecialEffects: { name: "Special Effect Research", desc: "Unlock Echo Chamber, Siphoning, Bane effects", cat: "effectResearch", max: 1, base: 500, studyTime: 10, req: { enchanting: 4 } }, - researchExecutioner: { name: "Executioner Research", desc: "Unlock Executioner effect", cat: "effectResearch", max: 1, base: 700, studyTime: 12, req: { researchSpecialEffects: 1, enchanting: 5 } }, + researchOverpower: { name: "Overpower Research", desc: "Unlock Overpower effect", cat: "effectResearch", max: 1, base: 700, studyTime: 12, req: { researchSpecialEffects: 1, enchanting: 5 } }, // Research Skills (longer study times: 12-72 hours) manaTap: { name: "Mana Tap", desc: "+1 mana/click", cat: "research", max: 1, base: 300, studyTime: 12 }, @@ -806,7 +806,7 @@ export const EFFECT_RESEARCH_MAPPING: Record = { // Special Effect Research researchSpecialEffects: ['spell_echo_10', 'lifesteal_5', 'guardian_dmg_10'], - researchExecutioner: ['execute_25'], + researchOverpower: ['overpower_80'], }; // Base effects unlocked when player gets enchanting skill level 1 diff --git a/src/lib/game/data/enchantment-effects.ts b/src/lib/game/data/enchantment-effects.ts index 8458df2..ffd0f43 100755 --- a/src/lib/game/data/enchantment-effects.ts +++ b/src/lib/game/data/enchantment-effects.ts @@ -557,15 +557,15 @@ export const ENCHANTMENT_EFFECTS: Record = { allowedEquipmentCategories: CASTER_CATALYST_ACCESSORY, effect: { type: 'multiplier', stat: 'guardianDamage', value: 1.10 } }, - execute_25: { - id: 'execute_25', - name: 'Executioner', - description: '+50% damage to enemies below 25% HP', + overpower_80: { + id: 'overpower_80', + name: 'Overpower', + description: '+50% damage when mana above 80%', category: 'special', baseCapacityCost: 55, maxStacks: 1, allowedEquipmentCategories: CASTER_AND_HANDS, - effect: { type: 'special', specialId: 'executioner' } + effect: { type: 'special', specialId: 'overpower' } }, }; diff --git a/src/lib/game/skill-evolution.ts b/src/lib/game/skill-evolution.ts index 5c16b9b..7840239 100755 --- a/src/lib/game/skill-evolution.ts +++ b/src/lib/game/skill-evolution.ts @@ -48,7 +48,7 @@ const COMBAT_TRAIN_TIER1_UPGRADES_L5: SkillUpgradeChoice[] = [ ]; const COMBAT_TRAIN_TIER1_UPGRADES_L10: SkillUpgradeChoice[] = [ - { id: 'ct_t1_l10_execute', name: 'Executioner', desc: '+100% damage to enemies below 25% HP', milestone: 10, effect: { type: 'special', specialId: 'executioner', specialDesc: 'Execute bonus' } }, + { id: 'ct_t1_l10_overpower', name: 'Overpower', desc: '+50% damage when mana above 80%', milestone: 10, effect: { type: 'special', specialId: 'overpower', specialDesc: 'High mana damage bonus' } }, { id: 'ct_t1_l10_berserker', name: 'Berserker', desc: '+50% damage when below 50% mana', milestone: 10, effect: { type: 'special', specialId: 'berserker', specialDesc: 'Low mana damage bonus' } }, { id: 'ct_t1_l10_combo', name: 'Combo Master', desc: 'Every 5th attack deals 3x damage', milestone: 10, effect: { type: 'special', specialId: 'comboMaster', specialDesc: 'Combo finisher' } }, { id: 'ct_t1_l10_adrenaline', name: 'Adrenaline Rush', desc: 'Defeating an enemy restores 5% mana', milestone: 10, effect: { type: 'special', specialId: 'adrenalineRush', specialDesc: 'Kill restore' } }, diff --git a/src/lib/game/store.ts b/src/lib/game/store.ts index dbb2281..436659e 100755 --- a/src/lib/game/store.ts +++ b/src/lib/game/store.ts @@ -98,6 +98,43 @@ export function getFloorElement(floor: number): string { return FLOOR_ELEM_CYCLE[(floor - 1) % 8]; } +// Get all spells from equipped caster weapons (staves, wands, etc.) +// Returns array of { spellId, equipmentInstanceId } +function getActiveEquipmentSpells( + equippedInstances: Record, + equipmentInstances: Record +): Array<{ spellId: string; equipmentId: string }> { + const spells: Array<{ spellId: string; equipmentId: string }> = []; + + // Check main hand and off hand for caster equipment + const weaponSlots = ['mainHand', 'offHand'] as const; + + for (const slot of weaponSlots) { + const instanceId = equippedInstances[slot]; + if (!instanceId) continue; + + const instance = equipmentInstances[instanceId]; + if (!instance) continue; + + // Check if this is a caster-type equipment + const equipType = EQUIPMENT_TYPES[instance.typeId]; + if (!equipType || equipType.category !== 'caster') continue; + + // Get spells from enchantments + for (const ench of instance.enchantments) { + const effectDef = ENCHANTMENT_EFFECTS[ench.effectId]; + if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) { + spells.push({ + spellId: effectDef.effect.spellId, + equipmentId: instanceId, + }); + } + } + } + + return spells; +} + // ─── Computed Stats Functions ───────────────────────────────────────────────── // Helper to get effective skill level accounting for tiers @@ -186,8 +223,8 @@ export function computeRegen( * Compute regen with dynamic special effects (needs current mana, max mana, incursion) */ export function computeEffectiveRegen( - state: Pick, - effects?: ComputedEffects + state: Pick, + effects?: ComputedEffects | UnifiedEffects ): number { // Base regen from existing function let regen = computeRegen(state, effects); @@ -369,7 +406,12 @@ function deductSpellCost( function makeInitial(overrides: Partial = {}): GameState { const pu = overrides.prestigeUpgrades || {}; const startFloor = 1 + (pu.spireKey || 0) * 2; - const elemMax = computeElementMax({ skills: overrides.skills || {}, prestigeUpgrades: pu }); + const elemMax = computeElementMax({ + skills: overrides.skills || {}, + prestigeUpgrades: pu, + skillUpgrades: overrides.skillUpgrades || {}, + skillTiers: overrides.skillTiers || {} + }); const elements: Record = {}; Object.keys(ELEMENTS).forEach((k) => { @@ -695,18 +737,47 @@ export const useGameStore = create()( } } - // Combat - uses cast speed and spell casting - let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, castProgress } = state; + // Combat - MULTI-SPELL casting from all equipped weapons + let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, equipmentSpellStates } = state; const floorElement = getFloorElement(currentFloor); if (state.currentAction === 'climb') { - const spellId = state.activeSpell; - const spellDef = SPELLS_DEF[spellId]; + // Get all spells from equipped caster weapons + const activeSpells = getActiveEquipmentSpells(state.equippedInstances, state.equipmentInstances); - if (spellDef) { - // Compute attack speed from quickCast skill and upgrades - const baseAttackSpeed = 1 + (state.skills.quickCast || 0) * 0.05; - const totalAttackSpeed = baseAttackSpeed * effects.attackSpeedMultiplier; + // Initialize spell states if needed + if (!equipmentSpellStates) { + equipmentSpellStates = []; + } + + // Ensure we have state for all active spells + for (const { spellId, equipmentId } of activeSpells) { + if (!equipmentSpellStates.find(s => s.spellId === spellId && s.sourceEquipment === equipmentId)) { + equipmentSpellStates.push({ + spellId, + sourceEquipment: equipmentId, + castProgress: 0, + }); + } + } + + // Remove states for spells that are no longer equipped + equipmentSpellStates = equipmentSpellStates.filter(es => + activeSpells.some(as => as.spellId === es.spellId && as.equipmentId === es.sourceEquipment) + ); + + // Compute attack speed from quickCast skill and upgrades + const baseAttackSpeed = 1 + (state.skills.quickCast || 0) * 0.05; + const totalAttackSpeed = baseAttackSpeed * effects.attackSpeedMultiplier; + + // Process each active spell + for (const { spellId, equipmentId } of activeSpells) { + const spellDef = SPELLS_DEF[spellId]; + if (!spellDef) continue; + + // Get or create spell state + let spellState = equipmentSpellStates.find(s => s.spellId === spellId && s.sourceEquipment === equipmentId); + if (!spellState) continue; // Get spell cast speed (casts per hour, default 1) const spellCastSpeed = spellDef.castSpeed || 1; @@ -715,10 +786,10 @@ export const useGameStore = create()( const progressPerTick = HOURS_PER_TICK * spellCastSpeed * totalAttackSpeed; // Accumulate cast progress - castProgress = (castProgress || 0) + progressPerTick; + spellState = { ...spellState, castProgress: spellState.castProgress + progressPerTick }; // Process complete casts - while (castProgress >= 1 && canAffordSpellCost(spellDef.cost, rawMana, elements)) { + while (spellState.castProgress >= 1 && canAffordSpellCost(spellDef.cost, rawMana, elements)) { // Deduct cost const afterCost = deductSpellCost(spellDef.cost, rawMana, elements); rawMana = afterCost.rawMana; @@ -731,9 +802,9 @@ export const useGameStore = create()( // Apply upgrade damage multipliers and bonuses dmg = dmg * effects.baseDamageMultiplier + effects.baseDamageBonus; - // Executioner: +100% damage to enemies below 25% HP - if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && floorHP / floorMaxHP < 0.25) { - dmg *= 2; + // Overpower: +50% damage when mana above 80% + if (hasSpecial(effects, SPECIAL_EFFECTS.OVERPOWER) && rawMana >= maxMana * 0.8) { + dmg *= 1.5; } // Berserker: +50% damage when below 50% mana @@ -745,7 +816,7 @@ export const useGameStore = create()( const echoChance = (skills.spellEcho || 0) * 0.1; if (Math.random() < echoChance) { dmg *= 2; - log = [`✨ Spell Echo! Double damage!`, ...log.slice(0, 49)]; + log = [`✨ Spell Echo! ${spellDef.name} deals double damage!`, ...log.slice(0, 49)]; } // Lifesteal effect @@ -759,7 +830,7 @@ export const useGameStore = create()( floorHP = Math.max(0, floorHP - dmg); // Reduce cast progress by 1 (one cast completed) - castProgress -= 1; + spellState = { ...spellState, castProgress: spellState.castProgress - 1 }; if (floorHP <= 0) { // Floor cleared @@ -781,13 +852,17 @@ export const useGameStore = create()( floorHP = floorMaxHP; maxFloorReached = Math.max(maxFloorReached, currentFloor); - // Reset cast progress on floor change - castProgress = 0; + // Reset ALL spell progress on floor change + equipmentSpellStates = equipmentSpellStates.map(s => ({ ...s, castProgress: 0 })); + spellState = { ...spellState, castProgress: 0 }; + break; // Exit the while loop - new floor } } - } else { - // Not enough mana - pause casting (keep progress) - castProgress = castProgress || 0; + + // Update the spell state in the array + equipmentSpellStates = equipmentSpellStates.map(s => + (s.spellId === spellId && s.sourceEquipment === equipmentId) ? spellState : s + ); } } @@ -802,7 +877,7 @@ export const useGameStore = create()( floorMaxHP, maxFloorReached, signedPacts, - castProgress, + equipmentSpellStates, incursionStrength, currentStudyTarget, skills, @@ -837,8 +912,8 @@ export const useGameStore = create()( spells, elements, log, - castProgress, - }); + equipmentSpellStates, + }); return; } @@ -861,7 +936,7 @@ export const useGameStore = create()( elements, unlockedEffects, log, - castProgress, + equipmentSpellStates, ...craftingUpdates, }); }, diff --git a/src/lib/game/types.ts b/src/lib/game/types.ts index f13b73e..e8d0883 100755 --- a/src/lib/game/types.ts +++ b/src/lib/game/types.ts @@ -2,6 +2,9 @@ export type ElementCategory = 'base' | 'utility' | 'composite' | 'exotic'; +// Equipment slots for the equipment system +export type EquipmentSlot = 'mainHand' | 'offHand' | 'head' | 'body' | 'hands' | 'feet' | 'accessory1' | 'accessory2'; + export interface ElementDef { name: string; sym: string; diff --git a/src/lib/game/upgrade-effects.ts b/src/lib/game/upgrade-effects.ts index f4d0ace..5287843 100755 --- a/src/lib/game/upgrade-effects.ts +++ b/src/lib/game/upgrade-effects.ts @@ -77,7 +77,7 @@ export const SPECIAL_EFFECTS = { // Combat special effects BATTLE_FURY: 'battleFury', // +10% damage per consecutive hit ARMOR_PIERCE: 'armorPierce', // Ignore 10% floor defense - EXECUTIONER: 'executioner', // +100% damage to enemies below 25% HP + OVERPOWER: 'overpower', // +50% damage when mana above 80% BERSERKER: 'berserker', // +50% damage when below 50% mana COMBO_MASTER: 'comboMaster', // Every 5th attack deals 3x damage ADRENALINE_RUSH: 'adrenalineRush', // Defeating enemy restores 5% mana @@ -354,9 +354,9 @@ export function computeDynamicDamage( damage *= (1 + furyBonus); } - // Executioner: +100% damage to enemies below 25% HP - if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && floorHPPct < 0.25) { - damage *= 2; + // Overpower: +50% damage when mana above 80% + if (hasSpecial(effects, SPECIAL_EFFECTS.OVERPOWER) && currentMana >= maxMana * 0.8) { + damage *= 1.5; } // Berserker: +50% damage when below 50% mana diff --git a/worklog.md b/worklog.md index b1af9a1..3e110b4 100755 --- a/worklog.md +++ b/worklog.md @@ -1,5 +1,37 @@ # Mana Loop Worklog +--- +Task ID: 9 +Agent: Main +Task: Codebase optimization, error checking, and best practices review + +Work Log: +- Verified dev server is running correctly (GET / 200 in 3.9s) +- Ran lint check: No errors found +- Reviewed unified effects system in effects.ts: Properly merges skill upgrades AND equipment enchantments +- Verified guardian boon system: `getBoonBonuses()` correctly applies pact bonuses to damage, insight, and stats +- Checked tab components: All properly created (SpireTab, SpellsTab, LabTab, CraftingTab) with correct exports +- Reviewed combat system: + - Uses cast speed system (spells have unique cast speeds) + - Damage calculation includes skill bonuses, pact multipliers, and elemental modifiers + - Cast progress accumulates based on spell cast speed and attack speed bonuses +- Verified equipment/enchantment system: + - Equipment instances properly created with capacity system + - Enchantments apply effects through unified effects system + - Spell enchantments correctly grant spells via `getEquipmentSpells()` +- Checked for missing functionality: All major features implemented +- Verified type safety: GameAction type includes all necessary actions + +Stage Summary: +- No critical bugs found - game is running correctly +- Unified effects system properly integrates skill upgrades and equipment enchantments +- Guardian boons are applied through getBoonBonuses() for damage and insight calculations +- All tab components properly integrated and functional +- Combat uses proper cast speed mechanics with equipment effects applied +- Code quality: Clean, no lint errors, well-structured Zustand store + +--- + --- Task ID: 1 Agent: Main @@ -200,3 +232,80 @@ Stage Summary: - Spells only learnable from equipment and pacts - All crafting skills have evolution paths - All lint checks pass + +--- +Task ID: 10 +Agent: Main +Task: Comprehensive codebase review - effects application, UI display, combat mechanics, and replace Executioner powers + +Work Log: +- **Replaced Executioner powers with Overpower**: + - Executioner gave +100% damage to enemies below 25% HP (insta-kill mechanic) + - Replaced with Overpower: +50% damage when player mana is above 80% + - Updated files: upgrade-effects.ts, skill-evolution.ts, enchantment-effects.ts, constants.ts, store.ts + - Renamed researchExecutioner to researchOverpower + - Updated EFFECT_RESEARCH_MAPPING to use 'overpower_80' instead of 'execute_25' +- **Verified effects are correctly applied and displayed**: + - getUnifiedEffects() properly merges skill upgrades AND equipment enchantments + - Effects flow: computeEffects() → computeEquipmentEffects() → computeAllEffects() + - Stats tab displays active upgrades with their effects + - Equipment bonuses shown in dedicated section +- **Verified multiple offensive enchantments trigger correctly**: + - computeEquipmentEffects() iterates through all enchantments on all equipped items + - Bonus effects stack additively (value × stacks) + - Multiplier effects stack multiplicatively + - Special effects are accumulated in a Set (unique) +- **Verified spell cost prevents casting**: + - canAffordSpellCost() checks raw mana or elemental mana + - Combat tick checks affordability before casting + - If insufficient mana, cast progress is paused (not lost) +- **Verified DPS/casts per hour display**: + - DPS = damagePerCast × castsPerSecond + - Casts per second = castSpeed × HOURS_PER_TICK / (TICK_MS / 1000) + - Cast speed affected by spell base speed and attack speed multipliers + - UI shows cast progress bar and DPS when climbing +- **Created AGENTS.md**: + - Comprehensive project architecture documentation + - Directory structure explanation + - Key systems overview (state, effects, combat, crafting, skills) + - Important patterns for adding new features + - Common pitfalls to avoid + +Stage Summary: +- Executioner powers replaced with Overpower (high mana damage bonus) +- All effect systems verified working correctly +- Combat mechanics properly implement cast speed and damage calculation +- Spell cost correctly gates casting +- DPS display accurate based on cast speed formula +- AGENTS.md created for future AI agent reference +- All lint checks pass + +--- +Task ID: 11 +Agent: Main +Task: Implement multi-spell casting - all spells on equipped weapons cast automatically + +Work Log: +- **Added TypeScript type for EquipmentSlot** to types.ts +- **Created getActiveEquipmentSpells() helper function** in store.ts to get all spells from equipped caster weapons (mainHand, offHand) +- **Rewrote combat system** to process ALL spells from equipped weapons: + - Each spell has independent cast progress tracking + - Uses `equipmentSpellStates` array to track per-spell progress + - Processes each spell in sequence during combat tick + - Each spell deducts its own mana cost + - All spells share the same attack speed multiplier +- **Updated UI** in page.tsx: + - Added EQUIPMENT_TYPES import + - Added getActiveEquipmentSpells helper function + - Changed "Active Spell" card to "Active Spells (N)" showing all equipped spells + - Each spell shows its own progress bar when climbing + - Total DPS now sums DPS from all active spells +- **Fixed TypeScript errors** in computeEffectiveRegen and makeInitial functions +- **Verified game starts correctly** with HTTP 200 response + +Stage Summary: +- All spells on equipped weapons now cast automatically (no toggling required) +- Each spell has its own cast progress bar, time, and mana cost +- Multi-casting is fully functional +- Game compiles and runs without errors +- Lint passes with no issues