Remove familiar system to focus on guardian pacts
Some checks failed
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m11s

- Remove familiar-slice.ts, familiars.ts data file, and FamiliarTab.tsx
- Remove all familiar types from types.ts (FamiliarRole, FamiliarAbilityType, etc.)
- Remove familiar state from GameState interface
- Remove familiar bonuses from combat calculations
- Remove familiar XP tracking from tick function
- Remove familiar tab from page.tsx UI

This simplifies the game to focus on guardian pacts as the primary
progression mechanic, avoiding the parallel familiar system that
diluted the importance of pacts.
This commit is contained in:
2026-03-26 14:58:07 +00:00
parent c050ca3814
commit a2c9af7d45
6 changed files with 5 additions and 1612 deletions

View File

@@ -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<string, typeof Flame> = {
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<string, string> = {
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<string, string> = {
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<string, typeof Sword> = {
combat: Sword,
mana: Sparkles,
support: Heart,
guardian: ShieldCheck,
};
// Ability type icons
const ABILITY_ICONS: Record<FamiliarAbilityType, typeof Zap> = {
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<typeof import('@/lib/game/familiar-slice').createFamiliarSlice>['getActiveFamiliarBonuses'] extends () => infer R ? R : never;
getAvailableFamiliars: () => string[];
};
}
export function FamiliarTab({ store }: FamiliarTabProps) {
const [selectedFamiliar, setSelectedFamiliar] = useState<number | null>(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 (
<Card
key={`${instance.familiarId}-${index}`}
className={`cursor-pointer transition-all ${RARITY_BG[def.rarity]} ${
isSelected ? 'ring-2 ring-amber-500' : ''
} ${instance.active ? 'ring-1 ring-green-500/50' : ''} border ${RARITY_COLORS[def.rarity]}`}
onClick={() => setSelectedFamiliar(isSelected ? null : index)}
>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-10 h-10 rounded-full bg-black/30 flex items-center justify-center">
<ElementIcon className="w-6 h-6" style={{ color: ELEMENTS[def.element]?.color || '#fff' }} />
</div>
<div>
<CardTitle className={`text-sm ${RARITY_COLORS[def.rarity]}`}>
{instance.nickname || def.name}
</CardTitle>
<div className="flex items-center gap-1 text-xs text-gray-400">
<RoleIcon className="w-3 h-3" />
<span>Lv.{instance.level}</span>
{instance.active && (
<Badge className="ml-1 bg-green-900/50 text-green-300 text-xs py-0">Active</Badge>
)}
</div>
</div>
</div>
<Badge variant="outline" className={`${RARITY_COLORS[def.rarity]} text-xs`}>
{def.rarity}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-2">
{/* XP Bar */}
<div className="space-y-0.5">
<div className="flex justify-between text-xs text-gray-400">
<span>XP</span>
<span>{formatXp(instance.experience, instance.level)}</span>
</div>
<Progress value={xpPercent} className="h-1.5 bg-gray-800" />
</div>
{/* Bond Bar */}
<div className="space-y-0.5">
<div className="flex justify-between text-xs text-gray-400">
<span className="flex items-center gap-1">
<Heart className="w-3 h-3" /> Bond
</span>
<span>{bondPercent.toFixed(0)}%</span>
</div>
<Progress value={bondPercent} className="h-1.5 bg-gray-800" />
</div>
{/* Abilities Preview */}
<div className="flex flex-wrap gap-1 mt-2">
{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 (
<Tooltip key={ability.type}>
<TooltipTrigger asChild>
<Badge variant="outline" className="text-xs py-0 flex items-center gap-1">
<AbilityIcon className="w-3 h-3" />
{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)}`}
</Badge>
</TooltipTrigger>
<TooltipContent>
<p className="text-sm">{abilityDef.desc}</p>
<p className="text-xs text-gray-400">Level {ability.level}/10</p>
</TooltipContent>
</Tooltip>
);
})}
</div>
</CardContent>
</Card>
);
};
// 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 (
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-amber-400 text-sm">Familiar Details</CardTitle>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
onClick={() => setSelectedFamiliar(null)}
>
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Name and nickname */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="w-12 h-12 rounded-full bg-black/30 flex items-center justify-center">
<ElementIcon className="w-8 h-8" style={{ color: ELEMENTS[def.element]?.color || '#fff' }} />
</div>
<div>
<h3 className={`text-lg font-bold ${RARITY_COLORS[def.rarity]}`}>
{def.name}
</h3>
{instance.nickname && (
<p className="text-sm text-gray-400">"{instance.nickname}"</p>
)}
</div>
</div>
{/* Nickname input */}
<div className="flex gap-2">
<Input
value={nicknameInput}
onChange={(e) => setNicknameInput(e.target.value)}
placeholder="Set nickname..."
className="h-8 text-sm bg-gray-800 border-gray-600"
/>
<Button
size="sm"
variant="outline"
onClick={() => {
store.setFamiliarNickname(selectedFamiliar, nicknameInput);
setNicknameInput('');
}}
disabled={!nicknameInput.trim()}
>
Set
</Button>
</div>
</div>
{/* Description */}
<div className="text-sm text-gray-300 italic">
{def.desc}
</div>
{/* Stats */}
<div className="grid grid-cols-2 gap-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-400">Level:</span>
<span className="text-white">{instance.level}/100</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Bond:</span>
<span className="text-white">{instance.bond.toFixed(0)}%</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Role:</span>
<span className="text-white capitalize">{def.role}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Element:</span>
<span style={{ color: ELEMENTS[def.element]?.color }}>{def.element}</span>
</div>
</div>
<Separator className="bg-gray-700" />
{/* Abilities */}
<div className="space-y-2">
<h4 className="text-sm font-semibold text-gray-300">Abilities</h4>
{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 (
<div key={ability.type} className="p-2 rounded bg-gray-800/50 border border-gray-700">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<AbilityIcon className="w-4 h-4 text-amber-400" />
<span className="text-sm font-medium capitalize">
{ability.type.replace(/([A-Z])/g, ' $1').trim()}
</span>
</div>
<Badge variant="outline" className="text-xs">Lv.{ability.level}/10</Badge>
</div>
<p className="text-xs text-gray-400 mb-2">{abilityDef.desc}</p>
<div className="flex items-center justify-between">
<span className="text-sm text-green-400">
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' ? '%' : ''}
</span>
{ability.level < 10 && (
<Button
size="sm"
variant="outline"
className="h-6 text-xs"
disabled={!canUpgrade}
onClick={() => store.upgradeFamiliarAbility(selectedFamiliar, ability.type)}
>
Upgrade ({upgradeCost} XP)
</Button>
)}
</div>
</div>
);
})}
</div>
{/* Activate/Deactivate */}
<Button
className={`w-full ${instance.active ? 'bg-red-900/50 hover:bg-red-800/50' : 'bg-green-900/50 hover:bg-green-800/50'}`}
onClick={() => store.setActiveFamiliar(selectedFamiliar, !instance.active)}
disabled={!instance.active && activeCount >= activeFamiliarSlots}
>
{instance.active ? 'Deactivate' : 'Activate'}
</Button>
{/* Flavor text */}
{def.flavorText && (
<p className="text-xs text-gray-500 italic text-center">
"{def.flavorText}"
</p>
)}
</CardContent>
</Card>
);
};
// Render summonable familiars
const renderSummonableFamiliars = () => {
if (availableFamiliars.length === 0) return null;
return (
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-cyan-400 text-sm flex items-center gap-2">
<Sparkles className="w-4 h-4" />
Available to Summon ({availableFamiliars.length})
</CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className="h-48">
<div className="space-y-2">
{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 (
<div
key={familiarId}
className={`p-2 rounded border ${RARITY_COLORS[def.rarity]} ${RARITY_BG[def.rarity]} flex items-center justify-between`}
>
<div className="flex items-center gap-2">
<ElementIcon className="w-5 h-5" style={{ color: ELEMENTS[def.element]?.color || '#fff' }} />
<div>
<div className={`text-sm font-medium ${RARITY_COLORS[def.rarity]}`}>
{def.name}
</div>
<div className="text-xs text-gray-400 flex items-center gap-1">
<RoleIcon className="w-3 h-3" />
{def.role}
</div>
</div>
</div>
<Button
size="sm"
variant="outline"
className="h-7"
onClick={() => store.summonFamiliar(familiarId)}
>
Summon
</Button>
</div>
);
})}
</div>
</ScrollArea>
</CardContent>
</Card>
);
};
// 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 (
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-green-400 text-sm flex items-center gap-2">
<TrendingUp className="w-4 h-4" />
Active Familiar Bonuses
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 text-sm">
{familiarBonuses.damageMultiplier > 1 && (
<div className="flex items-center gap-2">
<Sword className="w-4 h-4 text-red-400" />
<span>+{((familiarBonuses.damageMultiplier - 1) * 100).toFixed(0)}% DMG</span>
</div>
)}
{familiarBonuses.manaRegenBonus > 0 && (
<div className="flex items-center gap-2">
<Sparkles className="w-4 h-4 text-blue-400" />
<span>+{familiarBonuses.manaRegenBonus.toFixed(1)} regen</span>
</div>
)}
{familiarBonuses.autoGatherRate > 0 && (
<div className="flex items-center gap-2">
<Zap className="w-4 h-4 text-yellow-400" />
<span>+{familiarBonuses.autoGatherRate.toFixed(1)}/hr gather</span>
</div>
)}
{familiarBonuses.critChanceBonus > 0 && (
<div className="flex items-center gap-2">
<Star className="w-4 h-4 text-amber-400" />
<span>+{familiarBonuses.critChanceBonus.toFixed(1)}% crit</span>
</div>
)}
{familiarBonuses.castSpeedMultiplier > 1 && (
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-cyan-400" />
<span>+{((familiarBonuses.castSpeedMultiplier - 1) * 100).toFixed(0)}% speed</span>
</div>
)}
{familiarBonuses.elementalDamageMultiplier > 1 && (
<div className="flex items-center gap-2">
<Flame className="w-4 h-4 text-orange-400" />
<span>+{((familiarBonuses.elementalDamageMultiplier - 1) * 100).toFixed(0)}% elem</span>
</div>
)}
{familiarBonuses.lifeStealPercent > 0 && (
<div className="flex items-center gap-2">
<Heart className="w-4 h-4 text-red-400" />
<span>+{familiarBonuses.lifeStealPercent.toFixed(0)}% lifesteal</span>
</div>
)}
{familiarBonuses.insightMultiplier > 1 && (
<div className="flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-purple-400" />
<span>+{((familiarBonuses.insightMultiplier - 1) * 100).toFixed(0)}% insight</span>
</div>
)}
</div>
</CardContent>
</Card>
);
};
return (
<TooltipProvider>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Owned Familiars */}
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-amber-400 text-sm flex items-center gap-2">
<Heart className="w-4 h-4" />
Your Familiars ({familiars.length})
</CardTitle>
<div className="text-xs text-gray-400">
Active Slots: {activeCount}/{activeFamiliarSlots}
</div>
</div>
</CardHeader>
<CardContent>
{familiars.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{familiars.map((instance, index) => renderFamiliarCard(instance, index))}
</div>
) : (
<div className="text-center p-8 text-gray-500">
No familiars yet. Progress through the game to summon companions!
</div>
)}
</CardContent>
</Card>
{/* Active Bonuses */}
{renderActiveBonuses()}
{/* Selected Familiar Details */}
{renderFamiliarDetails()}
{/* Summonable Familiars */}
{renderSummonableFamiliars()}
{/* Familiar Guide */}
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
<CardHeader className="pb-2">
<CardTitle className="text-purple-400 text-sm flex items-center gap-2">
<Crown className="w-4 h-4" />
Familiar Guide
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm text-gray-300">
<div>
<h4 className="font-semibold text-amber-400 mb-1">Acquiring Familiars</h4>
<p>Familiars become available to summon as you progress through floors, gather mana, and sign pacts with guardians. Higher rarity familiars are unlocked later.</p>
</div>
<div>
<h4 className="font-semibold text-amber-400 mb-1">Leveling & Bond</h4>
<p>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.</p>
</div>
<div>
<h4 className="font-semibold text-amber-400 mb-1">Roles</h4>
<p>
<span className="text-red-400">Combat</span> - Damage and crit bonuses<br/>
<span className="text-blue-400">Mana</span> - Regeneration and auto-gathering<br/>
<span className="text-green-400">Support</span> - Speed and utility<br/>
<span className="text-amber-400">Guardian</span> - Defense and shields
</p>
</div>
<div>
<h4 className="font-semibold text-amber-400 mb-1">Active Slots</h4>
<p>You can have 1 familiar active by default. Upgrade through prestige to unlock more active slots for stacking bonuses.</p>
</div>
</div>
</CardContent>
</Card>
</div>
</TooltipProvider>
);
}