diff --git a/src/app/page.tsx b/src/app/page.tsx index d39243e..29230ab 100755 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -13,7 +13,6 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { RotateCcw } from 'lucide-react'; import { CraftingTab, SpireTab, SpellsTab, LabTab, SkillsTab, StatsTab } from '@/components/game/tabs'; -import { FamiliarTab } from '@/components/game/tabs/FamiliarTab'; import { ComboMeter, ActionButtons, CalendarDisplay, ManaDisplay, TimeDisplay } from '@/components/game'; import { LootInventoryDisplay } from '@/components/game/LootInventory'; import { AchievementsDisplay } from '@/components/game/AchievementsDisplay'; @@ -227,13 +226,12 @@ export default function ManaLoopGame() { {/* Right Panel - Tabs */}
- + ⚔️ Spire 📚 Skills ✨ Spells 🔧 Craft 🔬 Lab - 🐾 Familiar 📊 Stats 📖 Grimoire @@ -266,10 +264,6 @@ export default function ManaLoopGame() { - - - - = { - 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 100644 index 433b499..0000000 --- a/src/lib/game/data/familiars.ts +++ /dev/null @@ -1,498 +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)`, - }), - - 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)`, - }), - - lifeSteal: (base: number, scaling: number): FamiliarAbility => ({ - type: 'lifeSteal', - baseValue: base, - scalingPerLevel: scaling, - desc: `+${base}% life steal (+${scaling}% per level)`, - }), - - thorns: (base: number, scaling: number): FamiliarAbility => ({ - type: 'thorns', - baseValue: base, - scalingPerLevel: scaling, - desc: `Reflect ${base}% damage taken (+${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)`, - }), - - manaShield: (base: number, scaling: number): FamiliarAbility => ({ - type: 'manaShield', - baseValue: base, - scalingPerLevel: scaling, - desc: `Shield absorbs ${base} damage, costs 1 mana per ${base} damage`, - }), - - // Support abilities - bonusGold: (base: number, scaling: number): FamiliarAbility => ({ - type: 'bonusGold', - baseValue: base, - scalingPerLevel: scaling, - desc: `+${base}% insight gain (+${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.lifeSteal(1, 0.2), - ], - 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.thorns(2, 0.5), - ], - 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.thorns(5, 1), - ABILITIES.manaShield(10, 2), - ], - 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.lifeSteal(3, 0.5), - ], - 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: 'Shield Guardian', - desc: 'A loyal protector carved from enchanted stone.', - role: 'guardian', - element: 'earth', - rarity: 'rare', - abilities: [ - ABILITIES.thorns(8, 1.5), - ABILITIES.manaShield(20, 4), - ], - baseStats: { power: 50, bond: 8 }, - unlockCondition: { type: 'floor', value: 35 }, - flavorText: 'It stands motionless for hours, then suddenly moves to block danger.', - }, - - // === 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.manaShield(15, 3), - ], - 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.thorns(15, 3), - ABILITIES.manaShield(30, 5), - 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.lifeSteal(8, 1.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.manaShield(25, 5), - ], - 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.thorns(25, 5), - ABILITIES.manaShield(50, 10), - ABILITIES.damageBonus(10, 2), - ABILITIES.lifeSteal(5, 1), - ], - baseStats: { power: 120, bond: 12 }, - unlockCondition: { type: 'pact', value: 75 }, - flavorText: 'It radiates an aura of absolute protection and quiet 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 100644 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/store.ts b/src/lib/game/store.ts index 6225ab8..f450012 100755 --- a/src/lib/game/store.ts +++ b/src/lib/game/store.ts @@ -32,14 +32,6 @@ import { type CraftingActions } from './crafting-slice'; import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchantment-effects'; -import { - createFamiliarSlice, - processFamiliarTick, - grantStartingFamiliar, - type FamiliarActions, - type FamiliarBonuses, - DEFAULT_FAMILIAR_BONUSES, -} from './familiar-slice'; import { createNavigationSlice, type NavigationActions, @@ -280,20 +272,14 @@ function makeInitial(overrides: Partial = {}): GameState { totalSpellsCast: 0, totalCraftsCompleted: 0, - // Familiars - familiars: grantStartingFamiliar(), - activeFamiliarSlots: 1, - familiarSummonProgress: 0, - totalFamiliarXpEarned: 0, - - log: ['✨ The loop begins. You start with a Basic Staff (Mana Bolt) and civilian clothes. A friendly Mana Wisp floats nearby. Gather your strength, mage.'], + log: ['✨ The loop begins. You start with a Basic Staff (Mana Bolt) and civilian clothes. Gather your strength, mage.'], loopInsight: 0, }; } // ─── Game Store ─────────────────────────────────────────────────────────────── -interface GameStore extends GameState, CraftingActions, FamiliarActions, NavigationActions, StudyActions { +interface GameStore extends GameState, CraftingActions, NavigationActions, StudyActions { // Actions tick: () => void; gatherMana: () => void; @@ -329,7 +315,6 @@ export const useGameStore = create()( persist( (set, get) => ({ ...makeInitial(), - ...createFamiliarSlice(set, get), ...createNavigationSlice(set, get), ...createStudySlice(set, get), @@ -359,16 +344,8 @@ export const useGameStore = create()( // Compute unified effects (includes skill upgrades AND equipment enchantments) const effects = getUnifiedEffects(state); - // Compute familiar bonuses - const familiarBonuses = state.familiars.length > 0 - ? (() => { - const slice = createFamiliarSlice(set, get); - return slice.getActiveFamiliarBonuses(); - })() - : DEFAULT_FAMILIAR_BONUSES; - const maxMana = computeMaxMana(state, effects); - const baseRegen = computeRegen(state, effects) + familiarBonuses.manaRegenBonus; + const baseRegen = computeRegen(state, effects); // Time progression let hour = state.hour + HOURS_PER_TICK; @@ -431,18 +408,7 @@ export const useGameStore = create()( // Mana regeneration let rawMana = Math.min(state.rawMana + effectiveRegen * HOURS_PER_TICK, maxMana); let totalManaGathered = state.totalManaGathered; - - // Familiar auto-gather and auto-convert let elements = state.elements; - if (familiarBonuses.autoGatherRate > 0 || familiarBonuses.autoConvertRate > 0) { - const familiarUpdates = processFamiliarTick( - { rawMana, elements, totalManaGathered, familiars: state.familiars, activeFamiliarSlots: state.activeFamiliarSlots }, - familiarBonuses - ); - rawMana = Math.min(familiarUpdates.rawMana, maxMana); - elements = familiarUpdates.elements; - totalManaGathered = familiarUpdates.totalManaGathered; - } // Study progress let currentStudyTarget = state.currentStudyTarget; @@ -623,7 +589,7 @@ export const useGameStore = create()( // Compute attack speed from quickCast skill and upgrades const baseAttackSpeed = 1 + (state.skills.quickCast || 0) * 0.05; - const totalAttackSpeed = baseAttackSpeed * effects.attackSpeedMultiplier * familiarBonuses.castSpeedMultiplier; + const totalAttackSpeed = baseAttackSpeed * effects.attackSpeedMultiplier; // Process each active spell for (const { spellId, equipmentId } of activeSpells) { @@ -707,15 +673,6 @@ export const useGameStore = create()( log = [`💥 Combo Master! Triple damage!`, ...log.slice(0, 49)]; } - // Familiar bonuses - dmg *= familiarBonuses.damageMultiplier; - dmg *= familiarBonuses.elementalDamageMultiplier; - - // Familiar crit chance bonus - if (Math.random() < familiarBonuses.critChanceBonus / 100) { - dmg *= 1.5; - } - // Spell echo - chance to cast again const echoChance = (skills.spellEcho || 0) * 0.1; if (Math.random() < echoChance) { @@ -730,12 +687,6 @@ export const useGameStore = create()( rawMana = Math.min(rawMana + healAmount, maxMana); } - // Familiar lifesteal - if (familiarBonuses.lifeStealPercent > 0) { - const healAmount = dmg * (familiarBonuses.lifeStealPercent / 100); - rawMana = Math.min(rawMana + healAmount, maxMana); - } - // Track total damage for achievements totalDamageDealt += dmg; @@ -899,36 +850,6 @@ export const useGameStore = create()( return; } - // Grant XP to active familiars based on activity - let familiars = state.familiars; - if (familiars.some(f => f.active)) { - let xpGain = 0; - let xpSource: 'combat' | 'gather' | 'meditate' | 'study' | 'time' = 'time'; - - if (state.currentAction === 'climb') { - xpGain = 2 * HOURS_PER_TICK; // 2 XP per hour in combat - xpSource = 'combat'; - } else if (state.currentAction === 'meditate') { - xpGain = 1 * HOURS_PER_TICK; - xpSource = 'meditate'; - } else if (state.currentAction === 'study') { - xpGain = 1.5 * HOURS_PER_TICK; - xpSource = 'study'; - } else { - xpGain = 0.5 * HOURS_PER_TICK; // Passive XP - } - - // Update familiar XP and bond - familiars = familiars.map(f => { - if (!f.active) return f; - const bondMultiplier = 1 + (f.bond / 100); - const xpGained = Math.floor(xpGain * bondMultiplier); - const newXp = f.experience + xpGained; - const newBond = Math.min(100, f.bond + 0.02); // Slow bond gain - return { ...f, experience: newXp, bond: newBond }; - }); - } - set({ day, hour, @@ -955,7 +876,6 @@ export const useGameStore = create()( achievements, totalDamageDealt, totalSpellsCast, - familiars, consecutiveStudyHours, studyStartedAt, ...craftingUpdates, @@ -1738,11 +1658,6 @@ export const useGameStore = create()( totalDamageDealt: state.totalDamageDealt, totalSpellsCast: state.totalSpellsCast, totalCraftsCompleted: state.totalCraftsCompleted, - // Familiars - familiars: state.familiars, - activeFamiliarSlots: state.activeFamiliarSlots, - familiarSummonProgress: state.familiarSummonProgress, - totalFamiliarXpEarned: state.totalFamiliarXpEarned, }), } ) diff --git a/src/lib/game/types.ts b/src/lib/game/types.ts index 1dd3f43..fef717a 100755 --- a/src/lib/game/types.ts +++ b/src/lib/game/types.ts @@ -314,69 +314,6 @@ export interface BlueprintDef { learned: boolean; } -// ─── Familiar System ─────────────────────────────────────────────────────────── - -// Familiar role determines their primary function -export type FamiliarRole = 'combat' | 'mana' | 'support' | 'guardian'; - -// Familiar ability types -export type FamiliarAbilityType = - | 'damageBonus' // +X% damage - | 'manaRegen' // +X mana regen - | 'autoGather' // Gathers mana automatically - | 'critChance' // +X% crit chance - | 'castSpeed' // +X% cast speed - | 'manaShield' // Absorbs damage, costs mana - | 'elementalBonus' // +X% elemental damage - | 'lifeSteal' // Heal on hit - | 'bonusGold' // +X% insight gain - | 'autoConvert' // Auto-converts mana to elements - | 'thorns'; // Reflects damage - -export interface FamiliarAbility { - type: FamiliarAbilityType; - baseValue: number; // Base effect value - scalingPerLevel: number; // How much it increases per familiar level - desc: string; -} - -// Familiar definition (static data) -export interface FamiliarDef { - id: string; - name: string; - desc: string; - role: FamiliarRole; - element: string; // Associated element - rarity: 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary'; - abilities: FamiliarAbility[]; - baseStats: { - power: number; // Affects ability strength - bond: number; // How fast bond grows - }; - unlockCondition?: { - type: 'floor' | 'pact' | 'mana' | 'study'; - value: number; - }; - flavorText?: string; -} - -// Familiar instance (player's owned familiar) -export interface FamiliarInstance { - familiarId: string; // Reference to FamiliarDef - level: number; // 1-100 - bond: number; // 0-100, affects power multiplier - experience: number; // XP towards next level - abilities: Array<{ - type: FamiliarAbilityType; - level: number; // Ability level (1-10) - }>; - active: boolean; // Is this familiar currently summoned? - nickname?: string; // Optional custom name -} - -// Familiar experience gain sources -export type FamiliarXpSource = 'combat' | 'gather' | 'meditate' | 'study' | 'time'; - export type GameAction = 'meditate' | 'climb' | 'study' | 'craft' | 'repair' | 'convert' | 'design' | 'prepare' | 'enchant'; export interface ScheduleBlock { @@ -502,12 +439,6 @@ export interface GameState { totalSpellsCast: number; // For spell achievements totalCraftsCompleted: number; // For craft achievements - // Familiars - familiars: FamiliarInstance[]; // Owned familiars - activeFamiliarSlots: number; // How many familiars can be active (default 1) - familiarSummonProgress: number; // Progress toward summoning new familiar (0-100) - totalFamiliarXpEarned: number; // Lifetime XP earned for familiars - // Log log: string[];