Delete src/lib/game/familiar-slice.ts
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 2m5s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 2m5s
This commit is contained in:
@@ -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
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user