This commit is contained in:
Z User
2026-03-25 16:35:56 +00:00
parent 3b2e89db74
commit 7c5f2f30f0
11 changed files with 2929 additions and 10 deletions

View File

@@ -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,
});
},