pack
This commit is contained in:
@@ -1205,3 +1205,23 @@ Checked 846 installs across 915 packages (no changes) [45.00ms]
|
|||||||
$ prisma db push
|
$ 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'
|
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
|
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
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
548
|
488
|
||||||
|
|||||||
217
AGENTS.md
Normal file
217
AGENTS.md
Normal file
@@ -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<string, ElementState>;
|
||||||
|
|
||||||
|
// Combat
|
||||||
|
currentFloor: number;
|
||||||
|
floorHP: number;
|
||||||
|
activeSpell: string;
|
||||||
|
castProgress: number;
|
||||||
|
|
||||||
|
// Progression
|
||||||
|
skills: Record<string, number>;
|
||||||
|
spells: Record<string, SpellState>;
|
||||||
|
skillUpgrades: Record<string, string[]>;
|
||||||
|
skillTiers: Record<string, number>;
|
||||||
|
|
||||||
|
// Equipment
|
||||||
|
equipmentInstances: Record<string, EquipmentInstance>;
|
||||||
|
equippedInstances: Record<string, string | null>;
|
||||||
|
enchantmentDesigns: EnchantmentDesign[];
|
||||||
|
|
||||||
|
// Prestige
|
||||||
|
insight: number;
|
||||||
|
prestigeUpgrades: Record<string, number>;
|
||||||
|
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<string>, // 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
|
||||||
187
src/app/page.tsx
187
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 { 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 { 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 { 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 { SKILL_EVOLUTION_PATHS, getUpgradesForSkillAtMilestone, getNextTierSkill, getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||||
import { getUnifiedEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
import { getUnifiedEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
||||||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
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 type { GameAction } from '@/lib/game/types';
|
||||||
import { CraftingTab } from '@/components/game/tabs/CraftingTab';
|
import { CraftingTab } from '@/components/game/tabs/CraftingTab';
|
||||||
|
|
||||||
|
// Helper to get active spells from equipped caster weapons
|
||||||
|
function getActiveEquipmentSpells(
|
||||||
|
equippedInstances: Record<string, string | null>,
|
||||||
|
equipmentInstances: Record<string, { instanceId: string; typeId: string; enchantments: Array<{ effectId: string }> }>
|
||||||
|
): 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
|
// Element icon mapping
|
||||||
const ELEMENT_ICONS: Record<string, typeof Flame> = {
|
const ELEMENT_ICONS: Record<string, typeof Flame> = {
|
||||||
fire: Flame,
|
fire: Flame,
|
||||||
@@ -157,29 +190,33 @@ export default function ManaLoopGame() {
|
|||||||
|
|
||||||
const damageBreakdown = getDamageBreakdown();
|
const damageBreakdown = getDamageBreakdown();
|
||||||
|
|
||||||
// Compute DPS based on cast speed
|
// Get all active spells from equipment
|
||||||
const getDPS = () => {
|
const activeEquipmentSpells = getActiveEquipmentSpells(store.equippedInstances, store.equipmentInstances);
|
||||||
const spell = SPELLS_DEF[store.activeSpell];
|
|
||||||
if (!spell) return 0;
|
// Compute DPS based on cast speed for ALL active spells
|
||||||
|
const getTotalDPS = () => {
|
||||||
const spellCastSpeed = spell.castSpeed || 1;
|
|
||||||
const quickCastBonus = 1 + (store.skills.quickCast || 0) * 0.05;
|
const quickCastBonus = 1 + (store.skills.quickCast || 0) * 0.05;
|
||||||
const attackSpeedMult = upgradeEffects.attackSpeedMultiplier;
|
const attackSpeedMult = upgradeEffects.attackSpeedMultiplier;
|
||||||
const totalCastSpeed = spellCastSpeed * quickCastBonus * attackSpeedMult;
|
const castsPerSecondMult = HOURS_PER_TICK / (TICK_MS / 1000);
|
||||||
|
|
||||||
// Damage per cast
|
let totalDPS = 0;
|
||||||
const damagePerCast = calcDamage(store, store.activeSpell, floorElem);
|
|
||||||
|
|
||||||
// Casts per second = castSpeed / hours per second
|
for (const { spellId } of activeEquipmentSpells) {
|
||||||
// HOURS_PER_TICK = 0.04 hours per tick, TICK_MS = 200ms
|
const spell = SPELLS_DEF[spellId];
|
||||||
// So castSpeed casts/hour * 0.04 hours/tick = casts per tick
|
if (!spell) continue;
|
||||||
// casts per tick / 0.2 seconds per tick = casts per second
|
|
||||||
const castsPerSecond = totalCastSpeed * HOURS_PER_TICK / (TICK_MS / 1000);
|
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)
|
// Effective regen (with meditation, incursion, cascade)
|
||||||
// Note: baseRegen already includes upgradeEffects multipliers and bonuses
|
// Note: baseRegen already includes upgradeEffects multipliers and bonuses
|
||||||
@@ -516,7 +553,7 @@ export default function ManaLoopGame() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-xs text-gray-400 game-mono">
|
<div className="flex justify-between text-xs text-gray-400 game-mono">
|
||||||
<span>{fmt(store.floorHP)} / {fmt(store.floorMaxHP)} HP</span>
|
<span>{fmt(store.floorHP)} / {fmt(store.floorMaxHP)} HP</span>
|
||||||
<span>DPS: {store.currentAction === 'climb' && canCastSpell(store.activeSpell) ? fmtDec(dps) : '—'}</span>
|
<span>DPS: {store.currentAction === 'climb' && activeEquipmentSpells.length > 0 ? fmtDec(totalDPS) : '—'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -529,62 +566,74 @@ export default function ManaLoopGame() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Active Spell Card */}
|
{/* Active Spells Card - Shows all spells from equipped weapons */}
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
<Card className="bg-gray-900/80 border-gray-700">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Active Spell</CardTitle>
|
<CardTitle className="text-amber-400 game-panel-title text-xs">
|
||||||
|
Active Spells ({activeEquipmentSpells.length})
|
||||||
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
{activeSpellDef ? (
|
{activeEquipmentSpells.length > 0 ? (
|
||||||
<>
|
<div className="space-y-3">
|
||||||
<div className="text-lg font-semibold game-panel-title" style={{ color: activeSpellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[activeSpellDef.elem]?.color }}>
|
{activeEquipmentSpells.map(({ spellId, equipmentId }) => {
|
||||||
{activeSpellDef.name}
|
const spellDef = SPELLS_DEF[spellId];
|
||||||
{activeSpellDef.tier === 0 && <Badge className="ml-2 bg-gray-600 text-gray-200">Basic</Badge>}
|
if (!spellDef) return null;
|
||||||
{activeSpellDef.tier >= 4 && <Badge className="ml-2 bg-amber-600 text-amber-100">Legendary</Badge>}
|
|
||||||
</div>
|
const spellState = store.equipmentSpellStates?.find(
|
||||||
<div className="text-sm text-gray-400 game-mono">
|
s => s.spellId === spellId && s.sourceEquipment === equipmentId
|
||||||
⚔️ {fmt(calcDamage(store, store.activeSpell))} dmg •
|
);
|
||||||
<span style={{ color: getSpellCostColor(activeSpellDef.cost) }}>
|
const progress = spellState?.castProgress || 0;
|
||||||
{' '}{formatSpellCost(activeSpellDef.cost)}
|
const canCast = canAffordSpellCost(spellDef.cost, store.rawMana, store.elements);
|
||||||
</span>
|
|
||||||
{' '}• ⚡ {(activeSpellDef.castSpeed || 1).toFixed(1)} casts/hr
|
return (
|
||||||
</div>
|
<div key={`${spellId}-${equipmentId}`} className="p-2 rounded border border-gray-700 bg-gray-800/30">
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
{/* Cast progress bar when climbing */}
|
<div className="text-sm font-semibold game-panel-title" style={{ color: spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color }}>
|
||||||
{store.currentAction === 'climb' && (
|
{spellDef.name}
|
||||||
<div className="space-y-1">
|
{spellDef.tier === 0 && <Badge className="ml-2 bg-gray-600 text-gray-200 text-xs">Basic</Badge>}
|
||||||
<div className="flex justify-between text-xs text-gray-400">
|
{spellDef.tier >= 4 && <Badge className="ml-2 bg-amber-600 text-amber-100 text-xs">Legendary</Badge>}
|
||||||
<span>Cast Progress</span>
|
</div>
|
||||||
<span>{((store.castProgress || 0) * 100).toFixed(0)}%</span>
|
<span className={`text-xs ${canCast ? 'text-green-400' : 'text-red-400'}`}>
|
||||||
|
{canCast ? '✓' : '✗'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400 game-mono mb-1">
|
||||||
|
⚔️ {fmt(calcDamage(store, spellId, floorElem))} dmg •
|
||||||
|
<span style={{ color: getSpellCostColor(spellDef.cost) }}>
|
||||||
|
{' '}{formatSpellCost(spellDef.cost)}
|
||||||
|
</span>
|
||||||
|
{' '}• ⚡ {(spellDef.castSpeed || 1).toFixed(1)}/hr
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cast progress bar when climbing */}
|
||||||
|
{store.currentAction === 'climb' && (
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<div className="flex justify-between text-xs text-gray-500">
|
||||||
|
<span>Cast</span>
|
||||||
|
<span>{(progress * 100).toFixed(0)}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={Math.min(100, progress * 100)} className="h-1.5 bg-gray-700" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{spellDef.effects && spellDef.effects.length > 0 && (
|
||||||
|
<div className="flex gap-1 flex-wrap mt-1">
|
||||||
|
{spellDef.effects.map((eff, i) => (
|
||||||
|
<Badge key={i} variant="outline" className="text-xs py-0">
|
||||||
|
{eff.type === 'lifesteal' && `🩸 ${Math.round(eff.value * 100)}%`}
|
||||||
|
{eff.type === 'burn' && `🔥 Burn`}
|
||||||
|
{eff.type === 'freeze' && `❄️ Freeze`}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Progress value={Math.min(100, (store.castProgress || 0) * 100)} className="h-2 bg-gray-800" />
|
);
|
||||||
</div>
|
})}
|
||||||
)}
|
|
||||||
|
|
||||||
{activeSpellDef.desc && (
|
|
||||||
<div className="text-xs text-gray-500 italic">{activeSpellDef.desc}</div>
|
|
||||||
)}
|
|
||||||
{activeSpellDef.effects && activeSpellDef.effects.length > 0 && (
|
|
||||||
<div className="flex gap-1 flex-wrap">
|
|
||||||
{activeSpellDef.effects.map((eff, i) => (
|
|
||||||
<Badge key={i} variant="outline" className="text-xs">
|
|
||||||
{eff.type === 'lifesteal' && `🩸 ${Math.round(eff.value * 100)}% lifesteal`}
|
|
||||||
{eff.type === 'burn' && `🔥 Burn`}
|
|
||||||
{eff.type === 'freeze' && `❄️ Freeze`}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="text-gray-500">No spell selected</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Can cast indicator */}
|
|
||||||
{activeSpellDef && (
|
|
||||||
<div className={`text-xs ${canCastSpell(store.activeSpell) ? 'text-green-400' : 'text-red-400'}`}>
|
|
||||||
{canCastSpell(store.activeSpell) ? '✓ Can cast' : '✗ Insufficient mana'}
|
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-gray-500 text-sm">No spells on equipped weapons. Enchant a staff with spell effects.</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{incursionStrength > 0 && (
|
{incursionStrength > 0 && (
|
||||||
@@ -1444,10 +1493,6 @@ export default function ManaLoopGame() {
|
|||||||
})()}
|
})()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Deep Reservoir Bonus:</span>
|
|
||||||
<span className="text-blue-300">+{fmt((store.skills.deepReservoir || 0) * 500)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-gray-400">Prestige Mana Well:</span>
|
<span className="text-gray-400">Prestige Mana Well:</span>
|
||||||
<span className="text-blue-300">+{fmt((store.prestigeUpgrades.manaWell || 0) * 500)}</span>
|
<span className="text-blue-300">+{fmt((store.prestigeUpgrades.manaWell || 0) * 500)}</span>
|
||||||
|
|||||||
@@ -714,7 +714,7 @@ export const SKILLS_DEF: Record<string, SkillDef> = {
|
|||||||
|
|
||||||
// Special Effect Research
|
// 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 } },
|
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)
|
// Research Skills (longer study times: 12-72 hours)
|
||||||
manaTap: { name: "Mana Tap", desc: "+1 mana/click", cat: "research", max: 1, base: 300, studyTime: 12 },
|
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<string, string[]> = {
|
|||||||
|
|
||||||
// Special Effect Research
|
// Special Effect Research
|
||||||
researchSpecialEffects: ['spell_echo_10', 'lifesteal_5', 'guardian_dmg_10'],
|
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
|
// Base effects unlocked when player gets enchanting skill level 1
|
||||||
|
|||||||
@@ -557,15 +557,15 @@ export const ENCHANTMENT_EFFECTS: Record<string, EnchantmentEffectDef> = {
|
|||||||
allowedEquipmentCategories: CASTER_CATALYST_ACCESSORY,
|
allowedEquipmentCategories: CASTER_CATALYST_ACCESSORY,
|
||||||
effect: { type: 'multiplier', stat: 'guardianDamage', value: 1.10 }
|
effect: { type: 'multiplier', stat: 'guardianDamage', value: 1.10 }
|
||||||
},
|
},
|
||||||
execute_25: {
|
overpower_80: {
|
||||||
id: 'execute_25',
|
id: 'overpower_80',
|
||||||
name: 'Executioner',
|
name: 'Overpower',
|
||||||
description: '+50% damage to enemies below 25% HP',
|
description: '+50% damage when mana above 80%',
|
||||||
category: 'special',
|
category: 'special',
|
||||||
baseCapacityCost: 55,
|
baseCapacityCost: 55,
|
||||||
maxStacks: 1,
|
maxStacks: 1,
|
||||||
allowedEquipmentCategories: CASTER_AND_HANDS,
|
allowedEquipmentCategories: CASTER_AND_HANDS,
|
||||||
effect: { type: 'special', specialId: 'executioner' }
|
effect: { type: 'special', specialId: 'overpower' }
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ const COMBAT_TRAIN_TIER1_UPGRADES_L5: SkillUpgradeChoice[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const COMBAT_TRAIN_TIER1_UPGRADES_L10: 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_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_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' } },
|
{ 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' } },
|
||||||
|
|||||||
@@ -98,6 +98,43 @@ export function getFloorElement(floor: number): string {
|
|||||||
return FLOOR_ELEM_CYCLE[(floor - 1) % 8];
|
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<string, string | null>,
|
||||||
|
equipmentInstances: Record<string, EquipmentInstance>
|
||||||
|
): 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 ─────────────────────────────────────────────────
|
// ─── Computed Stats Functions ─────────────────────────────────────────────────
|
||||||
|
|
||||||
// Helper to get effective skill level accounting for tiers
|
// 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)
|
* Compute regen with dynamic special effects (needs current mana, max mana, incursion)
|
||||||
*/
|
*/
|
||||||
export function computeEffectiveRegen(
|
export function computeEffectiveRegen(
|
||||||
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'rawMana' | 'incursionStrength' | 'skillUpgrades' | 'skillTiers'>,
|
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'rawMana' | 'incursionStrength' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
|
||||||
effects?: ComputedEffects
|
effects?: ComputedEffects | UnifiedEffects
|
||||||
): number {
|
): number {
|
||||||
// Base regen from existing function
|
// Base regen from existing function
|
||||||
let regen = computeRegen(state, effects);
|
let regen = computeRegen(state, effects);
|
||||||
@@ -369,7 +406,12 @@ function deductSpellCost(
|
|||||||
function makeInitial(overrides: Partial<GameState> = {}): GameState {
|
function makeInitial(overrides: Partial<GameState> = {}): GameState {
|
||||||
const pu = overrides.prestigeUpgrades || {};
|
const pu = overrides.prestigeUpgrades || {};
|
||||||
const startFloor = 1 + (pu.spireKey || 0) * 2;
|
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<string, { current: number; max: number; unlocked: boolean }> = {};
|
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
|
||||||
Object.keys(ELEMENTS).forEach((k) => {
|
Object.keys(ELEMENTS).forEach((k) => {
|
||||||
@@ -695,18 +737,47 @@ export const useGameStore = create<GameStore>()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combat - uses cast speed and spell casting
|
// Combat - MULTI-SPELL casting from all equipped weapons
|
||||||
let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, castProgress } = state;
|
let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, equipmentSpellStates } = state;
|
||||||
const floorElement = getFloorElement(currentFloor);
|
const floorElement = getFloorElement(currentFloor);
|
||||||
|
|
||||||
if (state.currentAction === 'climb') {
|
if (state.currentAction === 'climb') {
|
||||||
const spellId = state.activeSpell;
|
// Get all spells from equipped caster weapons
|
||||||
const spellDef = SPELLS_DEF[spellId];
|
const activeSpells = getActiveEquipmentSpells(state.equippedInstances, state.equipmentInstances);
|
||||||
|
|
||||||
if (spellDef) {
|
// Initialize spell states if needed
|
||||||
// Compute attack speed from quickCast skill and upgrades
|
if (!equipmentSpellStates) {
|
||||||
const baseAttackSpeed = 1 + (state.skills.quickCast || 0) * 0.05;
|
equipmentSpellStates = [];
|
||||||
const totalAttackSpeed = baseAttackSpeed * effects.attackSpeedMultiplier;
|
}
|
||||||
|
|
||||||
|
// 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)
|
// Get spell cast speed (casts per hour, default 1)
|
||||||
const spellCastSpeed = spellDef.castSpeed || 1;
|
const spellCastSpeed = spellDef.castSpeed || 1;
|
||||||
@@ -715,10 +786,10 @@ export const useGameStore = create<GameStore>()(
|
|||||||
const progressPerTick = HOURS_PER_TICK * spellCastSpeed * totalAttackSpeed;
|
const progressPerTick = HOURS_PER_TICK * spellCastSpeed * totalAttackSpeed;
|
||||||
|
|
||||||
// Accumulate cast progress
|
// Accumulate cast progress
|
||||||
castProgress = (castProgress || 0) + progressPerTick;
|
spellState = { ...spellState, castProgress: spellState.castProgress + progressPerTick };
|
||||||
|
|
||||||
// Process complete casts
|
// Process complete casts
|
||||||
while (castProgress >= 1 && canAffordSpellCost(spellDef.cost, rawMana, elements)) {
|
while (spellState.castProgress >= 1 && canAffordSpellCost(spellDef.cost, rawMana, elements)) {
|
||||||
// Deduct cost
|
// Deduct cost
|
||||||
const afterCost = deductSpellCost(spellDef.cost, rawMana, elements);
|
const afterCost = deductSpellCost(spellDef.cost, rawMana, elements);
|
||||||
rawMana = afterCost.rawMana;
|
rawMana = afterCost.rawMana;
|
||||||
@@ -731,9 +802,9 @@ export const useGameStore = create<GameStore>()(
|
|||||||
// Apply upgrade damage multipliers and bonuses
|
// Apply upgrade damage multipliers and bonuses
|
||||||
dmg = dmg * effects.baseDamageMultiplier + effects.baseDamageBonus;
|
dmg = dmg * effects.baseDamageMultiplier + effects.baseDamageBonus;
|
||||||
|
|
||||||
// Executioner: +100% damage to enemies below 25% HP
|
// Overpower: +50% damage when mana above 80%
|
||||||
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && floorHP / floorMaxHP < 0.25) {
|
if (hasSpecial(effects, SPECIAL_EFFECTS.OVERPOWER) && rawMana >= maxMana * 0.8) {
|
||||||
dmg *= 2;
|
dmg *= 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Berserker: +50% damage when below 50% mana
|
// Berserker: +50% damage when below 50% mana
|
||||||
@@ -745,7 +816,7 @@ export const useGameStore = create<GameStore>()(
|
|||||||
const echoChance = (skills.spellEcho || 0) * 0.1;
|
const echoChance = (skills.spellEcho || 0) * 0.1;
|
||||||
if (Math.random() < echoChance) {
|
if (Math.random() < echoChance) {
|
||||||
dmg *= 2;
|
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
|
// Lifesteal effect
|
||||||
@@ -759,7 +830,7 @@ export const useGameStore = create<GameStore>()(
|
|||||||
floorHP = Math.max(0, floorHP - dmg);
|
floorHP = Math.max(0, floorHP - dmg);
|
||||||
|
|
||||||
// Reduce cast progress by 1 (one cast completed)
|
// Reduce cast progress by 1 (one cast completed)
|
||||||
castProgress -= 1;
|
spellState = { ...spellState, castProgress: spellState.castProgress - 1 };
|
||||||
|
|
||||||
if (floorHP <= 0) {
|
if (floorHP <= 0) {
|
||||||
// Floor cleared
|
// Floor cleared
|
||||||
@@ -781,13 +852,17 @@ export const useGameStore = create<GameStore>()(
|
|||||||
floorHP = floorMaxHP;
|
floorHP = floorMaxHP;
|
||||||
maxFloorReached = Math.max(maxFloorReached, currentFloor);
|
maxFloorReached = Math.max(maxFloorReached, currentFloor);
|
||||||
|
|
||||||
// Reset cast progress on floor change
|
// Reset ALL spell progress on floor change
|
||||||
castProgress = 0;
|
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)
|
// Update the spell state in the array
|
||||||
castProgress = castProgress || 0;
|
equipmentSpellStates = equipmentSpellStates.map(s =>
|
||||||
|
(s.spellId === spellId && s.sourceEquipment === equipmentId) ? spellState : s
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -802,7 +877,7 @@ export const useGameStore = create<GameStore>()(
|
|||||||
floorMaxHP,
|
floorMaxHP,
|
||||||
maxFloorReached,
|
maxFloorReached,
|
||||||
signedPacts,
|
signedPacts,
|
||||||
castProgress,
|
equipmentSpellStates,
|
||||||
incursionStrength,
|
incursionStrength,
|
||||||
currentStudyTarget,
|
currentStudyTarget,
|
||||||
skills,
|
skills,
|
||||||
@@ -837,8 +912,8 @@ export const useGameStore = create<GameStore>()(
|
|||||||
spells,
|
spells,
|
||||||
elements,
|
elements,
|
||||||
log,
|
log,
|
||||||
castProgress,
|
equipmentSpellStates,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -861,7 +936,7 @@ export const useGameStore = create<GameStore>()(
|
|||||||
elements,
|
elements,
|
||||||
unlockedEffects,
|
unlockedEffects,
|
||||||
log,
|
log,
|
||||||
castProgress,
|
equipmentSpellStates,
|
||||||
...craftingUpdates,
|
...craftingUpdates,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
export type ElementCategory = 'base' | 'utility' | 'composite' | 'exotic';
|
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 {
|
export interface ElementDef {
|
||||||
name: string;
|
name: string;
|
||||||
sym: string;
|
sym: string;
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export const SPECIAL_EFFECTS = {
|
|||||||
// Combat special effects
|
// Combat special effects
|
||||||
BATTLE_FURY: 'battleFury', // +10% damage per consecutive hit
|
BATTLE_FURY: 'battleFury', // +10% damage per consecutive hit
|
||||||
ARMOR_PIERCE: 'armorPierce', // Ignore 10% floor defense
|
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
|
BERSERKER: 'berserker', // +50% damage when below 50% mana
|
||||||
COMBO_MASTER: 'comboMaster', // Every 5th attack deals 3x damage
|
COMBO_MASTER: 'comboMaster', // Every 5th attack deals 3x damage
|
||||||
ADRENALINE_RUSH: 'adrenalineRush', // Defeating enemy restores 5% mana
|
ADRENALINE_RUSH: 'adrenalineRush', // Defeating enemy restores 5% mana
|
||||||
@@ -354,9 +354,9 @@ export function computeDynamicDamage(
|
|||||||
damage *= (1 + furyBonus);
|
damage *= (1 + furyBonus);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Executioner: +100% damage to enemies below 25% HP
|
// Overpower: +50% damage when mana above 80%
|
||||||
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && floorHPPct < 0.25) {
|
if (hasSpecial(effects, SPECIAL_EFFECTS.OVERPOWER) && currentMana >= maxMana * 0.8) {
|
||||||
damage *= 2;
|
damage *= 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Berserker: +50% damage when below 50% mana
|
// Berserker: +50% damage when below 50% mana
|
||||||
|
|||||||
109
worklog.md
109
worklog.md
@@ -1,5 +1,37 @@
|
|||||||
# Mana Loop Worklog
|
# 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
|
Task ID: 1
|
||||||
Agent: Main
|
Agent: Main
|
||||||
@@ -200,3 +232,80 @@ Stage Summary:
|
|||||||
- Spells only learnable from equipment and pacts
|
- Spells only learnable from equipment and pacts
|
||||||
- All crafting skills have evolution paths
|
- All crafting skills have evolution paths
|
||||||
- All lint checks pass
|
- 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
|
||||||
|
|||||||
Reference in New Issue
Block a user