pack
This commit is contained in:
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 { 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<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
|
||||
const ELEMENT_ICONS: Record<string, typeof Flame> = {
|
||||
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() {
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-gray-400 game-mono">
|
||||
<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>
|
||||
|
||||
@@ -529,62 +566,74 @@ export default function ManaLoopGame() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Active Spell Card */}
|
||||
{/* Active Spells Card - Shows all spells from equipped weapons */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<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>
|
||||
<CardContent className="space-y-3">
|
||||
{activeSpellDef ? (
|
||||
<>
|
||||
<div className="text-lg font-semibold game-panel-title" style={{ color: activeSpellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[activeSpellDef.elem]?.color }}>
|
||||
{activeSpellDef.name}
|
||||
{activeSpellDef.tier === 0 && <Badge className="ml-2 bg-gray-600 text-gray-200">Basic</Badge>}
|
||||
{activeSpellDef.tier >= 4 && <Badge className="ml-2 bg-amber-600 text-amber-100">Legendary</Badge>}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400 game-mono">
|
||||
⚔️ {fmt(calcDamage(store, store.activeSpell))} dmg •
|
||||
<span style={{ color: getSpellCostColor(activeSpellDef.cost) }}>
|
||||
{' '}{formatSpellCost(activeSpellDef.cost)}
|
||||
</span>
|
||||
{' '}• ⚡ {(activeSpellDef.castSpeed || 1).toFixed(1)} casts/hr
|
||||
</div>
|
||||
|
||||
{/* Cast progress bar when climbing */}
|
||||
{store.currentAction === 'climb' && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs text-gray-400">
|
||||
<span>Cast Progress</span>
|
||||
<span>{((store.castProgress || 0) * 100).toFixed(0)}%</span>
|
||||
{activeEquipmentSpells.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{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 (
|
||||
<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">
|
||||
<div className="text-sm font-semibold game-panel-title" style={{ color: spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color }}>
|
||||
{spellDef.name}
|
||||
{spellDef.tier === 0 && <Badge className="ml-2 bg-gray-600 text-gray-200 text-xs">Basic</Badge>}
|
||||
{spellDef.tier >= 4 && <Badge className="ml-2 bg-amber-600 text-amber-100 text-xs">Legendary</Badge>}
|
||||
</div>
|
||||
<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>
|
||||
<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 className="text-gray-500 text-sm">No spells on equipped weapons. Enchant a staff with spell effects.</div>
|
||||
)}
|
||||
|
||||
{incursionStrength > 0 && (
|
||||
@@ -1444,10 +1493,6 @@ export default function ManaLoopGame() {
|
||||
})()}
|
||||
</span>
|
||||
</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">
|
||||
<span className="text-gray-400">Prestige Mana Well:</span>
|
||||
<span className="text-blue-300">+{fmt((store.prestigeUpgrades.manaWell || 0) * 500)}</span>
|
||||
|
||||
Reference in New Issue
Block a user