Delete src/lib/game/familiar-slice.ts
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 2m5s

This commit is contained in:
2026-04-10 10:34:15 +02:00
parent 36db242033
commit 4fc9d3f582
-367
View File
@@ -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<GameState>) => 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<GameState, 'familiars' | 'rawMana' | 'elements' | 'totalManaGathered' | 'activeFamiliarSlots'>,
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
}];
}