From e41a1a3553ced81e80c160a9c73ceac731b11d7c Mon Sep 17 00:00:00 2001 From: Z User Date: Sat, 28 Mar 2026 15:24:09 +0000 Subject: [PATCH] Remove familiar system completely - Delete familiar-slice.ts, familiars.ts, and FamiliarTab.tsx - Remove familiar types from types.ts (FamiliarRole, FamiliarAbilityType, FamiliarAbility, FamiliarDef, FamiliarInstance) - Fix SpireTab spell effects to show valid types (burn, stun, pierce, multicast, buff) instead of invalid lifesteal/freeze - Remove Executioner test from store.test.ts (execute effect was previously removed) - Remove familiar fields from skills.test.ts mock state Familiars detract from the Invoker pact system. The familiar system was incomplete (missing state fields) and referenced removed ability types. --- src/components/game/SpireTab.tsx | 8 +- src/components/game/tabs/FamiliarTab.tsx | 582 ----------------------- src/lib/game/data/familiars.ts | 519 -------------------- src/lib/game/familiar-slice.ts | 367 -------------- src/lib/game/skills.test.ts | 2 - src/lib/game/store.test.ts | 10 - src/lib/game/types.ts | 54 --- 7 files changed, 5 insertions(+), 1537 deletions(-) delete mode 100755 src/components/game/tabs/FamiliarTab.tsx delete mode 100755 src/lib/game/data/familiars.ts delete mode 100755 src/lib/game/familiar-slice.ts diff --git a/src/components/game/SpireTab.tsx b/src/components/game/SpireTab.tsx index 67f3e52..7990b55 100755 --- a/src/components/game/SpireTab.tsx +++ b/src/components/game/SpireTab.tsx @@ -157,9 +157,11 @@ export function SpireTab() {
{activeSpellDef.effects.map((eff, i) => ( - {eff.type === 'lifesteal' && `🩸 ${Math.round(eff.value * 100)}% lifesteal`} - {eff.type === 'burn' && `🔥 Burn`} - {eff.type === 'freeze' && `❄️ Freeze`} + {eff.type === 'burn' && `🔥 Burn ${eff.value}/hr`} + {eff.type === 'stun' && `⚡ Stun ${eff.value}s`} + {eff.type === 'pierce' && `🗡️ Pierce ${Math.round(eff.value * 100)}%`} + {eff.type === 'multicast' && `✨ ${Math.round(eff.value * 100)}% Multicast`} + {eff.type === 'buff' && `💪 Buff`} ))}
diff --git a/src/components/game/tabs/FamiliarTab.tsx b/src/components/game/tabs/FamiliarTab.tsx deleted file mode 100755 index de18311..0000000 --- a/src/components/game/tabs/FamiliarTab.tsx +++ /dev/null @@ -1,582 +0,0 @@ -'use client'; - -import { useMemo, useState } from 'react'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; -import { Progress } from '@/components/ui/progress'; -import { ScrollArea } from '@/components/ui/scroll-area'; -import { Separator } from '@/components/ui/separator'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; -import { Input } from '@/components/ui/input'; -import { - Sparkles, Heart, Zap, Star, Shield, Flame, Droplet, Wind, Mountain, Sun, Moon, Leaf, Skull, - Brain, Link, Wind as Force, Droplets, TreeDeciduous, Hourglass, Gem, CircleDot, Circle, - Sword, Wand2, ShieldCheck, TrendingUp, Clock, Crown -} from 'lucide-react'; -import type { GameState, FamiliarInstance, FamiliarDef, FamiliarAbilityType } from '@/lib/game/types'; -import { FAMILIARS_DEF, getFamiliarXpRequired, getFamiliarAbilityValue } from '@/lib/game/data/familiars'; -import { ELEMENTS } from '@/lib/game/constants'; - -// Element icon mapping -const ELEMENT_ICONS: Record = { - fire: Flame, - water: Droplet, - air: Wind, - earth: Mountain, - light: Sun, - dark: Moon, - life: Leaf, - death: Skull, - mental: Brain, - transference: Link, - force: Force, - blood: Droplets, - metal: Shield, - wood: TreeDeciduous, - sand: Hourglass, - crystal: Gem, - stellar: Star, - void: CircleDot, - raw: Circle, -}; - -// Rarity colors -const RARITY_COLORS: Record = { - common: 'text-gray-400 border-gray-600', - uncommon: 'text-green-400 border-green-600', - rare: 'text-blue-400 border-blue-600', - epic: 'text-purple-400 border-purple-600', - legendary: 'text-amber-400 border-amber-600', -}; - -const RARITY_BG: Record = { - common: 'bg-gray-900/50', - uncommon: 'bg-green-900/20', - rare: 'bg-blue-900/20', - epic: 'bg-purple-900/20', - legendary: 'bg-amber-900/20', -}; - -// Role icons -const ROLE_ICONS: Record = { - combat: Sword, - mana: Sparkles, - support: Heart, - guardian: ShieldCheck, -}; - -// Ability type icons -const ABILITY_ICONS: Record = { - damageBonus: Sword, - manaRegen: Sparkles, - autoGather: Zap, - critChance: Star, - castSpeed: Clock, - manaShield: Shield, - elementalBonus: Flame, - lifeSteal: Heart, - bonusGold: TrendingUp, - autoConvert: Wand2, - thorns: ShieldCheck, -}; - -interface FamiliarTabProps { - store: GameState & { - setActiveFamiliar: (index: number, active: boolean) => void; - setFamiliarNickname: (index: number, nickname: string) => void; - summonFamiliar: (familiarId: string) => void; - upgradeFamiliarAbility: (index: number, abilityType: FamiliarAbilityType) => void; - getActiveFamiliarBonuses: () => ReturnType['getActiveFamiliarBonuses'] extends () => infer R ? R : never; - getAvailableFamiliars: () => string[]; - }; -} - -export function FamiliarTab({ store }: FamiliarTabProps) { - const [selectedFamiliar, setSelectedFamiliar] = useState(null); - const [nicknameInput, setNicknameInput] = useState(''); - - const familiars = store.familiars; - const activeFamiliarSlots = store.activeFamiliarSlots; - const activeCount = familiars.filter(f => f.active).length; - const availableFamiliars = store.getAvailableFamiliars(); - const familiarBonuses = store.getActiveFamiliarBonuses(); - - // Format XP display - const formatXp = (current: number, level: number) => { - const required = getFamiliarXpRequired(level); - return `${current}/${required}`; - }; - - // Get familiar definition - const getFamiliarDef = (instance: FamiliarInstance): FamiliarDef | undefined => { - return FAMILIARS_DEF[instance.familiarId]; - }; - - // Render a single familiar card - const renderFamiliarCard = (instance: FamiliarInstance, index: number) => { - const def = getFamiliarDef(instance); - if (!def) return null; - - const ElementIcon = ELEMENT_ICONS[def.element] || Circle; - const RoleIcon = ROLE_ICONS[def.role] || Sparkles; - const xpRequired = getFamiliarXpRequired(instance.level); - const xpPercent = Math.min(100, (instance.experience / xpRequired) * 100); - const bondPercent = instance.bond; - const isSelected = selectedFamiliar === index; - - return ( - setSelectedFamiliar(isSelected ? null : index)} - > - -
-
-
- -
-
- - {instance.nickname || def.name} - -
- - Lv.{instance.level} - {instance.active && ( - Active - )} -
-
-
- - {def.rarity} - -
-
- - {/* XP Bar */} -
-
- XP - {formatXp(instance.experience, instance.level)} -
- -
- - {/* Bond Bar */} -
-
- - Bond - - {bondPercent.toFixed(0)}% -
- -
- - {/* Abilities Preview */} -
- {instance.abilities.slice(0, 3).map(ability => { - const abilityDef = def.abilities.find(a => a.type === ability.type); - if (!abilityDef) return null; - const AbilityIcon = ABILITY_ICONS[ability.type] || Zap; - const value = getFamiliarAbilityValue(abilityDef, instance.level, ability.level); - - return ( - - - - - {ability.type === 'damageBonus' || ability.type === 'elementalBonus' || - ability.type === 'castSpeed' || ability.type === 'critChance' || - ability.type === 'lifeSteal' || ability.type === 'thorns' || - ability.type === 'bonusGold' - ? `+${value.toFixed(1)}%` - : `+${value.toFixed(1)}`} - - - -

{abilityDef.desc}

-

Level {ability.level}/10

-
-
- ); - })} -
-
-
- ); - }; - - // Render selected familiar details - const renderFamiliarDetails = () => { - if (selectedFamiliar === null || selectedFamiliar >= familiars.length) return null; - - const instance = familiars[selectedFamiliar]; - const def = getFamiliarDef(instance); - if (!def) return null; - - const ElementIcon = ELEMENT_ICONS[def.element] || Circle; - - return ( - - -
- Familiar Details - -
-
- - {/* Name and nickname */} -
-
-
- -
-
-

- {def.name} -

- {instance.nickname && ( -

"{instance.nickname}"

- )} -
-
- - {/* Nickname input */} -
- setNicknameInput(e.target.value)} - placeholder="Set nickname..." - className="h-8 text-sm bg-gray-800 border-gray-600" - /> - -
-
- - {/* Description */} -
- {def.desc} -
- - {/* Stats */} -
-
- Level: - {instance.level}/100 -
-
- Bond: - {instance.bond.toFixed(0)}% -
-
- Role: - {def.role} -
-
- Element: - {def.element} -
-
- - - - {/* Abilities */} -
-

Abilities

- {instance.abilities.map(ability => { - const abilityDef = def.abilities.find(a => a.type === ability.type); - if (!abilityDef) return null; - const AbilityIcon = ABILITY_ICONS[ability.type] || Zap; - const value = getFamiliarAbilityValue(abilityDef, instance.level, ability.level); - const upgradeCost = ability.level * 100; - const canUpgrade = instance.experience >= upgradeCost && ability.level < 10; - - return ( -
-
-
- - - {ability.type.replace(/([A-Z])/g, ' $1').trim()} - -
- Lv.{ability.level}/10 -
-

{abilityDef.desc}

-
- - Current: +{value.toFixed(2)} - {ability.type === 'damageBonus' || ability.type === 'elementalBonus' || - ability.type === 'castSpeed' || ability.type === 'critChance' || - ability.type === 'lifeSteal' || ability.type === 'thorns' || - ability.type === 'bonusGold' ? '%' : ''} - - {ability.level < 10 && ( - - )} -
-
- ); - })} -
- - {/* Activate/Deactivate */} - - - {/* Flavor text */} - {def.flavorText && ( -

- "{def.flavorText}" -

- )} -
-
- ); - }; - - // Render summonable familiars - const renderSummonableFamiliars = () => { - if (availableFamiliars.length === 0) return null; - - return ( - - - - - Available to Summon ({availableFamiliars.length}) - - - - -
- {availableFamiliars.map(familiarId => { - const def = FAMILIARS_DEF[familiarId]; - if (!def) return null; - - const ElementIcon = ELEMENT_ICONS[def.element] || Circle; - const RoleIcon = ROLE_ICONS[def.role] || Sparkles; - - return ( -
-
- -
-
- {def.name} -
-
- - {def.role} -
-
-
- -
- ); - })} -
-
-
-
- ); - }; - - // Render active bonuses - const renderActiveBonuses = () => { - const hasBonuses = Object.entries(familiarBonuses).some(([key, value]) => { - if (key === 'damageMultiplier' || key === 'castSpeedMultiplier' || - key === 'elementalDamageMultiplier' || key === 'insightMultiplier') { - return value > 1; - } - return value > 0; - }); - - if (!hasBonuses) return null; - - return ( - - - - - Active Familiar Bonuses - - - -
- {familiarBonuses.damageMultiplier > 1 && ( -
- - +{((familiarBonuses.damageMultiplier - 1) * 100).toFixed(0)}% DMG -
- )} - {familiarBonuses.manaRegenBonus > 0 && ( -
- - +{familiarBonuses.manaRegenBonus.toFixed(1)} regen -
- )} - {familiarBonuses.autoGatherRate > 0 && ( -
- - +{familiarBonuses.autoGatherRate.toFixed(1)}/hr gather -
- )} - {familiarBonuses.critChanceBonus > 0 && ( -
- - +{familiarBonuses.critChanceBonus.toFixed(1)}% crit -
- )} - {familiarBonuses.castSpeedMultiplier > 1 && ( -
- - +{((familiarBonuses.castSpeedMultiplier - 1) * 100).toFixed(0)}% speed -
- )} - {familiarBonuses.elementalDamageMultiplier > 1 && ( -
- - +{((familiarBonuses.elementalDamageMultiplier - 1) * 100).toFixed(0)}% elem -
- )} - {familiarBonuses.lifeStealPercent > 0 && ( -
- - +{familiarBonuses.lifeStealPercent.toFixed(0)}% lifesteal -
- )} - {familiarBonuses.insightMultiplier > 1 && ( -
- - +{((familiarBonuses.insightMultiplier - 1) * 100).toFixed(0)}% insight -
- )} -
-
-
- ); - }; - - return ( - -
- {/* Owned Familiars */} - - -
- - - Your Familiars ({familiars.length}) - -
- Active Slots: {activeCount}/{activeFamiliarSlots} -
-
-
- - {familiars.length > 0 ? ( -
- {familiars.map((instance, index) => renderFamiliarCard(instance, index))} -
- ) : ( -
- No familiars yet. Progress through the game to summon companions! -
- )} -
-
- - {/* Active Bonuses */} - {renderActiveBonuses()} - - {/* Selected Familiar Details */} - {renderFamiliarDetails()} - - {/* Summonable Familiars */} - {renderSummonableFamiliars()} - - {/* Familiar Guide */} - - - - - Familiar Guide - - - -
-
-

Acquiring Familiars

-

Familiars become available to summon as you progress through floors, gather mana, and sign pacts with guardians. Higher rarity familiars are unlocked later.

-
-
-

Leveling & Bond

-

Active familiars gain XP from combat, gathering, and time. Higher bond increases their power and XP gain. Upgrade abilities using XP to boost their effects.

-
-
-

Roles

-

- Combat - Damage and crit bonuses
- Mana - Regeneration and auto-gathering
- Support - Speed and utility
- Guardian - Defense and shields -

-
-
-

Active Slots

-

You can have 1 familiar active by default. Upgrade through prestige to unlock more active slots for stacking bonuses.

-
-
-
-
-
-
- ); -} diff --git a/src/lib/game/data/familiars.ts b/src/lib/game/data/familiars.ts deleted file mode 100755 index 2641d55..0000000 --- a/src/lib/game/data/familiars.ts +++ /dev/null @@ -1,519 +0,0 @@ -// ─── Familiar Definitions ─────────────────────────────────────────────────────── -// Magical companions that provide passive bonuses and active assistance - -import type { FamiliarDef, FamiliarAbility } from '../types'; - -// ─── Familiar Abilities ───────────────────────────────────────────────────────── - -const ABILITIES = { - // Combat abilities - damageBonus: (base: number, scaling: number): FamiliarAbility => ({ - type: 'damageBonus', - baseValue: base, - scalingPerLevel: scaling, - desc: `+${base}% damage (+${scaling}% per level)`, - }), - - critChance: (base: number, scaling: number): FamiliarAbility => ({ - type: 'critChance', - baseValue: base, - scalingPerLevel: scaling, - desc: `+${base}% crit chance (+${scaling}% per level)`, - }), - - critDamage: (base: number, scaling: number): FamiliarAbility => ({ - type: 'critDamage', - baseValue: base, - scalingPerLevel: scaling, - desc: `+${base}% crit damage (+${scaling}% per level)`, - }), - - castSpeed: (base: number, scaling: number): FamiliarAbility => ({ - type: 'castSpeed', - baseValue: base, - scalingPerLevel: scaling, - desc: `+${base}% cast speed (+${scaling}% per level)`, - }), - - elementalBonus: (base: number, scaling: number): FamiliarAbility => ({ - type: 'elementalBonus', - baseValue: base, - scalingPerLevel: scaling, - desc: `+${base}% elemental damage (+${scaling}% per level)`, - }), - - guardianDamage: (base: number, scaling: number): FamiliarAbility => ({ - type: 'guardianDamage', - baseValue: base, - scalingPerLevel: scaling, - desc: `+${base}% damage to guardians (+${scaling}% per level)`, - }), - - manaSiphon: (base: number, scaling: number): FamiliarAbility => ({ - type: 'manaSiphon', - baseValue: base, - scalingPerLevel: scaling, - desc: `Restore ${base}% of damage as mana (+${scaling}% per level)`, - }), - - barrierBreaker: (base: number, scaling: number): FamiliarAbility => ({ - type: 'barrierBreaker', - baseValue: base, - scalingPerLevel: scaling, - desc: `+${base}% damage to barriers (+${scaling}% per level)`, - }), - - // Mana abilities - manaRegen: (base: number, scaling: number): FamiliarAbility => ({ - type: 'manaRegen', - baseValue: base, - scalingPerLevel: scaling, - desc: `+${base} mana regen (+${scaling} per level)`, - }), - - autoGather: (base: number, scaling: number): FamiliarAbility => ({ - type: 'autoGather', - baseValue: base, - scalingPerLevel: scaling, - desc: `Auto-gather ${base} mana/hour (+${scaling} per level)`, - }), - - autoConvert: (base: number, scaling: number): FamiliarAbility => ({ - type: 'autoConvert', - baseValue: base, - scalingPerLevel: scaling, - desc: `Auto-convert ${base} mana/hour (+${scaling} per level)`, - }), - - maxManaBonus: (base: number, scaling: number): FamiliarAbility => ({ - type: 'maxManaBonus', - baseValue: base, - scalingPerLevel: scaling, - desc: `+${base} max mana (+${scaling} per level)`, - }), - - // Support abilities - bonusGold: (base: number, scaling: number): FamiliarAbility => ({ - type: 'bonusGold', - baseValue: base, - scalingPerLevel: scaling, - desc: `+${base}% insight gain (+${scaling}% per level)`, - }), - - studySpeed: (base: number, scaling: number): FamiliarAbility => ({ - type: 'studySpeed', - baseValue: base, - scalingPerLevel: scaling, - desc: `+${base}% study speed (+${scaling}% per level)`, - }), -}; - -// ─── Familiar Definitions ─────────────────────────────────────────────────────── - -export const FAMILIARS_DEF: Record = { - // === COMMON FAMILIARS (Tier 1) === - - // Mana Wisps - Basic mana helpers - manaWisp: { - id: 'manaWisp', - name: 'Mana Wisp', - desc: 'A gentle spirit of pure mana that drifts lazily through the air.', - role: 'mana', - element: 'raw', - rarity: 'common', - abilities: [ - ABILITIES.manaRegen(0.5, 0.1), - ], - baseStats: { power: 10, bond: 15 }, - unlockCondition: { type: 'mana', value: 100 }, - flavorText: 'It hums with quiet contentment, barely visible in dim light.', - }, - - fireSpark: { - id: 'fireSpark', - name: 'Fire Spark', - desc: 'A tiny ember given life, crackling with barely contained energy.', - role: 'combat', - element: 'fire', - rarity: 'common', - abilities: [ - ABILITIES.damageBonus(2, 0.5), - ], - baseStats: { power: 12, bond: 10 }, - unlockCondition: { type: 'floor', value: 5 }, - flavorText: 'It bounces excitedly, leaving scorch marks on everything it touches.', - }, - - waterDroplet: { - id: 'waterDroplet', - name: 'Water Droplet', - desc: 'A perfect sphere of living water that never seems to evaporate.', - role: 'support', - element: 'water', - rarity: 'common', - abilities: [ - ABILITIES.manaRegen(0.3, 0.1), - ABILITIES.manaSiphon(2, 0.5), - ], - baseStats: { power: 8, bond: 12 }, - unlockCondition: { type: 'floor', value: 3 }, - flavorText: 'Ripples spread across its surface with each spell you cast.', - }, - - earthPebble: { - id: 'earthPebble', - name: 'Earth Pebble', - desc: 'A small stone with a surprisingly friendly personality.', - role: 'guardian', - element: 'earth', - rarity: 'common', - abilities: [ - ABILITIES.guardianDamage(3, 0.8), - ], - baseStats: { power: 15, bond: 8 }, - unlockCondition: { type: 'floor', value: 8 }, - flavorText: 'It occasionally rolls itself to a new position when bored.', - }, - - // === UNCOMMON FAMILIARS (Tier 2) === - - flameImp: { - id: 'flameImp', - name: 'Flame Imp', - desc: 'A mischievous fire spirit that delights in destruction.', - role: 'combat', - element: 'fire', - rarity: 'uncommon', - abilities: [ - ABILITIES.damageBonus(4, 0.8), - ABILITIES.elementalBonus(3, 0.6), - ], - baseStats: { power: 25, bond: 12 }, - unlockCondition: { type: 'floor', value: 15 }, - flavorText: 'It cackles with glee whenever you defeat an enemy.', - }, - - windSylph: { - id: 'windSylph', - name: 'Wind Sylph', - desc: 'An airy spirit that moves like a gentle breeze.', - role: 'support', - element: 'air', - rarity: 'uncommon', - abilities: [ - ABILITIES.castSpeed(3, 0.6), - ], - baseStats: { power: 20, bond: 15 }, - unlockCondition: { type: 'floor', value: 12 }, - flavorText: 'Its laughter sounds like wind chimes in a storm.', - }, - - manaSprite: { - id: 'manaSprite', - name: 'Mana Sprite', - desc: 'A more evolved mana spirit with a playful nature.', - role: 'mana', - element: 'raw', - rarity: 'uncommon', - abilities: [ - ABILITIES.manaRegen(1, 0.2), - ABILITIES.autoGather(2, 0.5), - ], - baseStats: { power: 18, bond: 18 }, - unlockCondition: { type: 'mana', value: 1000 }, - flavorText: 'It sometimes tickles your ear with invisible hands.', - }, - - crystalGolem: { - id: 'crystalGolem', - name: 'Crystal Golem', - desc: 'A small construct made of crystallized mana.', - role: 'guardian', - element: 'crystal', - rarity: 'uncommon', - abilities: [ - ABILITIES.guardianDamage(5, 1), - ABILITIES.barrierBreaker(8, 1.5), - ], - baseStats: { power: 30, bond: 10 }, - unlockCondition: { type: 'floor', value: 20 }, - flavorText: 'Light refracts through its body in mesmerizing patterns.', - }, - - // === RARE FAMILIARS (Tier 3) === - - phoenixHatchling: { - id: 'phoenixHatchling', - name: 'Phoenix Hatchling', - desc: 'A young phoenix, still learning to control its flames.', - role: 'combat', - element: 'fire', - rarity: 'rare', - abilities: [ - ABILITIES.damageBonus(6, 1.2), - ABILITIES.critDamage(15, 3), - ], - baseStats: { power: 40, bond: 15 }, - unlockCondition: { type: 'floor', value: 30 }, - flavorText: 'Tiny flames dance around its feathers as it practices flying.', - }, - - frostWisp: { - id: 'frostWisp', - name: 'Frost Wisp', - desc: 'A spirit of eternal winter, beautiful and deadly.', - role: 'combat', - element: 'water', - rarity: 'rare', - abilities: [ - ABILITIES.elementalBonus(8, 1.5), - ABILITIES.castSpeed(4, 0.8), - ], - baseStats: { power: 35, bond: 12 }, - unlockCondition: { type: 'floor', value: 25 }, - flavorText: 'Frost patterns appear on surfaces wherever it lingers.', - }, - - manaElemental: { - id: 'manaElemental', - name: 'Mana Elemental', - desc: 'A concentrated form of pure magical energy.', - role: 'mana', - element: 'raw', - rarity: 'rare', - abilities: [ - ABILITIES.manaRegen(2, 0.4), - ABILITIES.autoGather(5, 1), - ABILITIES.autoConvert(2, 0.5), - ], - baseStats: { power: 30, bond: 20 }, - unlockCondition: { type: 'mana', value: 5000 }, - flavorText: 'Reality seems to bend slightly around its fluctuating form.', - }, - - shieldGuardian: { - id: 'shieldGuardian', - name: 'Stone Guardian', - desc: 'A loyal protector carved from enchanted stone.', - role: 'guardian', - element: 'earth', - rarity: 'rare', - abilities: [ - ABILITIES.guardianDamage(8, 1.5), - ABILITIES.barrierBreaker(12, 2), - ], - baseStats: { power: 50, bond: 8 }, - unlockCondition: { type: 'floor', value: 35 }, - flavorText: 'It stands motionless for hours, then suddenly moves to strike.', - }, - - // === EPIC FAMILIARS (Tier 4) === - - infernoDrake: { - id: 'infernoDrake', - name: 'Inferno Drake', - desc: 'A small dragon wreathed in eternal flames.', - role: 'combat', - element: 'fire', - rarity: 'epic', - abilities: [ - ABILITIES.damageBonus(10, 2), - ABILITIES.elementalBonus(12, 2), - ABILITIES.critChance(3, 0.6), - ], - baseStats: { power: 60, bond: 12 }, - unlockCondition: { type: 'floor', value: 50 }, - flavorText: 'Smoke occasionally drifts from its nostrils as it dreams of conquest.', - }, - - starlightSerpent: { - id: 'starlightSerpent', - name: 'Starlight Serpent', - desc: 'A serpentine creature formed from captured starlight.', - role: 'support', - element: 'stellar', - rarity: 'epic', - abilities: [ - ABILITIES.castSpeed(8, 1.5), - ABILITIES.bonusGold(5, 1), - ABILITIES.manaRegen(1.5, 0.3), - ], - baseStats: { power: 45, bond: 25 }, - unlockCondition: { type: 'floor', value: 45 }, - flavorText: 'It traces constellations in the air with its glowing body.', - }, - - voidWalker: { - id: 'voidWalker', - name: 'Void Walker', - desc: 'A being that exists partially outside normal reality.', - role: 'mana', - element: 'void', - rarity: 'epic', - abilities: [ - ABILITIES.manaRegen(3, 0.6), - ABILITIES.autoGather(10, 2), - ABILITIES.maxManaBonus(50, 10), - ], - baseStats: { power: 55, bond: 15 }, - unlockCondition: { type: 'floor', value: 55 }, - flavorText: 'It sometimes disappears entirely, only to reappear moments later.', - }, - - ancientGolem: { - id: 'ancientGolem', - name: 'Ancient Golem', - desc: 'A construct from a forgotten age, still following its prime directive.', - role: 'guardian', - element: 'earth', - rarity: 'epic', - abilities: [ - ABILITIES.guardianDamage(15, 3), - ABILITIES.barrierBreaker(20, 4), - ABILITIES.damageBonus(5, 1), - ], - baseStats: { power: 80, bond: 6 }, - unlockCondition: { type: 'floor', value: 60 }, - flavorText: 'Ancient runes glow faintly across its weathered surface.', - }, - - // === LEGENDARY FAMILIARS (Tier 5) === - - primordialPhoenix: { - id: 'primordialPhoenix', - name: 'Primordial Phoenix', - desc: 'An ancient fire bird, reborn countless times through the ages.', - role: 'combat', - element: 'fire', - rarity: 'legendary', - abilities: [ - ABILITIES.damageBonus(15, 3), - ABILITIES.elementalBonus(20, 4), - ABILITIES.critDamage(30, 5), - ABILITIES.critChance(5, 1), - ], - baseStats: { power: 100, bond: 20 }, - unlockCondition: { type: 'pact', value: 25 }, // Guardian floor 25 - flavorText: 'Its eyes hold the wisdom of a thousand lifetimes.', - }, - - leviathanSpawn: { - id: 'leviathanSpawn', - name: 'Leviathan Spawn', - desc: 'The offspring of an ancient sea god, still growing into its power.', - role: 'mana', - element: 'water', - rarity: 'legendary', - abilities: [ - ABILITIES.manaRegen(5, 1), - ABILITIES.autoGather(20, 4), - ABILITIES.autoConvert(8, 1.5), - ABILITIES.maxManaBonus(100, 20), - ], - baseStats: { power: 90, bond: 18 }, - unlockCondition: { type: 'pact', value: 50 }, - flavorText: 'The air around it always smells of salt and deep ocean.', - }, - - celestialGuardian: { - id: 'celestialGuardian', - name: 'Celestial Guardian', - desc: 'A divine protector sent by powers beyond mortal comprehension.', - role: 'guardian', - element: 'light', - rarity: 'legendary', - abilities: [ - ABILITIES.guardianDamage(25, 5), - ABILITIES.barrierBreaker(30, 6), - ABILITIES.damageBonus(10, 2), - ABILITIES.critChance(8, 1.5), - ], - baseStats: { power: 120, bond: 12 }, - unlockCondition: { type: 'pact', value: 75 }, - flavorText: 'It radiates an aura of absolute judgment.', - }, - - voidEmperor: { - id: 'voidEmperor', - name: 'Void Emperor', - desc: 'A ruler from the spaces between dimensions, bound to your service.', - role: 'support', - element: 'void', - rarity: 'legendary', - abilities: [ - ABILITIES.castSpeed(15, 3), - ABILITIES.bonusGold(15, 3), - ABILITIES.manaRegen(4, 0.8), - ABILITIES.critChance(8, 1.5), - ], - baseStats: { power: 85, bond: 25 }, - unlockCondition: { type: 'floor', value: 90 }, - flavorText: 'It regards reality with the detached interest of a god.', - }, -}; - -// ─── Helper Functions ─────────────────────────────────────────────────────────── - -// Get XP required for next familiar level -export function getFamiliarXpRequired(level: number): number { - // Exponential scaling: 100 * 1.5^(level-1) - return Math.floor(100 * Math.pow(1.5, level - 1)); -} - -// Get bond required for next bond level (1-100) -export function getBondRequired(currentBond: number): number { - // Linear scaling, every 10 bond requires more time - const bondTier = Math.floor(currentBond / 10); - return 100 + bondTier * 50; // Base 100, +50 per tier -} - -// Calculate familiar's ability value at given level and ability level -export function getFamiliarAbilityValue( - ability: FamiliarAbility, - familiarLevel: number, - abilityLevel: number -): number { - // Base value + (familiar level bonus) + (ability level bonus) - const familiarBonus = Math.floor(familiarLevel / 10) * ability.scalingPerLevel; - const abilityBonus = (abilityLevel - 1) * ability.scalingPerLevel * 2; - return ability.baseValue + familiarBonus + abilityBonus; -} - -// Get all familiars of a specific rarity -export function getFamiliarsByRarity(rarity: FamiliarDef['rarity']): FamiliarDef[] { - return Object.values(FAMILIARS_DEF).filter(f => f.rarity === rarity); -} - -// Get all familiars of a specific role -export function getFamiliarsByRole(role: FamiliarRole): FamiliarDef[] { - return Object.values(FAMILIARS_DEF).filter(f => f.role === role); -} - -// Check if player meets unlock condition for a familiar -export function canUnlockFamiliar( - familiar: FamiliarDef, - maxFloor: number, - signedPacts: number[], - totalManaGathered: number, - skillsLearned: number -): boolean { - if (!familiar.unlockCondition) return true; - - const { type, value } = familiar.unlockCondition; - - switch (type) { - case 'floor': - return maxFloor >= value; - case 'pact': - return signedPacts.includes(value); - case 'mana': - return totalManaGathered >= value; - case 'study': - return skillsLearned >= value; - default: - return false; - } -} - -// Starting familiar (given to new players) -export const STARTING_FAMILIAR = 'manaWisp'; diff --git a/src/lib/game/familiar-slice.ts b/src/lib/game/familiar-slice.ts deleted file mode 100755 index 8f06c69..0000000 --- a/src/lib/game/familiar-slice.ts +++ /dev/null @@ -1,367 +0,0 @@ -// ─── Familiar Slice ───────────────────────────────────────────────────────────── -// Actions and computations for the familiar system - -import type { GameState, FamiliarInstance, FamiliarAbilityType } from './types'; -import { FAMILIARS_DEF, getFamiliarXpRequired, getFamiliarAbilityValue, canUnlockFamiliar, STARTING_FAMILIAR } from './data/familiars'; -import { HOURS_PER_TICK } from './constants'; - -// ─── Familiar Actions Interface ───────────────────────────────────────────────── - -export interface FamiliarActions { - // Summoning and management - summonFamiliar: (familiarId: string) => void; - setActiveFamiliar: (instanceIndex: number, active: boolean) => void; - setFamiliarNickname: (instanceIndex: number, nickname: string) => void; - - // Progression - gainFamiliarXp: (amount: number, source: 'combat' | 'gather' | 'meditate' | 'study' | 'time') => void; - upgradeFamiliarAbility: (instanceIndex: number, abilityType: FamiliarAbilityType) => void; - - // Computation - getActiveFamiliarBonuses: () => FamiliarBonuses; - getAvailableFamiliars: () => string[]; -} - -// ─── Computed Bonuses ─────────────────────────────────────────────────────────── - -export interface FamiliarBonuses { - damageMultiplier: number; - manaRegenBonus: number; - autoGatherRate: number; - autoConvertRate: number; - critChanceBonus: number; - castSpeedMultiplier: number; - elementalDamageMultiplier: number; - lifeStealPercent: number; - thornsPercent: number; - insightMultiplier: number; - manaShieldAmount: number; -} - -export const DEFAULT_FAMILIAR_BONUSES: FamiliarBonuses = { - damageMultiplier: 1, - manaRegenBonus: 0, - autoGatherRate: 0, - autoConvertRate: 0, - critChanceBonus: 0, - castSpeedMultiplier: 1, - elementalDamageMultiplier: 1, - lifeStealPercent: 0, - thornsPercent: 0, - insightMultiplier: 1, - manaShieldAmount: 0, -}; - -// ─── Familiar Slice Factory ───────────────────────────────────────────────────── - -export function createFamiliarSlice( - set: (fn: (state: GameState) => Partial) => void, - get: () => GameState -): FamiliarActions { - return { - // Summon a new familiar - summonFamiliar: (familiarId: string) => { - const state = get(); - const familiarDef = FAMILIARS_DEF[familiarId]; - if (!familiarDef) return; - - // Check if already owned - if (state.familiars.some(f => f.familiarId === familiarId)) return; - - // Check unlock condition - if (!canUnlockFamiliar( - familiarDef, - state.maxFloorReached, - state.signedPacts, - state.totalManaGathered, - Object.keys(state.skills).length - )) return; - - // Create new familiar instance - const newInstance: FamiliarInstance = { - familiarId, - level: 1, - bond: 0, - experience: 0, - abilities: familiarDef.abilities.map(a => ({ - type: a.type, - level: 1, - })), - active: false, - }; - - // Add to familiars list - set((s) => ({ - familiars: [...s.familiars, newInstance], - log: [`🌟 ${familiarDef.name} has answered your call!`, ...s.log.slice(0, 49)], - })); - }, - - // Set a familiar as active/inactive - setActiveFamiliar: (instanceIndex: number, active: boolean) => { - const state = get(); - if (instanceIndex < 0 || instanceIndex >= state.familiars.length) return; - - const activeCount = state.familiars.filter(f => f.active).length; - - // Check if we have slots available - if (active && activeCount >= state.activeFamiliarSlots) { - // Deactivate another familiar first - const newFamiliars = [...state.familiars]; - const activeIndex = newFamiliars.findIndex(f => f.active); - if (activeIndex >= 0) { - newFamiliars[activeIndex] = { ...newFamiliars[activeIndex], active: false }; - } - newFamiliars[instanceIndex] = { ...newFamiliars[instanceIndex], active }; - set({ familiars: newFamiliars }); - } else { - // Just toggle the familiar - const newFamiliars = [...state.familiars]; - newFamiliars[instanceIndex] = { ...newFamiliars[instanceIndex], active }; - set({ familiars: newFamiliars }); - } - }, - - // Set a familiar's nickname - setFamiliarNickname: (instanceIndex: number, nickname: string) => { - const state = get(); - if (instanceIndex < 0 || instanceIndex >= state.familiars.length) return; - - const newFamiliars = [...state.familiars]; - newFamiliars[instanceIndex] = { - ...newFamiliars[instanceIndex], - nickname: nickname || undefined - }; - set({ familiars: newFamiliars }); - }, - - // Grant XP to all active familiars - gainFamiliarXp: (amount: number, _source: 'combat' | 'gather' | 'meditate' | 'study' | 'time') => { - const state = get(); - if (state.familiars.length === 0) return; - - const newFamiliars = [...state.familiars]; - let leveled = false; - - for (let i = 0; i < newFamiliars.length; i++) { - const familiar = newFamiliars[i]; - if (!familiar.active) continue; - - const def = FAMILIARS_DEF[familiar.familiarId]; - if (!def) continue; - - // Apply bond multiplier to XP gain - const bondMultiplier = 1 + (familiar.bond / 100); - const xpGain = Math.floor(amount * bondMultiplier); - - let newExp = familiar.experience + xpGain; - let newLevel = familiar.level; - - // Check for level ups - while (newLevel < 100 && newExp >= getFamiliarXpRequired(newLevel)) { - newExp -= getFamiliarXpRequired(newLevel); - newLevel++; - leveled = true; - } - - // Gain bond passively - const newBond = Math.min(100, familiar.bond + 0.01); - - newFamiliars[i] = { - ...familiar, - level: newLevel, - experience: newExp, - bond: newBond, - }; - } - - set({ - familiars: newFamiliars, - totalFamiliarXpEarned: state.totalFamiliarXpEarned + amount, - ...(leveled ? { log: ['📈 Your familiar has grown stronger!', ...state.log.slice(0, 49)] } : {}), - }); - }, - - // Upgrade a familiar's ability - upgradeFamiliarAbility: (instanceIndex: number, abilityType: FamiliarAbilityType) => { - const state = get(); - if (instanceIndex < 0 || instanceIndex >= state.familiars.length) return; - - const familiar = state.familiars[instanceIndex]; - const def = FAMILIARS_DEF[familiar.familiarId]; - if (!def) return; - - // Find the ability - const abilityIndex = familiar.abilities.findIndex(a => a.type === abilityType); - if (abilityIndex < 0) return; - - const ability = familiar.abilities[abilityIndex]; - if (ability.level >= 10) return; // Max level - - // Cost: level * 100 XP - const cost = ability.level * 100; - if (familiar.experience < cost) return; - - // Upgrade - const newAbilities = [...familiar.abilities]; - newAbilities[abilityIndex] = { ...ability, level: ability.level + 1 }; - - const newFamiliars = [...state.familiars]; - newFamiliars[instanceIndex] = { - ...familiar, - abilities: newAbilities, - experience: familiar.experience - cost, - }; - - set({ familiars: newFamiliars }); - }, - - // Get total bonuses from active familiars - getActiveFamiliarBonuses: (): FamiliarBonuses => { - const state = get(); - const bonuses = { ...DEFAULT_FAMILIAR_BONUSES }; - - for (const familiar of state.familiars) { - if (!familiar.active) continue; - - const def = FAMILIARS_DEF[familiar.familiarId]; - if (!def) continue; - - // Bond multiplier: up to 50% bonus at max bond - const bondMultiplier = 1 + (familiar.bond / 200); - - for (const abilityInst of familiar.abilities) { - const abilityDef = def.abilities.find(a => a.type === abilityInst.type); - if (!abilityDef) continue; - - const value = getFamiliarAbilityValue(abilityDef, familiar.level, abilityInst.level) * bondMultiplier; - - switch (abilityInst.type) { - case 'damageBonus': - bonuses.damageMultiplier += value / 100; - break; - case 'manaRegen': - bonuses.manaRegenBonus += value; - break; - case 'autoGather': - bonuses.autoGatherRate += value; - break; - case 'autoConvert': - bonuses.autoConvertRate += value; - break; - case 'critChance': - bonuses.critChanceBonus += value; - break; - case 'castSpeed': - bonuses.castSpeedMultiplier += value / 100; - break; - case 'elementalBonus': - bonuses.elementalDamageMultiplier += value / 100; - break; - case 'lifeSteal': - bonuses.lifeStealPercent += value; - break; - case 'thorns': - bonuses.thornsPercent += value; - break; - case 'bonusGold': - bonuses.insightMultiplier += value / 100; - break; - case 'manaShield': - bonuses.manaShieldAmount += value; - break; - } - } - } - - return bonuses; - }, - - // Get list of available (unlocked but not owned) familiars - getAvailableFamiliars: (): string[] => { - const state = get(); - const owned = new Set(state.familiars.map(f => f.familiarId)); - - return Object.values(FAMILIARS_DEF) - .filter(f => - !owned.has(f.id) && - canUnlockFamiliar( - f, - state.maxFloorReached, - state.signedPacts, - state.totalManaGathered, - Object.keys(state.skills).length - ) - ) - .map(f => f.id); - }, - }; -} - -// ─── Familiar Tick Processing ─────────────────────────────────────────────────── - -// Process familiar-related tick effects (called from main tick) -export function processFamiliarTick( - state: Pick, - familiarBonuses: FamiliarBonuses -): { rawMana: number; elements: GameState['elements']; totalManaGathered: number; gatherLog?: string } { - let rawMana = state.rawMana; - let elements = state.elements; - let totalManaGathered = state.totalManaGathered; - let gatherLog: string | undefined; - - // Auto-gather from familiars - if (familiarBonuses.autoGatherRate > 0) { - const gathered = familiarBonuses.autoGatherRate * HOURS_PER_TICK; - rawMana += gathered; - totalManaGathered += gathered; - if (gathered >= 1) { - gatherLog = `✨ Familiars gathered ${Math.floor(gathered)} mana`; - } - } - - // Auto-convert from familiars - if (familiarBonuses.autoConvertRate > 0) { - const convertAmount = Math.min( - familiarBonuses.autoConvertRate * HOURS_PER_TICK, - Math.floor(rawMana / 5) // 5 raw mana per element - ); - - if (convertAmount > 0) { - // Find unlocked elements with space - const unlockedElements = Object.entries(elements) - .filter(([, e]) => e.unlocked && e.current < e.max) - .sort((a, b) => (b[1].max - b[1].current) - (a[1].max - a[1].current)); - - if (unlockedElements.length > 0) { - const [targetId, targetState] = unlockedElements[0]; - const canConvert = Math.min(convertAmount, targetState.max - targetState.current); - rawMana -= canConvert * 5; - elements = { - ...elements, - [targetId]: { ...targetState, current: targetState.current + canConvert }, - }; - } - } - } - - return { rawMana, elements, totalManaGathered, gatherLog }; -} - -// Grant starting familiar to new players -export function grantStartingFamiliar(): FamiliarInstance[] { - const starterDef = FAMILIARS_DEF[STARTING_FAMILIAR]; - if (!starterDef) return []; - - return [{ - familiarId: STARTING_FAMILIAR, - level: 1, - bond: 0, - experience: 0, - abilities: starterDef.abilities.map(a => ({ - type: a.type, - level: 1, - })), - active: true, // Start with familiar active - }]; -} diff --git a/src/lib/game/skills.test.ts b/src/lib/game/skills.test.ts index 405c0c5..db7cd75 100755 --- a/src/lib/game/skills.test.ts +++ b/src/lib/game/skills.test.ts @@ -102,8 +102,6 @@ function createMockState(overrides: Partial = {}): GameState { containmentWards: 0, log: [], loopInsight: 0, - familiars: [], - activeFamiliarSlots: 1, ...overrides, }; } diff --git a/src/lib/game/store.test.ts b/src/lib/game/store.test.ts index 26cc386..f7a1ea5 100755 --- a/src/lib/game/store.test.ts +++ b/src/lib/game/store.test.ts @@ -1972,16 +1972,6 @@ describe('Special Effect Application', () => { }); describe('Combat Special Effects', () => { - it('should recognize Executioner special effect', () => { - const state = createMockState({ - skills: { combatTrain: 10 }, - skillUpgrades: { combatTrain: ['ct_t1_l10_execute'] } - }); - - const effects = computeEffects(state.skillUpgrades, state.skillTiers || {}); - expect(hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER)).toBe(true); - }); - it('should recognize Berserker special effect', () => { const state = createMockState({ skills: { combatTrain: 10 }, diff --git a/src/lib/game/types.ts b/src/lib/game/types.ts index 05c5ae7..e177113 100755 --- a/src/lib/game/types.ts +++ b/src/lib/game/types.ts @@ -360,60 +360,6 @@ export interface ClearedFloors { [floor: number]: boolean; } -// ─── Familiar System ───────────────────────────────────────────────────────── - -// Familiar role types -export type FamiliarRole = 'combat' | 'support' | 'mana' | 'guardian'; - -// Familiar ability types (updated - removed lifesteal, thorns, manaShield since player can't take damage) -export type FamiliarAbilityType = - | 'damageBonus' - | 'critChance' - | 'critDamage' - | 'castSpeed' - | 'elementalBonus' - | 'guardianDamage' - | 'manaSiphon' - | 'barrierBreaker' - | 'manaRegen' - | 'autoGather' - | 'autoConvert' - | 'maxManaBonus' - | 'bonusGold' - | 'studySpeed'; - -// Familiar ability definition -export interface FamiliarAbility { - type: FamiliarAbilityType; - baseValue: number; - scalingPerLevel: number; - desc: string; -} - -// Familiar definition -export interface FamiliarDef { - id: string; - name: string; - desc: string; - role: FamiliarRole; - element: string; - rarity: 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary'; - abilities: FamiliarAbility[]; - baseStats: { power: number; bond: number }; - unlockCondition?: { type: 'floor' | 'pact' | 'mana' | 'study'; value: number }; - flavorText?: string; -} - -// Familiar instance (owned familiar) -export interface FamiliarInstance { - familiarId: string; - level: number; - xp: number; - bond: number; - abilities: { type: FamiliarAbilityType; level: number }[]; - active: boolean; -} - export interface GameState { // Time day: number;