pack
This commit is contained in:
@@ -39,6 +39,15 @@ import {
|
||||
} from './crafting-slice';
|
||||
import { EQUIPMENT_TYPES } from './data/equipment';
|
||||
import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchantment-effects';
|
||||
import {
|
||||
createFamiliarSlice,
|
||||
processFamiliarTick,
|
||||
grantStartingFamiliar,
|
||||
type FamiliarActions,
|
||||
type FamiliarBonuses,
|
||||
DEFAULT_FAMILIAR_BONUSES,
|
||||
} from './familiar-slice';
|
||||
import { rollLootDrops } from './data/loot-drops';
|
||||
|
||||
// Default empty effects for when effects aren't provided
|
||||
const DEFAULT_EFFECTS: ComputedEffects = {
|
||||
@@ -528,14 +537,48 @@ function makeInitial(overrides: Partial<GameState> = {}): GameState {
|
||||
incursionStrength: 0,
|
||||
containmentWards: 0,
|
||||
|
||||
log: ['✨ The loop begins. You start with a Basic Staff (Mana Bolt) and civilian clothes. Gather your strength, mage.'],
|
||||
// Combo System
|
||||
combo: {
|
||||
count: 0,
|
||||
multiplier: 1,
|
||||
lastCastTime: 0,
|
||||
decayTimer: 0,
|
||||
maxCombo: 0,
|
||||
elementChain: [],
|
||||
},
|
||||
totalTicks: 0,
|
||||
|
||||
// Loot System
|
||||
lootInventory: {
|
||||
materials: {},
|
||||
essence: {},
|
||||
blueprints: [],
|
||||
},
|
||||
lootDropsToday: 0,
|
||||
|
||||
// Achievements
|
||||
achievements: {
|
||||
unlocked: [],
|
||||
progress: {},
|
||||
},
|
||||
totalDamageDealt: 0,
|
||||
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.'],
|
||||
loopInsight: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Game Store ───────────────────────────────────────────────────────────────
|
||||
|
||||
interface GameStore extends GameState, CraftingActions {
|
||||
interface GameStore extends GameState, CraftingActions, FamiliarActions {
|
||||
// Actions
|
||||
tick: () => void;
|
||||
gatherMana: () => void;
|
||||
@@ -573,6 +616,7 @@ export const useGameStore = create<GameStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
...makeInitial(),
|
||||
...createFamiliarSlice(set, get),
|
||||
|
||||
getMaxMana: () => computeMaxMana(get()),
|
||||
getRegen: () => computeRegen(get()),
|
||||
@@ -600,8 +644,16 @@ export const useGameStore = create<GameStore>()(
|
||||
// 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);
|
||||
const baseRegen = computeRegen(state, effects) + familiarBonuses.manaRegenBonus;
|
||||
|
||||
// Time progression
|
||||
let hour = state.hour + HOURS_PER_TICK;
|
||||
@@ -653,18 +705,29 @@ export const useGameStore = create<GameStore>()(
|
||||
|
||||
// Calculate effective regen with incursion and meditation
|
||||
const effectiveRegen = baseRegen * (1 - incursionStrength) * meditationMultiplier;
|
||||
|
||||
|
||||
// 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;
|
||||
let skills = state.skills;
|
||||
let skillProgress = state.skillProgress;
|
||||
let spells = state.spells;
|
||||
let log = state.log;
|
||||
let elements = state.elements;
|
||||
let unlockedEffects = state.unlockedEffects;
|
||||
|
||||
if (state.currentAction === 'study' && currentStudyTarget) {
|
||||
@@ -738,8 +801,27 @@ export const useGameStore = create<GameStore>()(
|
||||
}
|
||||
|
||||
// Combat - MULTI-SPELL casting from all equipped weapons
|
||||
let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, equipmentSpellStates } = state;
|
||||
let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, equipmentSpellStates, combo, totalTicks, lootInventory, achievements, totalDamageDealt, totalSpellsCast } = state;
|
||||
const floorElement = getFloorElement(currentFloor);
|
||||
|
||||
// Increment total ticks
|
||||
const newTotalTicks = totalTicks + 1;
|
||||
|
||||
// Combo decay - decay combo when not climbing or when decay timer expires
|
||||
let newCombo = { ...combo };
|
||||
if (state.currentAction !== 'climb') {
|
||||
// Rapidly decay combo when not climbing
|
||||
newCombo.count = Math.max(0, newCombo.count - 5);
|
||||
newCombo.multiplier = 1 + newCombo.count * 0.02;
|
||||
} else if (newCombo.count > 0) {
|
||||
// Slow decay while climbing but not casting
|
||||
newCombo.decayTimer--;
|
||||
if (newCombo.decayTimer <= 0) {
|
||||
newCombo.count = Math.max(0, newCombo.count - 2);
|
||||
newCombo.multiplier = 1 + newCombo.count * 0.02;
|
||||
newCombo.decayTimer = 10;
|
||||
}
|
||||
}
|
||||
|
||||
if (state.currentAction === 'climb') {
|
||||
// Get all spells from equipped caster weapons
|
||||
@@ -768,7 +850,7 @@ export const useGameStore = create<GameStore>()(
|
||||
|
||||
// Compute attack speed from quickCast skill and upgrades
|
||||
const baseAttackSpeed = 1 + (state.skills.quickCast || 0) * 0.05;
|
||||
const totalAttackSpeed = baseAttackSpeed * effects.attackSpeedMultiplier;
|
||||
const totalAttackSpeed = baseAttackSpeed * effects.attackSpeedMultiplier * familiarBonuses.castSpeedMultiplier;
|
||||
|
||||
// Process each active spell
|
||||
for (const { spellId, equipmentId } of activeSpells) {
|
||||
@@ -796,9 +878,41 @@ export const useGameStore = create<GameStore>()(
|
||||
elements = afterCost.elements;
|
||||
totalManaGathered += spellDef.cost.amount;
|
||||
|
||||
// Increment spell cast counter
|
||||
totalSpellsCast++;
|
||||
|
||||
// ─── Combo System ───
|
||||
// Build combo on each cast
|
||||
newCombo.count = Math.min(100, newCombo.count + 1);
|
||||
newCombo.lastCastTime = newTotalTicks;
|
||||
newCombo.decayTimer = 10; // Reset decay timer
|
||||
newCombo.maxCombo = Math.max(newCombo.maxCombo, newCombo.count);
|
||||
|
||||
// Track element chain
|
||||
const spellElement = spellDef.elem;
|
||||
newCombo.elementChain = [...newCombo.elementChain.slice(-2), spellElement];
|
||||
|
||||
// Calculate combo multiplier
|
||||
let comboMult = 1 + newCombo.count * 0.02; // +2% per combo
|
||||
|
||||
// Element chain bonus: +25% if last 3 spells were different elements
|
||||
const uniqueElements = new Set(newCombo.elementChain);
|
||||
if (newCombo.elementChain.length === 3 && uniqueElements.size === 3) {
|
||||
comboMult += 0.25;
|
||||
// Log elemental chain occasionally
|
||||
if (newCombo.count % 10 === 0) {
|
||||
log = [`🌈 Elemental Chain! (${newCombo.elementChain.join(' → ')})`, ...log.slice(0, 49)];
|
||||
}
|
||||
}
|
||||
|
||||
newCombo.multiplier = Math.min(3.0, comboMult);
|
||||
|
||||
// Calculate damage
|
||||
let dmg = calcDamage(state, spellId, floorElement);
|
||||
|
||||
// Apply combo multiplier FIRST
|
||||
dmg *= newCombo.multiplier;
|
||||
|
||||
// Apply upgrade damage multipliers and bonuses
|
||||
dmg = dmg * effects.baseDamageMultiplier + effects.baseDamageBonus;
|
||||
|
||||
@@ -811,7 +925,16 @@ export const useGameStore = create<GameStore>()(
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
|
||||
dmg *= 1.5;
|
||||
}
|
||||
|
||||
|
||||
// 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) {
|
||||
@@ -825,6 +948,15 @@ export const useGameStore = create<GameStore>()(
|
||||
const healAmount = dmg * lifestealEffect.value;
|
||||
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;
|
||||
|
||||
// Apply damage
|
||||
floorHP = Math.max(0, floorHP - dmg);
|
||||
@@ -835,6 +967,33 @@ export const useGameStore = create<GameStore>()(
|
||||
if (floorHP <= 0) {
|
||||
// Floor cleared
|
||||
const wasGuardian = GUARDIANS[currentFloor];
|
||||
|
||||
// ─── Loot Drop System ───
|
||||
const lootDrops = rollLootDrops(currentFloor, !!wasGuardian, 0);
|
||||
|
||||
for (const { drop, amount } of lootDrops) {
|
||||
if (drop.type === 'material') {
|
||||
lootInventory.materials[drop.id] = (lootInventory.materials[drop.id] || 0) + amount;
|
||||
log = [`💎 Found: ${drop.name}!`, ...log.slice(0, 49)];
|
||||
} else if (drop.type === 'essence' && drop.id) {
|
||||
// Extract element from essence drop id (e.g., 'fireEssenceDrop' -> 'fire')
|
||||
const element = drop.id.replace('EssenceDrop', '');
|
||||
if (elements[element]) {
|
||||
const gain = Math.min(amount, elements[element].max - elements[element].current);
|
||||
elements[element] = { ...elements[element], current: elements[element].current + gain };
|
||||
log = [`✨ Gained ${gain} ${element} essence!`, ...log.slice(0, 49)];
|
||||
}
|
||||
} else if (drop.type === 'gold') {
|
||||
rawMana += amount;
|
||||
log = [`💫 Gained ${amount} mana from ${drop.name}!`, ...log.slice(0, 49)];
|
||||
} else if (drop.type === 'blueprint') {
|
||||
if (!lootInventory.blueprints.includes(drop.id)) {
|
||||
lootInventory.blueprints.push(drop.id);
|
||||
log = [`📜 Discovered: ${drop.name}!`, ...log.slice(0, 49)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wasGuardian && !signedPacts.includes(currentFloor)) {
|
||||
signedPacts = [...signedPacts, currentFloor];
|
||||
log = [`⚔️ ${wasGuardian.name} defeated! Pact signed! (${wasGuardian.pact}x)`, ...log.slice(0, 49)];
|
||||
@@ -852,6 +1011,11 @@ export const useGameStore = create<GameStore>()(
|
||||
floorHP = floorMaxHP;
|
||||
maxFloorReached = Math.max(maxFloorReached, currentFloor);
|
||||
|
||||
// Reset combo on floor change (partial reset - keep 50%)
|
||||
newCombo.count = Math.floor(newCombo.count * 0.5);
|
||||
newCombo.multiplier = 1 + newCombo.count * 0.02;
|
||||
newCombo.elementChain = [];
|
||||
|
||||
// Reset ALL spell progress on floor change
|
||||
equipmentSpellStates = equipmentSpellStates.map(s => ({ ...s, castProgress: 0 }));
|
||||
spellState = { ...spellState, castProgress: 0 };
|
||||
@@ -865,6 +1029,9 @@ export const useGameStore = create<GameStore>()(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Update combo state
|
||||
combo = newCombo;
|
||||
|
||||
// Process crafting actions (design, prepare, enchant)
|
||||
const craftingUpdates = processCraftingTick(
|
||||
@@ -912,11 +1079,47 @@ export const useGameStore = create<GameStore>()(
|
||||
spells,
|
||||
elements,
|
||||
log,
|
||||
equipmentSpellStates,
|
||||
equipmentSpellStates,
|
||||
combo,
|
||||
totalTicks: newTotalTicks,
|
||||
lootInventory,
|
||||
achievements,
|
||||
totalDamageDealt,
|
||||
totalSpellsCast,
|
||||
});
|
||||
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,
|
||||
@@ -937,6 +1140,13 @@ export const useGameStore = create<GameStore>()(
|
||||
unlockedEffects,
|
||||
log,
|
||||
equipmentSpellStates,
|
||||
combo,
|
||||
totalTicks: newTotalTicks,
|
||||
lootInventory,
|
||||
achievements,
|
||||
totalDamageDealt,
|
||||
totalSpellsCast,
|
||||
familiars,
|
||||
...craftingUpdates,
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user