Initial commit

This commit is contained in:
Z User
2026-04-03 17:23:15 +00:00
commit 4f474dbcf3
192 changed files with 47527 additions and 0 deletions
+157
View File
@@ -0,0 +1,157 @@
// ─── Combat Slice ─────────────────────────────────────────────────────────────
// Manages spire climbing, combat, and floor progression
import type { StateCreator } from 'zustand';
import type { GameState, GameAction, SpellCost } from '../types';
import { GUARDIANS, SPELLS_DEF, ELEMENTS, ELEMENT_OPPOSITES } from '../constants';
import { getFloorMaxHP, getFloorElement, calcDamage, computePactMultiplier, canAffordSpellCost, deductSpellCost } from './computed';
import { computeEffects, hasSpecial, SPECIAL_EFFECTS } from '../upgrade-effects';
export interface CombatSlice {
// State
currentFloor: number;
floorHP: number;
floorMaxHP: number;
maxFloorReached: number;
activeSpell: string;
currentAction: GameAction;
castProgress: number;
// Actions
setAction: (action: GameAction) => void;
setSpell: (spellId: string) => void;
getDamage: (spellId: string) => number;
// Internal combat processing
processCombat: (deltaHours: number) => Partial<GameState>;
}
export const createCombatSlice = (
set: StateCreator<GameState>['set'],
get: () => GameState
): CombatSlice => ({
currentFloor: 1,
floorHP: getFloorMaxHP(1),
floorMaxHP: getFloorMaxHP(1),
maxFloorReached: 1,
activeSpell: 'manaBolt',
currentAction: 'meditate',
castProgress: 0,
setAction: (action: GameAction) => {
set((state) => ({
currentAction: action,
meditateTicks: action === 'meditate' ? state.meditateTicks : 0,
}));
},
setSpell: (spellId: string) => {
const state = get();
if (state.spells[spellId]?.learned) {
set({ activeSpell: spellId });
}
},
getDamage: (spellId: string) => {
const state = get();
const floorElem = getFloorElement(state.currentFloor);
return calcDamage(state, spellId, floorElem);
},
processCombat: (deltaHours: number) => {
const state = get();
if (state.currentAction !== 'climb') return {};
const spellId = state.activeSpell;
const spellDef = SPELLS_DEF[spellId];
if (!spellDef) return {};
const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
const baseAttackSpeed = 1 + (state.skills.quickCast || 0) * 0.05;
const totalAttackSpeed = baseAttackSpeed * effects.attackSpeedMultiplier;
const spellCastSpeed = spellDef.castSpeed || 1;
const progressPerTick = deltaHours * spellCastSpeed * totalAttackSpeed;
let castProgress = (state.castProgress || 0) + progressPerTick;
let rawMana = state.rawMana;
let elements = state.elements;
let totalManaGathered = state.totalManaGathered;
let currentFloor = state.currentFloor;
let floorHP = state.floorHP;
let floorMaxHP = state.floorMaxHP;
let maxFloorReached = state.maxFloorReached;
let signedPacts = state.signedPacts;
let pendingPactOffer = state.pendingPactOffer;
const log = [...state.log];
const skills = state.skills;
const floorElement = getFloorElement(currentFloor);
while (castProgress >= 1 && canAffordSpellCost(spellDef.cost, rawMana, elements)) {
// Deduct cost
const afterCost = deductSpellCost(spellDef.cost, rawMana, elements);
rawMana = afterCost.rawMana;
elements = afterCost.elements;
totalManaGathered += spellDef.cost.amount;
// Calculate damage
let dmg = calcDamage(state, spellId, floorElement);
dmg = dmg * effects.baseDamageMultiplier + effects.baseDamageBonus;
// Executioner: +100% damage to enemies below 25% HP
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && floorHP / floorMaxHP < 0.25) {
dmg *= 2;
}
// Berserker: +50% damage when below 50% mana
const maxMana = 100; // Would need proper max mana calculation
if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
dmg *= 1.5;
}
// Spell echo - chance to cast again
const echoChance = (skills.spellEcho || 0) * 0.1;
if (Math.random() < echoChance) {
dmg *= 2;
log.unshift('✨ Spell Echo! Double damage!');
}
// Apply damage
floorHP = Math.max(0, floorHP - dmg);
castProgress -= 1;
if (floorHP <= 0) {
const wasGuardian = GUARDIANS[currentFloor];
if (wasGuardian && !signedPacts.includes(currentFloor)) {
pendingPactOffer = currentFloor;
log.unshift(`⚔️ ${wasGuardian.name} defeated! They offer a pact...`);
} else if (!wasGuardian) {
if (currentFloor % 5 === 0) {
log.unshift(`🏰 Floor ${currentFloor} cleared!`);
}
}
currentFloor = currentFloor + 1;
if (currentFloor > 100) currentFloor = 100;
floorMaxHP = getFloorMaxHP(currentFloor);
floorHP = floorMaxHP;
maxFloorReached = Math.max(maxFloorReached, currentFloor);
castProgress = 0;
}
}
return {
rawMana,
elements,
totalManaGathered,
currentFloor,
floorHP,
floorMaxHP,
maxFloorReached,
signedPacts,
pendingPactOffer,
castProgress,
log,
};
},
});
+322
View File
@@ -0,0 +1,322 @@
// ─── Computed Stats Functions ─────────────────────────────────────────────────
import type { GameState } from '../types';
import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF, PRESTIGE_DEF, ELEMENT_OPPOSITES } from '../constants';
import { computeEffects } from '../upgrade-effects';
import { getTierMultiplier } from '../skill-evolution';
// Helper to get effective skill level accounting for tiers
export function getEffectiveSkillLevel(
skills: Record<string, number>,
baseSkillId: string,
skillTiers: Record<string, number> = {}
): { level: number; tier: number; tierMultiplier: number } {
const currentTier = skillTiers[baseSkillId] || 1;
const tieredSkillId = currentTier > 1 ? `${baseSkillId}_t${currentTier}` : baseSkillId;
const level = skills[tieredSkillId] || skills[baseSkillId] || 0;
const tierMultiplier = Math.pow(10, currentTier - 1);
return { level, tier: currentTier, tierMultiplier };
}
export function computeMaxMana(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers'>,
effects?: ReturnType<typeof computeEffects>
): number {
const pu = state.prestigeUpgrades;
const skillTiers = state.skillTiers || {};
const skillUpgrades = state.skillUpgrades || {};
const manaWellLevel = getEffectiveSkillLevel(state.skills, 'manaWell', skillTiers);
const base =
100 +
manaWellLevel.level * 100 * manaWellLevel.tierMultiplier +
(pu.manaWell || 0) * 500;
const computedEffects = effects ?? computeEffects(skillUpgrades, skillTiers);
return Math.floor((base + computedEffects.maxManaBonus) * computedEffects.maxManaMultiplier);
}
export function computeElementMax(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers'>,
effects?: ReturnType<typeof computeEffects>
): number {
const pu = state.prestigeUpgrades;
const skillTiers = state.skillTiers || {};
const skillUpgrades = state.skillUpgrades || {};
const elemAttuneLevel = getEffectiveSkillLevel(state.skills, 'elemAttune', skillTiers);
const base = 10 + elemAttuneLevel.level * 50 * elemAttuneLevel.tierMultiplier + (pu.elementalAttune || 0) * 25;
const computedEffects = effects ?? computeEffects(skillUpgrades, skillTiers);
return Math.floor((base + computedEffects.elementCapBonus) * computedEffects.elementCapMultiplier);
}
export function computeRegen(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers'>,
effects?: ReturnType<typeof computeEffects>
): number {
const pu = state.prestigeUpgrades;
const skillTiers = state.skillTiers || {};
const skillUpgrades = state.skillUpgrades || {};
const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1;
const manaFlowLevel = getEffectiveSkillLevel(state.skills, 'manaFlow', skillTiers);
const manaSpringLevel = getEffectiveSkillLevel(state.skills, 'manaSpring', skillTiers);
const base =
2 +
manaFlowLevel.level * 1 * manaFlowLevel.tierMultiplier +
manaSpringLevel.level * 2 * manaSpringLevel.tierMultiplier +
(pu.manaFlow || 0) * 0.5;
let regen = base * temporalBonus;
const computedEffects = effects ?? computeEffects(skillUpgrades, skillTiers);
regen = (regen + computedEffects.regenBonus + computedEffects.permanentRegenBonus) * computedEffects.regenMultiplier;
return regen;
}
export function computeClickMana(
state: Pick<GameState, 'skills' | 'skillUpgrades' | 'skillTiers'>,
effects?: ReturnType<typeof computeEffects>
): number {
const skillTiers = state.skillTiers || {};
const skillUpgrades = state.skillUpgrades || {};
const manaTapLevel = getEffectiveSkillLevel(state.skills, 'manaTap', skillTiers);
const manaSurgeLevel = getEffectiveSkillLevel(state.skills, 'manaSurge', skillTiers);
const base =
1 +
manaTapLevel.level * 1 * manaTapLevel.tierMultiplier +
manaSurgeLevel.level * 3 * manaSurgeLevel.tierMultiplier;
const computedEffects = effects ?? computeEffects(skillUpgrades, skillTiers);
return Math.floor((base + computedEffects.clickManaBonus) * computedEffects.clickManaMultiplier);
}
// Elemental damage bonus
export function getElementalBonus(spellElem: string, floorElem: string): number {
if (spellElem === 'raw') return 1.0;
if (spellElem === floorElem) return 1.25; // Same element: +25% damage
if (ELEMENT_OPPOSITES[floorElem] === spellElem) return 1.5; // Super effective
if (ELEMENT_OPPOSITES[spellElem] === floorElem) return 0.75; // Weak
return 1.0;
}
// Compute the pact multiplier with interference/synergy system
export function computePactMultiplier(
state: Pick<GameState, 'signedPacts' | 'pactInterferenceMitigation' | 'signedPactDetails'>
): number {
const { signedPacts, pactInterferenceMitigation = 0 } = state;
if (signedPacts.length === 0) return 1.0;
let baseMult = 1.0;
for (const floor of signedPacts) {
const guardian = GUARDIANS[floor];
if (guardian) {
baseMult *= guardian.damageMultiplier;
}
}
if (signedPacts.length === 1) return baseMult;
const numAdditionalPacts = signedPacts.length - 1;
const basePenalty = 0.5 * numAdditionalPacts;
const mitigationReduction = Math.min(pactInterferenceMitigation, 5) * 0.1;
const effectivePenalty = Math.max(0, basePenalty - mitigationReduction);
if (pactInterferenceMitigation >= 5) {
const synergyBonus = (pactInterferenceMitigation - 5) * 0.1;
return baseMult * (1 + synergyBonus);
}
return baseMult * (1 - effectivePenalty);
}
// Compute the insight multiplier from signed pacts
export function computePactInsightMultiplier(
state: Pick<GameState, 'signedPacts' | 'pactInterferenceMitigation'>
): number {
const { signedPacts, pactInterferenceMitigation = 0 } = state;
if (signedPacts.length === 0) return 1.0;
let mult = 1.0;
for (const floor of signedPacts) {
const guardian = GUARDIANS[floor];
if (guardian) {
mult *= guardian.insightMultiplier;
}
}
if (signedPacts.length > 1) {
const numAdditionalPacts = signedPacts.length - 1;
const basePenalty = 0.5 * numAdditionalPacts;
const mitigationReduction = Math.min(pactInterferenceMitigation, 5) * 0.1;
const effectivePenalty = Math.max(0, basePenalty - mitigationReduction);
if (pactInterferenceMitigation >= 5) {
const synergyBonus = (pactInterferenceMitigation - 5) * 0.1;
return mult * (1 + synergyBonus);
}
return mult * (1 - effectivePenalty);
}
return mult;
}
export function calcDamage(
state: Pick<GameState, 'skills' | 'signedPacts' | 'pactInterferenceMitigation' | 'signedPactDetails' | 'skillUpgrades' | 'skillTiers'>,
spellId: string,
floorElem?: string,
effects?: ReturnType<typeof computeEffects>
): number {
const sp = SPELLS_DEF[spellId];
if (!sp) return 5;
const skillTiers = state.skillTiers || {};
const skillUpgrades = state.skillUpgrades || {};
const computedEffects = effects ?? computeEffects(skillUpgrades, skillTiers);
// Get effective skill levels with tier multipliers
const combatTrainLevel = getEffectiveSkillLevel(state.skills, 'combatTrain', skillTiers);
const arcaneFuryLevel = getEffectiveSkillLevel(state.skills, 'arcaneFury', skillTiers);
const elemMasteryLevel = getEffectiveSkillLevel(state.skills, 'elementalMastery', skillTiers);
const guardianBaneLevel = getEffectiveSkillLevel(state.skills, 'guardianBane', skillTiers);
const precisionLevel = getEffectiveSkillLevel(state.skills, 'precision', skillTiers);
// Base damage from spell + combat training
const baseDmg = sp.dmg + combatTrainLevel.level * 5 * combatTrainLevel.tierMultiplier;
// Spell damage multiplier from arcane fury
const pct = 1 + arcaneFuryLevel.level * 0.1 * arcaneFuryLevel.tierMultiplier;
// Elemental mastery bonus
const elemMasteryBonus = 1 + elemMasteryLevel.level * 0.15 * elemMasteryLevel.tierMultiplier;
// Guardian bane bonus (only for guardian floors)
const guardianBonus = floorElem && Object.values(GUARDIANS).find(g => g.element === floorElem)
? 1 + guardianBaneLevel.level * 0.2 * guardianBaneLevel.tierMultiplier
: 1;
// Crit chance from precision
const skillCritChance = precisionLevel.level * 0.05 * precisionLevel.tierMultiplier;
const totalCritChance = skillCritChance + computedEffects.critChanceBonus;
// Pact multiplier
const pactMult = computePactMultiplier(state);
// Calculate base damage
let damage = baseDmg * pct * pactMult * elemMasteryBonus * guardianBonus;
// Apply upgrade effects: base damage multiplier and bonus
damage = damage * computedEffects.baseDamageMultiplier + computedEffects.baseDamageBonus;
// Apply elemental damage multiplier from upgrades
damage *= computedEffects.elementalDamageMultiplier;
// Apply elemental bonus for floor
if (floorElem) {
damage *= getElementalBonus(sp.elem, floorElem);
}
// Apply critical hit
if (Math.random() < totalCritChance) {
damage *= computedEffects.critDamageMultiplier;
}
return damage;
}
export function calcInsight(state: Pick<GameState, 'maxFloorReached' | 'totalManaGathered' | 'signedPacts' | 'prestigeUpgrades' | 'skills'>): number {
const pu = state.prestigeUpgrades;
const skillBonus = 1 + (state.skills.insightHarvest || 0) * 0.1;
const mult = (1 + (pu.insightAmp || 0) * 0.25) * skillBonus;
return Math.floor(
(state.maxFloorReached * 15 + state.totalManaGathered / 500 + state.signedPacts.length * 150) * mult
);
}
export function getMeditationBonus(meditateTicks: number, skills: Record<string, number>, meditationEfficiency: number = 1): number {
const hasMeditation = skills.meditation === 1;
const hasDeepTrance = skills.deepTrance === 1;
const hasVoidMeditation = skills.voidMeditation === 1;
const hours = meditateTicks * 0.04; // HOURS_PER_TICK
let bonus = 1 + Math.min(hours / 4, 0.5);
if (hasMeditation && hours >= 4) {
bonus = 2.5;
}
if (hasDeepTrance && hours >= 6) {
bonus = 3.0;
}
if (hasVoidMeditation && hours >= 8) {
bonus = 5.0;
}
bonus *= meditationEfficiency;
return bonus;
}
export function getIncursionStrength(day: number, hour: number): number {
const INCURSION_START_DAY = 20;
const MAX_DAY = 30;
if (day < INCURSION_START_DAY) return 0;
const totalHours = (day - INCURSION_START_DAY) * 24 + hour;
const maxHours = (MAX_DAY - INCURSION_START_DAY) * 24;
return Math.min(0.95, (totalHours / maxHours) * 0.95);
}
export function getFloorMaxHP(floor: number): number {
if (GUARDIANS[floor]) return GUARDIANS[floor].hp;
const baseHP = 100;
const floorScaling = floor * 50;
const exponentialScaling = Math.pow(floor, 1.7);
return Math.floor(baseHP + floorScaling + exponentialScaling);
}
export function getFloorElement(floor: number): string {
const FLOOR_ELEM_CYCLE = ["fire", "water", "air", "earth", "light", "dark", "life", "death"];
return FLOOR_ELEM_CYCLE[(floor - 1) % 8];
}
// Formatting utilities
export function fmt(n: number): string {
if (!isFinite(n) || isNaN(n)) return '0';
if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B';
if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M';
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
return Math.floor(n).toString();
}
export function fmtDec(n: number, d: number = 1): string {
return isFinite(n) ? n.toFixed(d) : '0';
}
// Check if player can afford spell cost
export function canAffordSpellCost(
cost: { type: 'raw' | 'element'; element?: string; amount: number },
rawMana: number,
elements: Record<string, { current: number; max: number; unlocked: boolean }>
): boolean {
if (cost.type === 'raw') {
return rawMana >= cost.amount;
} else {
const elem = elements[cost.element || ''];
return elem && elem.unlocked && elem.current >= cost.amount;
}
}
+644
View File
@@ -0,0 +1,644 @@
// ─── Crafting Store Slice ────────────────────────────────────────────────────────
// Handles equipment, enchantments, and crafting progress
import type {
EquipmentInstance,
AppliedEnchantment,
EnchantmentDesign,
DesignEffect,
DesignProgress,
PreparationProgress,
ApplicationProgress,
EquipmentSpellState
} from '../types';
import {
EQUIPMENT_TYPES,
EQUIPMENT_SLOTS,
type EquipmentSlot,
type EquipmentTypeDef,
getEquipmentType,
calculateRarity
} from '../data/equipment';
import {
ENCHANTMENT_EFFECTS,
getEnchantmentEffect,
canApplyEffect,
calculateEffectCapacityCost,
type EnchantmentEffectDef
} from '../data/enchantment-effects';
import { SPELLS_DEF } from '../constants';
import type { StateCreator } from 'zustand';
// ─── Helper Functions ────────────────────────────────────────────────────────────
let instanceIdCounter = 0;
function generateInstanceId(): string {
return `equip_${Date.now()}_${++instanceIdCounter}`;
}
let designIdCounter = 0;
function generateDesignId(): string {
return `design_${Date.now()}_${++designIdCounter}`;
}
// Calculate efficiency bonus from skills
function getEnchantEfficiencyBonus(skills: Record<string, number>): number {
const enchantingLevel = skills.enchanting || 0;
const efficientEnchantLevel = skills.efficientEnchant || 0;
// 2% per enchanting level + 5% per efficient enchant level
return (enchantingLevel * 0.02) + (efficientEnchantLevel * 0.05);
}
// Calculate design time based on effects
function calculateDesignTime(effects: DesignEffect[]): number {
const totalCapacity = effects.reduce((sum, e) => sum + e.capacityCost, 0);
return Math.max(1, Math.floor(totalCapacity / 10)); // Hours
}
// Calculate preparation time for equipment
function calculatePreparationTime(equipmentType: string): number {
const typeDef = getEquipmentType(equipmentType);
if (!typeDef) return 1;
return Math.max(1, Math.floor(typeDef.baseCapacity / 5)); // Hours
}
// Calculate preparation mana cost
function calculatePreparationManaCost(equipmentType: string): number {
const typeDef = getEquipmentType(equipmentType);
if (!typeDef) return 50;
return typeDef.baseCapacity * 5;
}
// Calculate application time based on effects
function calculateApplicationTime(effects: DesignEffect[], skills: Record<string, number>): number {
const totalCapacity = effects.reduce((sum, e) => sum + e.capacityCost, 0);
const speedBonus = 1 + (skills.enchantSpeed || 0) * 0.1;
return Math.max(4, Math.floor(totalCapacity / 20 * 24 / speedBonus)); // Hours (days * 24)
}
// Calculate mana per hour for application
function calculateApplicationManaPerHour(effects: DesignEffect[]): number {
const totalCapacity = effects.reduce((sum, e) => sum + e.capacityCost, 0);
return Math.max(1, Math.floor(totalCapacity * 0.5));
}
// Create a new equipment instance
export function createEquipmentInstance(typeId: string, name?: string): EquipmentInstance {
const typeDef = getEquipmentType(typeId);
if (!typeDef) {
throw new Error(`Unknown equipment type: ${typeId}`);
}
return {
instanceId: generateInstanceId(),
typeId,
name: name || typeDef.name,
enchantments: [],
usedCapacity: 0,
totalCapacity: typeDef.baseCapacity,
rarity: 'common',
quality: 100, // Full quality for new items
};
}
// Get spells from equipment
export function getSpellsFromEquipment(equipment: EquipmentInstance): string[] {
const spells: string[] = [];
for (const ench of equipment.enchantments) {
const effectDef = getEnchantmentEffect(ench.effectId);
if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) {
spells.push(effectDef.effect.spellId);
}
}
return spells;
}
// Compute total effects from equipment
export function computeEquipmentEffects(equipment: EquipmentInstance[]): Record<string, number> {
const effects: Record<string, number> = {};
const multipliers: Record<string, number> = {};
const specials: Set<string> = new Set();
for (const equip of equipment) {
for (const ench of equip.enchantments) {
const effectDef = getEnchantmentEffect(ench.effectId);
if (!effectDef) continue;
const value = (effectDef.effect.value || 0) * ench.stacks;
if (effectDef.effect.type === 'bonus' && effectDef.effect.stat) {
effects[effectDef.effect.stat] = (effects[effectDef.effect.stat] || 0) + value;
} else if (effectDef.effect.type === 'multiplier' && effectDef.effect.stat) {
multipliers[effectDef.effect.stat] = (multipliers[effectDef.effect.stat] || 1) * Math.pow(value, ench.stacks);
} else if (effectDef.effect.type === 'special' && effectDef.effect.specialId) {
specials.add(effectDef.effect.specialId);
}
}
}
// Apply multipliers to bonus effects
for (const [stat, mult] of Object.entries(multipliers)) {
effects[`${stat}_multiplier`] = mult;
}
// Add special effect flags
for (const special of specials) {
effects[`special_${special}`] = 1;
}
return effects;
}
// ─── Store Interface ─────────────────────────────────────────────────────────────
export interface CraftingState {
// Equipment instances
equippedInstances: Record<string, string | null>; // slot -> instanceId
equipmentInstances: Record<string, EquipmentInstance>; // instanceId -> instance
// Enchantment designs
enchantmentDesigns: EnchantmentDesign[];
// Crafting progress
designProgress: DesignProgress | null;
preparationProgress: PreparationProgress | null;
applicationProgress: ApplicationProgress | null;
// Equipment spell states
equipmentSpellStates: EquipmentSpellState[];
}
export interface CraftingActions {
// Equipment management
createEquipment: (typeId: string, slot?: EquipmentSlot) => EquipmentInstance;
equipInstance: (instanceId: string, slot: EquipmentSlot) => void;
unequipSlot: (slot: EquipmentSlot) => void;
deleteInstance: (instanceId: string) => void;
// Enchantment design
startDesign: (name: string, equipmentType: string, effects: DesignEffect[]) => void;
cancelDesign: () => void;
deleteDesign: (designId: string) => void;
// Equipment preparation
startPreparation: (instanceId: string) => void;
cancelPreparation: () => void;
// Enchantment application
startApplication: (instanceId: string, designId: string) => void;
pauseApplication: () => void;
resumeApplication: () => void;
cancelApplication: () => void;
// Tick processing
processDesignTick: (hours: number) => void;
processPreparationTick: (hours: number, manaAvailable: number) => number; // Returns mana used
processApplicationTick: (hours: number, manaAvailable: number) => number; // Returns mana used
// Getters
getEquippedInstance: (slot: EquipmentSlot) => EquipmentInstance | null;
getAllEquipped: () => EquipmentInstance[];
getAvailableSpells: () => string[];
getEquipmentEffects: () => Record<string, number>;
}
export type CraftingStore = CraftingState & CraftingActions;
// ─── Initial State ──────────────────────────────────────────────────────────────
export const initialCraftingState: CraftingState = {
equippedInstances: {
mainHand: null,
offHand: null,
head: null,
body: null,
hands: null,
feet: null,
accessory1: null,
accessory2: null,
},
equipmentInstances: {},
enchantmentDesigns: [],
designProgress: null,
preparationProgress: null,
applicationProgress: null,
equipmentSpellStates: [],
};
// ─── Store Slice Creator ────────────────────────────────────────────────────────
// We need to access skills from the main store - this is a workaround
// The store will pass skills when calling these methods
let cachedSkills: Record<string, number> = {};
export function setCachedSkills(skills: Record<string, number>): void {
cachedSkills = skills;
}
export const createCraftingSlice: StateCreator<CraftingStore, [], [], CraftingStore> = (set, get) => ({
...initialCraftingState,
// Equipment management
createEquipment: (typeId: string, slot?: EquipmentSlot) => {
const instance = createEquipmentInstance(typeId);
set((state) => ({
equipmentInstances: {
...state.equipmentInstances,
[instance.instanceId]: instance,
},
}));
// Auto-equip if slot provided
if (slot) {
get().equipInstance(instance.instanceId, slot);
}
return instance;
},
equipInstance: (instanceId: string, slot: EquipmentSlot) => {
const instance = get().equipmentInstances[instanceId];
if (!instance) return;
const typeDef = getEquipmentType(instance.typeId);
if (!typeDef) return;
// Check if equipment can go in this slot
if (typeDef.slot !== slot) {
// For accessories, both accessory1 and accessory2 are valid
if (typeDef.category !== 'accessory' || (slot !== 'accessory1' && slot !== 'accessory2')) {
return;
}
}
set((state) => ({
equippedInstances: {
...state.equippedInstances,
[slot]: instanceId,
},
}));
},
unequipSlot: (slot: EquipmentSlot) => {
set((state) => ({
equippedInstances: {
...state.equippedInstances,
[slot]: null,
},
}));
},
deleteInstance: (instanceId: string) => {
set((state) => {
const newInstanceMap = { ...state.equipmentInstances };
delete newInstanceMap[instanceId];
// Remove from equipped slots
const newEquipped = { ...state.equippedInstances };
for (const slot of EQUIPMENT_SLOTS) {
if (newEquipped[slot] === instanceId) {
newEquipped[slot] = null;
}
}
return {
equipmentInstances: newInstanceMap,
equippedInstances: newEquipped,
};
});
},
// Enchantment design
startDesign: (name: string, equipmentType: string, effects: DesignEffect[]) => {
const totalCapacity = effects.reduce((sum, e) => sum + e.capacityCost, 0);
const designTime = calculateDesignTime(effects);
const design: EnchantmentDesign = {
id: generateDesignId(),
name,
equipmentType,
effects,
totalCapacityUsed: totalCapacity,
designTime,
created: Date.now(),
};
set((state) => ({
enchantmentDesigns: [...state.enchantmentDesigns, design],
designProgress: {
designId: design.id,
progress: 0,
required: designTime,
},
}));
},
cancelDesign: () => {
const progress = get().designProgress;
if (!progress) return;
set((state) => ({
designProgress: null,
enchantmentDesigns: state.enchantmentDesigns.filter(d => d.id !== progress.designId),
}));
},
deleteDesign: (designId: string) => {
set((state) => ({
enchantmentDesigns: state.enchantmentDesigns.filter(d => d.id !== designId),
}));
},
// Equipment preparation
startPreparation: (instanceId: string) => {
const instance = get().equipmentInstances[instanceId];
if (!instance) return;
const prepTime = calculatePreparationTime(instance.typeId);
const manaCost = calculatePreparationManaCost(instance.typeId);
set({
preparationProgress: {
equipmentInstanceId: instanceId,
progress: 0,
required: prepTime,
manaCostPaid: 0,
},
});
},
cancelPreparation: () => {
set({ preparationProgress: null });
},
// Enchantment application
startApplication: (instanceId: string, designId: string) => {
const instance = get().equipmentInstances[instanceId];
const design = get().enchantmentDesigns.find(d => d.id === designId);
if (!instance || !design) return;
const appTime = calculateApplicationTime(design.effects, cachedSkills);
const manaPerHour = calculateApplicationManaPerHour(design.effects);
set({
applicationProgress: {
equipmentInstanceId: instanceId,
designId,
progress: 0,
required: appTime,
manaPerHour,
paused: false,
manaSpent: 0,
},
});
},
pauseApplication: () => {
const progress = get().applicationProgress;
if (!progress) return;
set({
applicationProgress: { ...progress, paused: true },
});
},
resumeApplication: () => {
const progress = get().applicationProgress;
if (!progress) return;
set({
applicationProgress: { ...progress, paused: false },
});
},
cancelApplication: () => {
set({ applicationProgress: null });
},
// Tick processing
processDesignTick: (hours: number) => {
const progress = get().designProgress;
if (!progress) return;
const newProgress = progress.progress + hours;
if (newProgress >= progress.required) {
// Design complete
set({ designProgress: null });
} else {
set({
designProgress: { ...progress, progress: newProgress },
});
}
},
processPreparationTick: (hours: number, manaAvailable: number) => {
const progress = get().preparationProgress;
if (!progress) return 0;
const instance = get().equipmentInstances[progress.equipmentInstanceId];
if (!instance) {
set({ preparationProgress: null });
return 0;
}
const totalManaCost = calculatePreparationManaCost(instance.typeId);
const remainingManaCost = totalManaCost - progress.manaCostPaid;
const manaToPay = Math.min(manaAvailable, remainingManaCost);
if (manaToPay < remainingManaCost) {
// Not enough mana, just pay what we can
set({
preparationProgress: {
...progress,
manaCostPaid: progress.manaCostPaid + manaToPay,
},
});
return manaToPay;
}
// Pay remaining mana and progress
const newProgress = progress.progress + hours;
if (newProgress >= progress.required) {
// Preparation complete - clear enchantments
set((state) => ({
preparationProgress: null,
equipmentInstances: {
...state.equipmentInstances,
[instance.instanceId]: {
...instance,
enchantments: [],
usedCapacity: 0,
rarity: 'common',
},
},
}));
} else {
set({
preparationProgress: {
...progress,
progress: newProgress,
manaCostPaid: progress.manaCostPaid + manaToPay,
},
});
}
return manaToPay;
},
processApplicationTick: (hours: number, manaAvailable: number) => {
const progress = get().applicationProgress;
if (!progress || progress.paused) return 0;
const design = get().enchantmentDesigns.find(d => d.id === progress.designId);
const instance = get().equipmentInstances[progress.equipmentInstanceId];
if (!design || !instance) {
set({ applicationProgress: null });
return 0;
}
const manaNeeded = progress.manaPerHour * hours;
const manaToUse = Math.min(manaAvailable, manaNeeded);
if (manaToUse < manaNeeded) {
// Not enough mana - pause and save progress
set({
applicationProgress: {
...progress,
manaSpent: progress.manaSpent + manaToUse,
},
});
return manaToUse;
}
const newProgress = progress.progress + hours;
if (newProgress >= progress.required) {
// Application complete - apply enchantments
const efficiencyBonus = getEnchantEfficiencyBonus(cachedSkills);
const newEnchantments: AppliedEnchantment[] = design.effects.map(e => ({
effectId: e.effectId,
stacks: e.stacks,
actualCost: calculateEffectCapacityCost(e.effectId, e.stacks, efficiencyBonus),
}));
const totalUsedCapacity = newEnchantments.reduce((sum, e) => sum + e.actualCost, 0);
set((state) => ({
applicationProgress: null,
equipmentInstances: {
...state.equipmentInstances,
[instance.instanceId]: {
...instance,
enchantments: newEnchantments,
usedCapacity: totalUsedCapacity,
rarity: calculateRarity(newEnchantments),
},
},
}));
} else {
set({
applicationProgress: {
...progress,
progress: newProgress,
manaSpent: progress.manaSpent + manaToUse,
},
});
}
return manaToUse;
},
// Getters
getEquippedInstance: (slot: EquipmentSlot) => {
const state = get();
const instanceId = state.equippedInstances[slot];
if (!instanceId) return null;
return state.equipmentInstances[instanceId] || null;
},
getAllEquipped: () => {
const state = get();
const equipped: EquipmentInstance[] = [];
for (const slot of EQUIPMENT_SLOTS) {
const instanceId = state.equippedInstances[slot];
if (instanceId && state.equipmentInstances[instanceId]) {
equipped.push(state.equipmentInstances[instanceId]);
}
}
return equipped;
},
getAvailableSpells: () => {
const equipped = get().getAllEquipped();
const spells: string[] = [];
for (const equip of equipped) {
spells.push(...getSpellsFromEquipment(equip));
}
return spells;
},
getEquipmentEffects: () => {
return computeEquipmentEffects(get().getAllEquipped());
},
});
// ─── Starting Equipment Factory ────────────────────────────────────────────────
export function createStartingEquipment(): {
equippedInstances: Record<string, string | null>;
equipmentInstances: Record<string, EquipmentInstance>;
} {
const instances: EquipmentInstance[] = [];
// Create starting equipment
const basicStaff = createEquipmentInstance('basicStaff');
basicStaff.enchantments = [{
effectId: 'spell_manaBolt',
stacks: 1,
actualCost: 50, // Fills the staff completely
}];
basicStaff.usedCapacity = 50;
basicStaff.rarity = 'uncommon';
instances.push(basicStaff);
const civilianShirt = createEquipmentInstance('civilianShirt');
instances.push(civilianShirt);
const civilianGloves = createEquipmentInstance('civilianGloves');
instances.push(civilianGloves);
const civilianShoes = createEquipmentInstance('civilianShoes');
instances.push(civilianShoes);
// Build instance map
const equipmentInstances: Record<string, EquipmentInstance> = {};
for (const inst of instances) {
equipmentInstances[inst.instanceId] = inst;
}
// Build equipped map
const equippedInstances: Record<string, string | null> = {
mainHand: basicStaff.instanceId,
offHand: null,
head: null,
body: civilianShirt.instanceId,
hands: civilianGloves.instanceId,
feet: civilianShoes.instanceId,
accessory1: null,
accessory2: null,
};
return { equippedInstances, equipmentInstances };
}
+9
View File
@@ -0,0 +1,9 @@
// ─── Store Module Exports ─────────────────────────────────────────────────────
// Re-exports from main store and adds new computed utilities
// This allows gradual migration while keeping existing functionality
// Re-export everything from the main store
export * from '../store';
// Export new computed utilities
export * from './computed';
+197
View File
@@ -0,0 +1,197 @@
// ─── Mana Slice ───────────────────────────────────────────────────────────────
// Manages raw mana, elements, and meditation
import type { StateCreator } from 'zustand';
import type { GameState, ElementState, SpellCost } from '../types';
import { ELEMENTS, MANA_PER_ELEMENT, BASE_UNLOCKED_ELEMENTS } from '../constants';
import { computeMaxMana, computeElementMax, computeClickMana, canAffordSpellCost, getMeditationBonus } from './computed';
import { computeEffects } from '../upgrade-effects';
export interface ManaSlice {
// State
rawMana: number;
totalManaGathered: number;
meditateTicks: number;
elements: Record<string, ElementState>;
// Actions
gatherMana: () => void;
convertMana: (element: string, amount: number) => void;
unlockElement: (element: string) => void;
craftComposite: (target: string) => void;
// Computed getters
getMaxMana: () => number;
getRegen: () => number;
getClickMana: () => number;
getMeditationMultiplier: () => number;
}
export const createManaSlice = (
set: StateCreator<GameState>['set'],
get: () => GameState
): ManaSlice => ({
rawMana: 10,
totalManaGathered: 0,
meditateTicks: 0,
elements: (() => {
const elems: Record<string, ElementState> = {};
const pu = get().prestigeUpgrades;
const elemMax = computeElementMax(get());
Object.keys(ELEMENTS).forEach((k) => {
const isUnlocked = BASE_UNLOCKED_ELEMENTS.includes(k);
let startAmount = 0;
if (isUnlocked && pu.elemStart) {
startAmount = pu.elemStart * 5;
}
elems[k] = {
current: startAmount,
max: elemMax,
unlocked: isUnlocked,
};
});
return elems;
})(),
gatherMana: () => {
const state = get();
let cm = computeClickMana(state);
// Mana overflow bonus
const overflowBonus = 1 + (state.skills.manaOverflow || 0) * 0.25;
cm = Math.floor(cm * overflowBonus);
const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
const max = computeMaxMana(state, effects);
// Mana Echo: 10% chance to gain double mana from clicks
const hasManaEcho = effects.specials?.has('MANA_ECHO') ?? false;
if (hasManaEcho && Math.random() < 0.1) {
cm *= 2;
}
set({
rawMana: Math.min(state.rawMana + cm, max),
totalManaGathered: state.totalManaGathered + cm,
});
},
convertMana: (element: string, amount: number = 1) => {
const state = get();
const e = state.elements[element];
if (!e?.unlocked) return;
const cost = MANA_PER_ELEMENT * amount;
if (state.rawMana < cost) return;
if (e.current >= e.max) return;
const canConvert = Math.min(
amount,
Math.floor(state.rawMana / MANA_PER_ELEMENT),
e.max - e.current
);
set({
rawMana: state.rawMana - canConvert * MANA_PER_ELEMENT,
elements: {
...state.elements,
[element]: { ...e, current: e.current + canConvert },
},
});
},
unlockElement: (element: string) => {
const state = get();
if (state.elements[element]?.unlocked) return;
const cost = 500;
if (state.rawMana < cost) return;
set({
rawMana: state.rawMana - cost,
elements: {
...state.elements,
[element]: { ...state.elements[element], unlocked: true },
},
});
},
craftComposite: (target: string) => {
const state = get();
const edef = ELEMENTS[target];
if (!edef?.recipe) return;
const recipe = edef.recipe;
const costs: Record<string, number> = {};
recipe.forEach((r) => {
costs[r] = (costs[r] || 0) + 1;
});
// Check ingredients
for (const [r, amt] of Object.entries(costs)) {
if ((state.elements[r]?.current || 0) < amt) return;
}
const newElems = { ...state.elements };
for (const [r, amt] of Object.entries(costs)) {
newElems[r] = { ...newElems[r], current: newElems[r].current - amt };
}
// Elemental crafting bonus
const craftBonus = 1 + (state.skills.elemCrafting || 0) * 0.25;
const outputAmount = Math.floor(craftBonus);
const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
const elemMax = computeElementMax(state, effects);
newElems[target] = {
...(newElems[target] || { current: 0, max: elemMax, unlocked: false }),
current: (newElems[target]?.current || 0) + outputAmount,
max: elemMax,
unlocked: true,
};
set({
elements: newElems,
});
},
getMaxMana: () => computeMaxMana(get()),
getRegen: () => {
const state = get();
const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
// This would need proper regen calculation
return 2;
},
getClickMana: () => computeClickMana(get()),
getMeditationMultiplier: () => {
const state = get();
const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
return getMeditationBonus(state.meditateTicks, state.skills, effects.meditationEfficiency);
},
});
// Helper function to deduct spell cost
export function deductSpellCost(
cost: SpellCost,
rawMana: number,
elements: Record<string, ElementState>
): { rawMana: number; elements: Record<string, ElementState> } {
const newElements = { ...elements };
if (cost.type === 'raw') {
return { rawMana: rawMana - cost.amount, elements: newElements };
} else if (cost.element && newElements[cost.element]) {
newElements[cost.element] = {
...newElements[cost.element],
current: newElements[cost.element].current - cost.amount,
};
return { rawMana, elements: newElements };
}
return { rawMana, elements: newElements };
}
export { canAffordSpellCost };
+180
View File
@@ -0,0 +1,180 @@
// ─── Pact Slice ───────────────────────────────────────────────────────────────
// Manages guardian pacts, signing, and mana unlocking
import type { StateCreator } from 'zustand';
import type { GameState } from '../types';
import { GUARDIANS, ELEMENTS } from '../constants';
import { computePactMultiplier, computePactInsightMultiplier } from './computed';
export interface PactSlice {
// State
signedPacts: number[];
pendingPactOffer: number | null;
maxPacts: number;
pactSigningProgress: {
floor: number;
progress: number;
required: number;
manaCost: number;
} | null;
signedPactDetails: Record<number, {
floor: number;
guardianId: string;
signedAt: { day: number; hour: number };
skillLevels: Record<string, number>;
}>;
pactInterferenceMitigation: number;
pactSynergyUnlocked: boolean;
// Actions
acceptPact: (floor: number) => void;
declinePact: (floor: number) => void;
// Computed getters
getPactMultiplier: () => number;
getPactInsightMultiplier: () => number;
}
export const createPactSlice = (
set: StateCreator<GameState>['set'],
get: () => GameState
): PactSlice => ({
signedPacts: [],
pendingPactOffer: null,
maxPacts: 1,
pactSigningProgress: null,
signedPactDetails: {},
pactInterferenceMitigation: 0,
pactSynergyUnlocked: false,
acceptPact: (floor: number) => {
const state = get();
const guardian = GUARDIANS[floor];
if (!guardian || state.signedPacts.includes(floor)) return;
const maxPacts = 1 + (state.prestigeUpgrades.pactCapacity || 0);
if (state.signedPacts.length >= maxPacts) {
set({
log: [`⚠️ Cannot sign more pacts! Maximum: ${maxPacts}.`, ...state.log.slice(0, 49)],
});
return;
}
const baseCost = guardian.signingCost.mana;
const discount = Math.min((state.prestigeUpgrades.pactDiscount || 0) * 0.1, 0.5);
const manaCost = Math.floor(baseCost * (1 - discount));
if (state.rawMana < manaCost) {
set({
log: [`⚠️ Need ${manaCost} mana to sign pact with ${guardian.name}!`, ...state.log.slice(0, 49)],
});
return;
}
const baseTime = guardian.signingCost.time;
const haste = Math.min((state.prestigeUpgrades.pactHaste || 0) * 0.1, 0.5);
const signingTime = Math.max(1, baseTime * (1 - haste));
set({
rawMana: state.rawMana - manaCost,
pactSigningProgress: {
floor,
progress: 0,
required: signingTime,
manaCost,
},
pendingPactOffer: null,
currentAction: 'study',
log: [`📜 Beginning pact signing with ${guardian.name}... (${signingTime}h, ${manaCost} mana)`, ...state.log.slice(0, 49)],
});
},
declinePact: (floor: number) => {
const state = get();
const guardian = GUARDIANS[floor];
if (!guardian) return;
set({
pendingPactOffer: null,
log: [`🚫 Declined pact with ${guardian.name}.`, ...state.log.slice(0, 49)],
});
},
getPactMultiplier: () => computePactMultiplier(get()),
getPactInsightMultiplier: () => computePactInsightMultiplier(get()),
});
// Process pact signing progress (called during tick)
export function processPactSigning(state: GameState, deltaHours: number): Partial<GameState> {
if (!state.pactSigningProgress) return {};
const progress = state.pactSigningProgress.progress + deltaHours;
const log = [...state.log];
if (progress >= state.pactSigningProgress.required) {
const floor = state.pactSigningProgress.floor;
const guardian = GUARDIANS[floor];
if (!guardian || state.signedPacts.includes(floor)) {
return { pactSigningProgress: null };
}
const signedPacts = [...state.signedPacts, floor];
const signedPactDetails = {
...state.signedPactDetails,
[floor]: {
floor,
guardianId: guardian.element,
signedAt: { day: state.day, hour: state.hour },
skillLevels: {},
},
};
// Unlock mana types
let elements = { ...state.elements };
for (const elemId of guardian.unlocksMana) {
if (elements[elemId]) {
elements = {
...elements,
[elemId]: { ...elements[elemId], unlocked: true },
};
}
}
// Check for compound element unlocks
const unlockedSet = new Set(
Object.entries(elements)
.filter(([, e]) => e.unlocked)
.map(([id]) => id)
);
for (const [elemId, elemDef] of Object.entries(ELEMENTS)) {
if (elemDef.recipe && !elements[elemId]?.unlocked) {
const canUnlock = elemDef.recipe.every(comp => unlockedSet.has(comp));
if (canUnlock) {
elements = {
...elements,
[elemId]: { ...elements[elemId], unlocked: true },
};
log.unshift(`🔮 ${elemDef.name} mana unlocked through component synergy!`);
}
}
}
log.unshift(`📜 Pact with ${guardian.name} signed! ${guardian.unlocksMana.map(e => ELEMENTS[e]?.name || e).join(', ')} mana unlocked!`);
return {
signedPacts,
signedPactDetails,
elements,
pactSigningProgress: null,
log,
};
}
return {
pactSigningProgress: {
...state.pactSigningProgress,
progress,
},
};
}
+128
View File
@@ -0,0 +1,128 @@
// ─── Prestige Slice ───────────────────────────────────────────────────────────
// Manages insight, prestige upgrades, and loop resources
import type { StateCreator } from 'zustand';
import type { GameState } from '../types';
import { PRESTIGE_DEF } from '../constants';
export interface PrestigeSlice {
// State
insight: number;
totalInsight: number;
prestigeUpgrades: Record<string, number>;
loopInsight: number;
memorySlots: number;
memories: string[];
// Actions
doPrestige: (id: string) => void;
startNewLoop: () => void;
}
export const createPrestigeSlice = (
set: StateCreator<GameState>['set'],
get: () => GameState
): PrestigeSlice => ({
insight: 0,
totalInsight: 0,
prestigeUpgrades: {},
loopInsight: 0,
memorySlots: 3,
memories: [],
doPrestige: (id: string) => {
const state = get();
const pd = PRESTIGE_DEF[id];
if (!pd) return;
const lvl = state.prestigeUpgrades[id] || 0;
if (lvl >= pd.max || state.insight < pd.cost) return;
const newPU = { ...state.prestigeUpgrades, [id]: lvl + 1 };
set({
insight: state.insight - pd.cost,
prestigeUpgrades: newPU,
memorySlots: id === 'deepMemory' ? state.memorySlots + 1 : state.memorySlots,
maxPacts: id === 'pactCapacity' ? state.maxPacts + 1 : state.maxPacts,
pactInterferenceMitigation: id === 'pactInterference' ? (state.pactInterferenceMitigation || 0) + 1 : state.pactInterferenceMitigation,
log: [`${pd.name} upgraded to Lv.${lvl + 1}!`, ...state.log.slice(0, 49)],
});
},
startNewLoop: () => {
const state = get();
const insightGained = state.loopInsight || calcInsight(state);
const total = state.insight + insightGained;
// Reset to initial state with insight carried over
const pu = state.prestigeUpgrades;
const startFloor = 1 + (pu.spireKey || 0) * 2;
const startRawMana = 10 + (pu.manaWell || 0) * 500 + (pu.quickStart || 0) * 100;
// Reset elements
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
Object.keys(ELEMENTS).forEach((k) => {
elements[k] = {
current: 0,
max: 10 + (pu.elementalAttune || 0) * 25,
unlocked: false,
};
});
// Reset spells - always start with Mana Bolt
const spells: Record<string, { learned: boolean; level: number; studyProgress: number }> = {
manaBolt: { learned: true, level: 1, studyProgress: 0 },
};
// Add random starting spells from spell memory prestige upgrade (purchased with insight)
if (pu.spellMemory) {
const availableSpells = Object.keys(SPELLS_DEF).filter(s => s !== 'manaBolt');
const shuffled = availableSpells.sort(() => Math.random() - 0.5);
for (let i = 0; i < Math.min(pu.spellMemory, shuffled.length); i++) {
spells[shuffled[i]] = { learned: true, level: 1, studyProgress: 0 };
}
}
set({
day: 1,
hour: 0,
gameOver: false,
victory: false,
loopCount: state.loopCount + 1,
rawMana: startRawMana,
totalManaGathered: 0,
meditateTicks: 0,
elements,
currentFloor: startFloor,
floorHP: getFloorMaxHP(startFloor),
floorMaxHP: getFloorMaxHP(startFloor),
maxFloorReached: startFloor,
signedPacts: [],
pendingPactOffer: null,
pactSigningProgress: null,
signedPactDetails: {},
activeSpell: 'manaBolt',
currentAction: 'meditate',
castProgress: 0,
spells,
skills: {},
skillProgress: {},
skillUpgrades: {},
skillTiers: {},
currentStudyTarget: null,
parallelStudyTarget: null,
insight: total,
totalInsight: (state.totalInsight || 0) + insightGained,
loopInsight: 0,
maxPacts: 1 + (pu.pactCapacity || 0),
pactInterferenceMitigation: pu.pactInterference || 0,
memorySlots: 3 + (pu.deepMemory || 0),
log: ['✨ A new loop begins. Your insight grows...', '✨ The loop begins. You start with Mana Bolt.'],
});
},
});
// Need to import these
import { ELEMENTS, SPELLS_DEF } from '../constants';
import { getFloorMaxHP, calcInsight } from './computed';
+346
View File
@@ -0,0 +1,346 @@
// ─── Skill Slice ──────────────────────────────────────────────────────────────
// Manages skills, studying, and skill progress
import type { StateCreator } from 'zustand';
import type { GameState, StudyTarget, SkillUpgradeChoice } from '../types';
import { SKILLS_DEF, SPELLS_DEF, getStudySpeedMultiplier, getStudyCostMultiplier } from '../constants';
import { getUpgradesForSkillAtMilestone, getNextTierSkill, getTierMultiplier } from '../skill-evolution';
import { computeEffects } from '../upgrade-effects';
export interface SkillSlice {
// State
skills: Record<string, number>;
skillProgress: Record<string, number>;
skillUpgrades: Record<string, string[]>;
skillTiers: Record<string, number>;
currentStudyTarget: StudyTarget | null;
parallelStudyTarget: StudyTarget | null;
// Actions
startStudyingSkill: (skillId: string) => void;
startStudyingSpell: (spellId: string) => void;
startParallelStudySkill: (skillId: string) => void;
cancelStudy: () => void;
cancelParallelStudy: () => void;
selectSkillUpgrade: (skillId: string, upgradeId: string) => void;
deselectSkillUpgrade: (skillId: string, upgradeId: string) => void;
commitSkillUpgrades: (skillId: string, upgradeIds: string[], milestone: 5 | 10) => void;
tierUpSkill: (skillId: string) => void;
// Getters
getSkillUpgradeChoices: (skillId: string, milestone: 5 | 10) => { available: SkillUpgradeChoice[]; selected: string[] };
}
export const createSkillSlice = (
set: StateCreator<GameState>['set'],
get: () => GameState
): SkillSlice => ({
skills: {},
skillProgress: {},
skillUpgrades: {},
skillTiers: {},
currentStudyTarget: null,
parallelStudyTarget: null,
startStudyingSkill: (skillId: string) => {
const state = get();
const sk = SKILLS_DEF[skillId];
if (!sk) return;
const currentLevel = state.skills[skillId] || 0;
if (currentLevel >= sk.max) return;
// Check prerequisites
if (sk.req) {
for (const [r, rl] of Object.entries(sk.req)) {
if ((state.skills[r] || 0) < rl) return;
}
}
const costMult = getStudyCostMultiplier(state.skills);
const totalCost = Math.floor(sk.base * (currentLevel + 1) * costMult);
const manaCostPerHour = totalCost / sk.studyTime;
set({
currentAction: 'study',
currentStudyTarget: {
type: 'skill',
id: skillId,
progress: state.skillProgress[skillId] || 0,
required: sk.studyTime,
manaCostPerHour,
},
log: [`📚 Started studying ${sk.name}... (${Math.floor(manaCostPerHour)} mana/hour)`, ...state.log.slice(0, 49)],
});
},
startStudyingSpell: (spellId: string) => {
const state = get();
const sp = SPELLS_DEF[spellId];
if (!sp || state.spells[spellId]?.learned) return;
const costMult = getStudyCostMultiplier(state.skills);
const totalCost = Math.floor(sp.unlock * costMult);
const studyTime = sp.studyTime || (sp.tier * 4);
const manaCostPerHour = totalCost / studyTime;
set({
currentAction: 'study',
currentStudyTarget: {
type: 'spell',
id: spellId,
progress: state.spells[spellId]?.studyProgress || 0,
required: studyTime,
manaCostPerHour,
},
spells: {
...state.spells,
[spellId]: {
...(state.spells[spellId] || { learned: false, level: 0 }),
studyProgress: state.spells[spellId]?.studyProgress || 0,
},
},
log: [`📚 Started studying ${sp.name}... (${Math.floor(manaCostPerHour)} mana/hour)`, ...state.log.slice(0, 49)],
});
},
startParallelStudySkill: (skillId: string) => {
const state = get();
if (state.parallelStudyTarget) return;
if (!state.currentStudyTarget) return;
const sk = SKILLS_DEF[skillId];
if (!sk) return;
const currentLevel = state.skills[skillId] || 0;
if (currentLevel >= sk.max) return;
if (state.currentStudyTarget.id === skillId) return;
set({
parallelStudyTarget: {
type: 'skill',
id: skillId,
progress: state.skillProgress[skillId] || 0,
required: sk.studyTime,
manaCostPerHour: 0, // Parallel study doesn't cost extra
},
log: [`📚 Started parallel study of ${sk.name}... (50% speed)`, ...state.log.slice(0, 49)],
});
},
cancelStudy: () => {
const state = get();
if (!state.currentStudyTarget) return;
const savedProgress = state.currentStudyTarget.progress;
const log = ['📖 Study paused. Progress saved.', ...state.log.slice(0, 49)];
if (state.currentStudyTarget.type === 'skill') {
set({
currentStudyTarget: null,
currentAction: 'meditate',
skillProgress: {
...state.skillProgress,
[state.currentStudyTarget.id]: savedProgress,
},
log,
});
} else {
set({
currentStudyTarget: null,
currentAction: 'meditate',
spells: {
...state.spells,
[state.currentStudyTarget.id]: {
...(state.spells[state.currentStudyTarget.id] || { learned: false, level: 0 }),
studyProgress: savedProgress,
},
},
log,
});
}
},
cancelParallelStudy: () => {
set((state) => {
if (!state.parallelStudyTarget) return state;
return {
parallelStudyTarget: null,
log: ['📖 Parallel study cancelled.', ...state.log.slice(0, 49)],
};
});
},
selectSkillUpgrade: (skillId: string, upgradeId: string) => {
set((state) => {
const current = state.skillUpgrades?.[skillId] || [];
if (current.includes(upgradeId)) return state;
if (current.length >= 2) return state;
return {
skillUpgrades: {
...state.skillUpgrades,
[skillId]: [...current, upgradeId],
},
};
});
},
deselectSkillUpgrade: (skillId: string, upgradeId: string) => {
set((state) => {
const current = state.skillUpgrades?.[skillId] || [];
return {
skillUpgrades: {
...state.skillUpgrades,
[skillId]: current.filter(id => id !== upgradeId),
},
};
});
},
commitSkillUpgrades: (skillId: string, upgradeIds: string[], milestone: 5 | 10) => {
set((state) => {
const existingUpgrades = state.skillUpgrades?.[skillId] || [];
const otherMilestoneUpgrades = existingUpgrades.filter(
id => milestone === 5 ? id.includes('_l10') : id.includes('_l5')
);
return {
skillUpgrades: {
...state.skillUpgrades,
[skillId]: [...otherMilestoneUpgrades, ...upgradeIds],
},
};
});
},
tierUpSkill: (skillId: string) => {
const state = get();
const baseSkillId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId;
const currentTier = state.skillTiers?.[baseSkillId] || 1;
const nextTier = currentTier + 1;
if (nextTier > 5) return;
const nextTierSkillId = `${baseSkillId}_t${nextTier}`;
set({
skillTiers: {
...state.skillTiers,
[baseSkillId]: nextTier,
},
skills: {
...state.skills,
[nextTierSkillId]: 0,
[skillId]: 0,
},
skillProgress: {
...state.skillProgress,
[skillId]: 0,
[nextTierSkillId]: 0,
},
skillUpgrades: {
...state.skillUpgrades,
[nextTierSkillId]: [],
[skillId]: [],
},
log: [`🌟 ${SKILLS_DEF[baseSkillId]?.name || baseSkillId} evolved to Tier ${nextTier}!`, ...state.log.slice(0, 49)],
});
},
getSkillUpgradeChoices: (skillId: string, milestone: 5 | 10) => {
const state = get();
const baseSkillId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId;
const tier = state.skillTiers?.[baseSkillId] || 1;
const available = getUpgradesForSkillAtMilestone(skillId, milestone, state.skillTiers || {});
const selected = (state.skillUpgrades?.[skillId] || []).filter(id =>
available.some(u => u.id === id)
);
return { available, selected };
},
});
// Process study progress (called during tick)
export function processStudy(state: GameState, deltaHours: number): Partial<GameState> {
if (state.currentAction !== 'study' || !state.currentStudyTarget) return {};
const target = state.currentStudyTarget;
const studySpeedMult = getStudySpeedMultiplier(state.skills);
const progressGain = deltaHours * studySpeedMult;
const manaCost = progressGain * target.manaCostPerHour;
let rawMana = state.rawMana;
let totalManaGathered = state.totalManaGathered;
let skills = state.skills;
let skillProgress = state.skillProgress;
let spells = state.spells;
const log = [...state.log];
if (rawMana >= manaCost) {
rawMana -= manaCost;
totalManaGathered += manaCost;
const newProgress = target.progress + progressGain;
if (newProgress >= target.required) {
// Study complete
if (target.type === 'skill') {
const skillId = target.id;
const currentLevel = skills[skillId] || 0;
skills = { ...skills, [skillId]: currentLevel + 1 };
skillProgress = { ...skillProgress, [skillId]: 0 };
log.unshift(`${SKILLS_DEF[skillId]?.name} Lv.${currentLevel + 1} mastered!`);
} else if (target.type === 'spell') {
const spellId = target.id;
spells = {
...spells,
[spellId]: { learned: true, level: 1, studyProgress: 0 },
};
log.unshift(`📖 ${SPELLS_DEF[spellId]?.name} learned!`);
}
return {
rawMana,
totalManaGathered,
skills,
skillProgress,
spells,
currentStudyTarget: null,
currentAction: 'meditate',
log,
};
}
return {
rawMana,
totalManaGathered,
currentStudyTarget: { ...target, progress: newProgress },
};
}
// Not enough mana
log.unshift('⚠️ Not enough mana to continue studying. Progress saved.');
if (target.type === 'skill') {
return {
currentStudyTarget: null,
currentAction: 'meditate',
skillProgress: { ...skillProgress, [target.id]: target.progress },
log,
};
} else {
return {
currentStudyTarget: null,
currentAction: 'meditate',
spells: {
...spells,
[target.id]: {
...(spells[target.id] || { learned: false, level: 0 }),
studyProgress: target.progress,
},
},
log,
};
}
}
+82
View File
@@ -0,0 +1,82 @@
// ─── Time Slice ───────────────────────────────────────────────────────────────
// Manages game time, loops, and game state
import type { StateCreator } from 'zustand';
import type { GameState } from '../types';
import { MAX_DAY } from '../constants';
import { calcInsight } from './computed';
export interface TimeSlice {
// State
day: number;
hour: number;
loopCount: number;
gameOver: boolean;
victory: boolean;
paused: boolean;
incursionStrength: number;
loopInsight: number;
log: string[];
// Actions
togglePause: () => void;
resetGame: () => void;
startNewLoop: () => void;
addLog: (message: string) => void;
}
export const createTimeSlice = (
set: StateCreator<GameState>['set'],
get: () => GameState,
initialOverrides?: Partial<GameState>
): TimeSlice => ({
day: 1,
hour: 0,
loopCount: initialOverrides?.loopCount || 0,
gameOver: false,
victory: false,
paused: false,
incursionStrength: 0,
loopInsight: 0,
log: ['✨ The loop begins. You start with Mana Bolt. Gather your strength, mage.'],
togglePause: () => {
set((state) => ({ paused: !state.paused }));
},
resetGame: () => {
if (typeof window !== 'undefined') {
localStorage.removeItem('mana-loop-storage');
}
// Reset to initial state
window.location.reload();
},
startNewLoop: () => {
const state = get();
const insightGained = state.loopInsight || calcInsight(state);
const total = state.insight + insightGained;
// Spell preservation is handled through the prestige upgrade "spellMemory"
// which is purchased with insight
// This will be handled by the main store reset
set({
day: 1,
hour: 0,
gameOver: false,
victory: false,
loopCount: state.loopCount + 1,
insight: total,
totalInsight: (state.totalInsight || 0) + insightGained,
loopInsight: 0,
log: ['✨ A new loop begins. Your insight grows...'],
});
},
addLog: (message: string) => {
set((state) => ({
log: [message, ...state.log.slice(0, 49)],
}));
},
});