diff --git a/src/lib/game/crafting-apply.ts b/src/lib/game/crafting-apply.ts
index d93011d..4d1ad5c 100644
--- a/src/lib/game/crafting-apply.ts
+++ b/src/lib/game/crafting-apply.ts
@@ -3,6 +3,7 @@
import type { EquipmentInstance, AppliedEnchantment, EnchantmentDesign, ApplicationProgress } from './types';
import { calculateApplicationTime, calculateApplicationManaPerHour } from './crafting-utils';
+import { HOURS_PER_TICK } from './constants';
import { hasSpecial, SPECIAL_EFFECTS } from './effects/special-effects';
import type { ComputedEffects } from './effects/upgrade-effects.types';
import type { AttunementState } from './types';
@@ -11,32 +12,16 @@ import { ENCHANTMENT_EFFECTS } from './data/enchantment-effects';
// βββ Application Validation βββββββββββββββββββββββββββββββββββββββββββββββββ
-// Check if enchantment application can start
export function canApplyEnchantment(
instance: EquipmentInstance | undefined,
design: EnchantmentDesign | undefined,
currentAction: string
): { canApply: boolean; reason?: string } {
- if (!instance) {
- return { canApply: false, reason: 'Equipment instance not found' };
- }
-
- if (!design) {
- return { canApply: false, reason: 'Enchantment design not found' };
- }
-
- if (currentAction !== 'meditate') {
- return { canApply: false, reason: 'Must be in meditate state' };
- }
-
- if (!instance.tags?.includes('Ready for Enchantment')) {
- return { canApply: false, reason: 'Equipment must be prepared for enchanting' };
- }
-
- if (instance.usedCapacity + design.totalCapacityUsed > instance.totalCapacity) {
- return { canApply: false, reason: 'Not enough capacity on equipment' };
- }
-
+ if (!instance) return { canApply: false, reason: 'Equipment instance not found' };
+ if (!design) return { canApply: false, reason: 'Enchantment design not found' };
+ if (currentAction !== 'meditate') return { canApply: false, reason: 'Must be in meditate state' };
+ if (!instance.tags?.includes('Ready for Enchantment')) return { canApply: false, reason: 'Equipment must be prepared for enchanting' };
+ if (instance.usedCapacity + design.totalCapacityUsed > instance.totalCapacity) return { canApply: false, reason: 'Not enough capacity on equipment' };
return { canApply: true };
}
@@ -51,21 +36,18 @@ export interface ApplicationCosts {
export function calculateApplicationCosts(design: EnchantmentDesign): ApplicationCosts {
const time = calculateApplicationTime(design);
const manaPerHour = calculateApplicationManaPerHour(design);
- const manaPerTick = manaPerHour * 0.04; // HOURS_PER_TICK
-
+ const manaPerTick = manaPerHour * HOURS_PER_TICK;
return { time, manaPerHour, manaPerTick };
}
// βββ Application Progress βββββββββββββββββββββββββββββββββββββββββββββββββββ
-// Initialize application progress
export function initializeApplicationProgress(
equipmentInstanceId: string,
designId: string,
design: EnchantmentDesign
): ApplicationProgress {
const costs = calculateApplicationCosts(design);
-
return {
equipmentInstanceId,
designId,
@@ -77,7 +59,13 @@ export function initializeApplicationProgress(
};
}
-// Calculate application progress after a tick
+// Free enchant chance per special effect
+const FREE_ENCHANT_CHANCES: Record = {
+ [SPECIAL_EFFECTS.ENCHANT_PRESERVATION]: 0.25,
+ [SPECIAL_EFFECTS.THRIFTY_ENCHANTER]: 0.10,
+ [SPECIAL_EFFECTS.OPTIMIZED_ENCHANTING]: 0.25,
+};
+
export interface ApplicationTickResult {
progress: number;
manaSpent: number;
@@ -93,20 +81,14 @@ export function calculateApplicationTick(
manaPerTick: number,
computedEffects: ComputedEffects
): ApplicationTickResult {
- let progress = currentProgress + 0.04;
+ let progress = currentProgress + HOURS_PER_TICK;
let manaSpent = currentManaSpent + manaPerTick;
let manaConsumed = manaPerTick;
let triggeredFreeEnchant = false;
let freeEnchantChance = 0;
- if (hasSpecial(computedEffects, SPECIAL_EFFECTS.ENCHANT_PRESERVATION)) {
- freeEnchantChance += 0.25;
- }
- if (hasSpecial(computedEffects, SPECIAL_EFFECTS.THRIFTY_ENCHANTER)) {
- freeEnchantChance += 0.10;
- }
- if (hasSpecial(computedEffects, SPECIAL_EFFECTS.OPTIMIZED_ENCHANTING)) {
- freeEnchantChance += 0.25;
+ for (const [special, chance] of Object.entries(FREE_ENCHANT_CHANCES)) {
+ if (hasSpecial(computedEffects, special)) freeEnchantChance += chance;
}
if (freeEnchantChance > 0 && Math.random() < freeEnchantChance) {
@@ -116,47 +98,32 @@ export function calculateApplicationTick(
triggeredFreeEnchant = true;
}
- return {
- progress,
- manaSpent,
- manaConsumed,
- isComplete: progress >= required,
- triggeredFreeEnchant,
- };
+ return { progress, manaSpent, manaConsumed, isComplete: progress >= required, triggeredFreeEnchant };
}
// βββ Enchantment Application ββββββββββββββββββββββββββββββββββββββββββββββββ
-// Apply enchantments to equipment instance
+const PURE_ESSENCE_STACK_BONUS = 1.25;
+const PURE_ESSENCE_COST_CAP = 100;
+
export function applyEnchantments(
instance: EquipmentInstance,
design: EnchantmentDesign,
computedEffects: ComputedEffects
-): {
- updatedInstance: EquipmentInstance;
- xpGained: number;
- logMessage: string;
-} {
+): { updatedInstance: EquipmentInstance; xpGained: number; logMessage: string } {
const isPureEssenceActive = hasSpecial(computedEffects, SPECIAL_EFFECTS.PURE_ESSENCE);
const newEnchantments: AppliedEnchantment[] = design.effects.map(eff => {
- let stacks = eff.stacks;
- let actualCost = eff.capacityCost;
-
const effectDef = ENCHANTMENT_EFFECTS[eff.effectId];
- if (isPureEssenceActive && effectDef && effectDef.baseCapacityCost < 100) {
- stacks = Math.ceil(stacks * 1.25);
- }
-
+ const bonusStacks = isPureEssenceActive && effectDef && effectDef.baseCapacityCost < PURE_ESSENCE_COST_CAP;
return {
effectId: eff.effectId,
- stacks,
- actualCost,
+ stacks: bonusStacks ? Math.ceil(eff.stacks * PURE_ESSENCE_STACK_BONUS) : eff.stacks,
+ actualCost: eff.capacityCost,
};
});
const xpGained = calculateEnchantingXP(design.totalCapacityUsed);
-
const updatedInstance: EquipmentInstance = {
...instance,
enchantments: [...instance.enchantments, ...newEnchantments],
@@ -176,15 +143,12 @@ export function updateEnchanterAttunement(
attunements: Record,
xpGained: number
): Record {
- if (!attunements?.enchanter?.active || xpGained <= 0) {
- return attunements;
- }
+ if (!attunements?.enchanter?.active || xpGained <= 0) return attunements;
const enchanterState = attunements.enchanter;
let newXP = enchanterState.experience + xpGained;
let newLevel = enchanterState.level;
-
while (newLevel < MAX_ATTUNEMENT_LEVEL) {
const xpNeeded = getAttunementXPForLevel(newLevel + 1);
if (newXP >= xpNeeded) {
@@ -197,11 +161,7 @@ export function updateEnchanterAttunement(
return {
...attunements,
- enchanter: {
- ...enchanterState,
- level: newLevel,
- experience: newXP,
- },
+ enchanter: { ...enchanterState, level: newLevel, experience: newXP },
};
}
@@ -222,7 +182,7 @@ export function resumeApplication() {
// βββ Progress Calculations ββββββββββββββββββββββββββββββββββββββββββββββββββ
export function getApplicationManaCostForTick(manaPerHour: number): number {
- return manaPerHour * 0.04;
+ return manaPerHour * HOURS_PER_TICK;
}
export function getApplicationRemainingTime(currentProgress: number, required: number): number {
diff --git a/src/lib/game/crafting-design.ts b/src/lib/game/crafting-design.ts
index d3dfe15..a507c1c 100644
--- a/src/lib/game/crafting-design.ts
+++ b/src/lib/game/crafting-design.ts
@@ -6,67 +6,55 @@ import type { ComputedEffects } from './effects/upgrade-effects.types';
import { calculateEnchantingXP } from './data/attunements';
import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchantment-effects';
import { hasSpecial, SPECIAL_EFFECTS } from './effects/special-effects';
+import { HOURS_PER_TICK } from './constants';
import { EQUIPMENT_TYPES } from './data/equipment';
+// Progress per tick expressed as a fraction of HOURS_PER_TICK
+const DESIGN_PROGRESS_PER_TICK = HOURS_PER_TICK;
+const HASTY_ENCHANTER_BONUS_MULTIPLIER = 0.25;
+
// βββ Design Creation & Calculation ββββββββββββββββββββββββββββββββββββββββββ
-// Validate effects for a design against equipment category
export function validateDesignEffects(
effects: DesignEffect[],
equipmentTypeId: string,
enchantingLevel: number
): { valid: boolean; reason?: string } {
- if (enchantingLevel < 1) {
- return { valid: false, reason: 'Requires enchanting skill level 1' };
- }
+ if (enchantingLevel < 1) return { valid: false, reason: 'Requires enchanting skill level 1' };
const equipType = EQUIPMENT_TYPES[equipmentTypeId];
- if (!equipType) {
- return { valid: false, reason: 'Invalid equipment type' };
- }
- const category = equipType.category;
- if (!category) {
- return { valid: false, reason: 'Invalid equipment category' };
- }
+ if (!equipType || !equipType.category) return { valid: false, reason: 'Invalid equipment type or category' };
for (const eff of effects) {
const effectDef = ENCHANTMENT_EFFECTS[eff.effectId];
- if (!effectDef) {
- return { valid: false, reason: `Unknown effect: ${eff.effectId}` };
- }
- if (!effectDef.allowedEquipmentCategories.includes(category)) {
- return { valid: false, reason: `Effect ${eff.effectId} not allowed on ${category}` };
- }
- if (eff.stacks > effectDef.maxStacks) {
- return { valid: false, reason: `Stacks exceed maximum for ${eff.effectId}` };
+ if (!effectDef) return { valid: false, reason: `Unknown effect: ${eff.effectId}` };
+ if (!effectDef.allowedEquipmentCategories.includes(equipType.category)) {
+ return { valid: false, reason: `Effect ${eff.effectId} not allowed on ${equipType.category}` };
}
+ if (eff.stacks > effectDef.maxStacks) return { valid: false, reason: `Stacks exceed maximum for ${eff.effectId}` };
}
return { valid: true };
}
-// Create an enchantment design from validated inputs
export function createEnchantmentDesign(
name: string,
equipmentType: string,
effects: DesignEffect[],
efficiencyBonus: number = 0
): EnchantmentDesign {
- const totalCapacityUsed = calculateDesignCapacityCost(effects, efficiencyBonus);
- const designTime = calculateDesignTime(effects);
-
return {
id: `design_${Date.now()}`,
name,
equipmentType,
effects,
- totalCapacityUsed,
- designTime,
+ totalCapacityUsed: calculateDesignCapacityCost(effects, efficiencyBonus),
+ designTime: calculateDesignTime(effects),
created: Date.now(),
};
}
-// βββ Capacity Cost Calculation ββββββββββββββββββββββββββββββββββββββββββββββ
+// βββ Capacity & Time Calculations βββββββββββββββββββββββββββββββββββββββββββ
export function calculateDesignCapacityCost(effects: DesignEffect[], efficiencyBonus: number = 0): number {
return effects.reduce((total, eff) => total + calculateEffectCapacityCost(eff.effectId, eff.stacks, efficiencyBonus), 0);
@@ -76,6 +64,25 @@ export function calculateTotalCapacityCost(design: EnchantmentDesign): number {
return design.totalCapacityUsed;
}
+export function calculateDesignTime(effects: DesignEffect[]): number {
+ let time = 1;
+ for (const eff of effects) {
+ if (ENCHANTMENT_EFFECTS[eff.effectId]) time += 0.5 * eff.stacks;
+ }
+ return time;
+}
+
+export function getDesignTimeWithHaste(
+ effects: DesignEffect[],
+ isRepeatDesign: boolean,
+ computedEffects: ComputedEffects
+): number {
+ const time = calculateDesignTime(effects);
+ return isRepeatDesign && hasSpecial(computedEffects, SPECIAL_EFFECTS.HASTY_ENCHANTER)
+ ? time * 0.75
+ : time;
+}
+
// βββ XP & Progression βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export function calculateEnchantingXpFromDesign(design: EnchantmentDesign): number {
@@ -88,37 +95,11 @@ export function calculateXpFromInstanceEnchantments(
let totalXp = 0;
for (const ench of instance.enchantments) {
const effectDef = ENCHANTMENT_EFFECTS[ench.effectId];
- const baseCost = effectDef?.baseCapacityCost || 0;
- totalXp += calculateEnchantingXP(baseCost * ench.stacks);
+ totalXp += calculateEnchantingXP((effectDef?.baseCapacityCost || 0) * ench.stacks);
}
return totalXp;
}
-// βββ Design Time Calculations ββββββββββββββββββββββββββββββββββββββββββββββ
-
-export function calculateDesignTime(effects: DesignEffect[]): number {
- let time = 1;
- for (const eff of effects) {
- const effectDef = ENCHANTMENT_EFFECTS[eff.effectId];
- if (effectDef) {
- time += 0.5 * eff.stacks;
- }
- }
- return time;
-}
-
-export function getDesignTimeWithHaste(
- effects: DesignEffect[],
- isRepeatDesign: boolean,
- computedEffects: ComputedEffects
-): number {
- let time = calculateDesignTime(effects);
- if (isRepeatDesign && hasSpecial(computedEffects, SPECIAL_EFFECTS.HASTY_ENCHANTER)) {
- time *= 0.75;
- }
- return time;
-}
-
// βββ Progress Calculations ββββββββββββββββββββββββββββββββββββββββββββββββββ
export interface DesignProgressUpdate {
@@ -128,21 +109,23 @@ export interface DesignProgressUpdate {
timeBonus: number;
}
+const INSTANT_DESIGN_CHANCE = 0.10;
+
export function calculateDesignProgress(
currentProgress: number,
required: number,
computedEffects: ComputedEffects,
isRepeatDesign: boolean
): DesignProgressUpdate {
- let progress = currentProgress + 0.04;
+ let progress = currentProgress + DESIGN_PROGRESS_PER_TICK;
let timeBonus = 0;
if (isRepeatDesign && hasSpecial(computedEffects, SPECIAL_EFFECTS.HASTY_ENCHANTER)) {
- timeBonus = 0.04 * 0.25;
+ timeBonus = DESIGN_PROGRESS_PER_TICK * HASTY_ENCHANTER_BONUS_MULTIPLIER;
progress += timeBonus;
}
- if (hasSpecial(computedEffects, SPECIAL_EFFECTS.INSTANT_DESIGNS) && Math.random() < 0.10) {
+ if (hasSpecial(computedEffects, SPECIAL_EFFECTS.INSTANT_DESIGNS) && Math.random() < INSTANT_DESIGN_CHANCE) {
progress = required;
}
@@ -165,8 +148,7 @@ export function isSecondDesignSlotAvailable(
): boolean {
if (!designProgress && !designProgress2) return true;
if (!designProgress && designProgress2) return false;
- if (designProgress && !designProgress2 && hasEnchantMastery) return true;
- return false;
+ return !!(designProgress && !designProgress2 && hasEnchantMastery);
}
// βββ Auto-save Completed Design ββββββββββββββββββββββββββββββββββββββββββββ
@@ -181,13 +163,12 @@ export function createCompletedDesignFromProgress(
},
efficiencyBonus: number = 0
): EnchantmentDesign {
- const totalCapacityCost = calculateDesignCapacityCost(progressData.effects, efficiencyBonus);
return {
id: progressData.designId,
name: progressData.name,
equipmentType: progressData.equipmentType,
effects: progressData.effects,
- totalCapacityUsed: totalCapacityCost,
+ totalCapacityUsed: calculateDesignCapacityCost(progressData.effects, efficiencyBonus),
designTime: progressData.required,
created: Date.now(),
};
@@ -206,13 +187,10 @@ export function filterDesignsByEquipment(
equipment: { instanceId: string; totalCapacity: number; usedCapacity: number } | null
): DesignWithCapacityInfo[] {
if (!equipment) return [];
+ const availableCapacity = equipment.totalCapacity - equipment.usedCapacity;
return designs.map(design => ({
design,
- fitsInEquipment: designFitsInEquipment(design, equipment),
- availableCapacity: equipment.totalCapacity - equipment.usedCapacity,
+ fitsInEquipment: (equipment.usedCapacity || 0) + design.totalCapacityUsed <= equipment.totalCapacity,
+ availableCapacity,
}));
}
-
-function designFitsInEquipment(design: EnchantmentDesign, instance: { usedCapacity: number; totalCapacity: number }): boolean {
- return (instance.usedCapacity || 0) + design.totalCapacityUsed <= instance.totalCapacity;
-}
diff --git a/src/lib/game/crafting-equipment.ts b/src/lib/game/crafting-equipment.ts
index 00a16b7..860e6f5 100644
--- a/src/lib/game/crafting-equipment.ts
+++ b/src/lib/game/crafting-equipment.ts
@@ -6,10 +6,12 @@ import { CRAFTING_RECIPES, canCraftRecipe, type CraftingRecipe } from './data/cr
import { EQUIPMENT_TYPES } from './data/equipment';
import { ok, fail, ErrorCode } from './utils/result';
import type { Result } from './utils/result';
+import { HOURS_PER_TICK } from './constants';
+
+const MANA_REFUND_RATE = 0.5;
// βββ Equipment Crafting Validation ββββββββββββββββββββββββββββββββββββββββββ
-// Check if equipment crafting can start
export function canStartEquipmentCrafting(
blueprintId: string,
hasBlueprint: boolean,
@@ -17,38 +19,27 @@ export function canStartEquipmentCrafting(
currentMana: number,
currentAction: string
): { canCraft: boolean; reason?: string; recipe?: CraftingRecipe; missingMaterials?: Record; missingMana?: number } {
- if (currentAction !== 'meditate') {
- return { canCraft: false, reason: 'Must be in meditate state' };
- }
+ if (currentAction !== 'meditate') return { canCraft: false, reason: 'Must be in meditate state' };
const recipe = CRAFTING_RECIPES[blueprintId];
- if (!recipe) {
- return { canCraft: false, reason: 'Invalid blueprint' };
- }
-
- if (!hasBlueprint) {
- return { canCraft: false, reason: 'Blueprint not acquired' };
- }
+ if (!recipe) return { canCraft: false, reason: 'Invalid blueprint' };
+ if (!hasBlueprint) return { canCraft: false, reason: 'Blueprint not acquired' };
const { canCraft, missingMaterials } = canCraftRecipe(recipe, materials, currentMana);
+ if (canCraft) return { canCraft: true, recipe };
- if (!canCraft) {
- const missingMana = Math.max(0, recipe.manaCost - currentMana);
- return {
- canCraft: false,
- reason: missingMana > 0 ? 'Insufficient mana' : 'Missing materials',
- recipe,
- missingMaterials,
- missingMana: missingMana > 0 ? missingMana : undefined,
- };
- }
-
- return { canCraft: true, recipe };
+ const missingManaAmount = Math.max(0, recipe.manaCost - currentMana);
+ return {
+ canCraft: false,
+ reason: missingManaAmount > 0 ? 'Insufficient mana' : 'Missing materials',
+ recipe,
+ missingMaterials,
+ missingMana: missingManaAmount > 0 ? missingManaAmount : undefined,
+ };
}
// βββ Equipment Crafting Execution βββββββββββββββββββββββββββββββββββββββββββ
-// Deduct crafting costs and initialize progress
export interface CraftingInitResult {
recipe: CraftingRecipe;
newMaterials: Record;
@@ -63,108 +54,80 @@ export function initializeEquipmentCrafting(
): CraftingInitResult {
const recipe = CRAFTING_RECIPES[blueprintId];
- // Deduct materials
const newMaterials = { ...materials };
for (const [matId, amount] of Object.entries(recipe.materials)) {
newMaterials[matId] = (newMaterials[matId] || 0) - amount;
- if (newMaterials[matId] <= 0) {
- delete newMaterials[matId];
- }
+ if (newMaterials[matId] <= 0) delete newMaterials[matId];
}
- // Create progress
- const progress: EquipmentCraftingProgress = {
- blueprintId,
- equipmentTypeId: recipe.equipmentTypeId,
- progress: 0,
- required: recipe.craftTime,
- manaSpent: recipe.manaCost,
- };
-
return {
recipe,
newMaterials,
manaCost: recipe.manaCost,
- progress,
+ progress: {
+ blueprintId,
+ equipmentTypeId: recipe.equipmentTypeId,
+ progress: 0,
+ required: recipe.craftTime,
+ manaSpent: recipe.manaCost,
+ },
};
}
// βββ Crafting Progress ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-// Calculate crafting progress after a tick
export interface CraftingTickResult {
progress: number;
isComplete: boolean;
}
export function calculateCraftingTick(currentProgress: number, required: number): CraftingTickResult {
- const progress = currentProgress + 0.04; // HOURS_PER_TICK
- return {
- progress,
- isComplete: progress >= required,
- };
+ const progress = currentProgress + HOURS_PER_TICK;
+ return { progress, isComplete: progress >= required };
}
// βββ Crafting Completion βββββββββββββββββββββββββββββββββββββββββββββββββββ
-// Create equipment instance from completed crafting
+const BASE_EQUIPMENT_QUALITY = 100;
+
export function completeEquipmentCrafting(
blueprintId: string,
recipe: CraftingRecipe
-): Result<{
- instanceId: string;
- instance: EquipmentInstance;
- logMessage: string;
-}> {
+): Result<{ instanceId: string; instance: EquipmentInstance; logMessage: string }> {
const equipType = EQUIPMENT_TYPES[recipe.equipmentTypeId];
- if (!equipType) {
- return fail(ErrorCode.INVALID_EQUIPMENT_TYPE, `Invalid equipment type: ${recipe.equipmentTypeId}`);
- }
+ if (!equipType) return fail(ErrorCode.INVALID_EQUIPMENT_TYPE, `Invalid equipment type: ${recipe.equipmentTypeId}`);
const instanceId = `equip_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
- const newInstance: EquipmentInstance = {
- instanceId,
- typeId: recipe.equipmentTypeId,
- name: recipe.name,
- enchantments: [],
- usedCapacity: 0,
- totalCapacity: equipType.baseCapacity,
- rarity: recipe.rarity,
- quality: 100,
- tags: [],
- };
-
return ok({
instanceId,
- instance: newInstance,
+ instance: {
+ instanceId,
+ typeId: recipe.equipmentTypeId,
+ name: recipe.name,
+ enchantments: [],
+ usedCapacity: 0,
+ totalCapacity: equipType.baseCapacity,
+ rarity: recipe.rarity,
+ quality: BASE_EQUIPMENT_QUALITY,
+ tags: [],
+ },
logMessage: `π¨ Crafted ${recipe.name}!`,
});
}
// βββ Crafting Cancellation ββββββββββββββββββββββββββββββββββββββββββββββββββ
-// Cancel active crafting and refund partial resources
export interface CraftingCancelResult {
manaRefund: number;
logMessage: string;
}
-export function cancelEquipmentCrafting(_blueprintId: string, manaSpent: number): CraftingCancelResult {
+export function cancelEquipmentCrafting(blueprintId: string, manaSpent: number): CraftingCancelResult {
const recipe = CRAFTING_RECIPES[blueprintId];
- if (!recipe) {
- return {
- manaRefund: 0,
- logMessage: 'Invalid crafting recipe.',
- };
- }
+ if (!recipe) return { manaRefund: 0, logMessage: 'Invalid crafting recipe.' };
- // Refund 50% of mana
- const manaRefund = Math.floor(manaSpent * 0.5);
-
- return {
- manaRefund,
- logMessage: `π« Equipment crafting cancelled. Refunded ${manaRefund} mana.`,
- };
+ const manaRefund = Math.floor(manaSpent * MANA_REFUND_RATE);
+ return { manaRefund, logMessage: `π« Equipment crafting cancelled. Refunded ${manaRefund} mana.` };
}
// βββ Recipe Information βββββββββββββββββββββββββββββββββββββββββββββββββββββ
@@ -179,23 +142,16 @@ export function getCraftableRecipes(
currentMana: number
): CraftingRecipe[] {
const craftable: CraftingRecipe[] = [];
-
for (const blueprintId of blueprints) {
const recipe = CRAFTING_RECIPES[blueprintId];
if (!recipe) continue;
-
- const { canCraft } = canCraftRecipe(recipe, materials, currentMana);
- if (canCraft) {
- craftable.push(recipe);
- }
+ if (canCraftRecipe(recipe, materials, currentMana).canCraft) craftable.push(recipe);
}
-
return craftable;
}
// βββ Material Management ββββββββββββββββββββββββββββββββββββββββββββββββββββ
-// Delete materials from inventory
export function deleteMaterials(materialId: string, amount: number, materials: Record): {
newMaterials: Record;
deleted: number;
@@ -204,25 +160,18 @@ export function deleteMaterials(materialId: string, amount: number, materials: R
const deleted = Math.min(amount, currentAmount);
const remaining = Math.max(0, currentAmount - amount);
const newMaterials = { ...materials };
-
if (remaining <= 0) {
delete newMaterials[materialId];
} else {
newMaterials[materialId] = remaining;
}
-
- return {
- newMaterials,
- deleted,
- };
+ return { newMaterials, deleted };
}
-// Get total material count
export function getMaterialCount(materials: Record, materialId: string): number {
return materials[materialId] || 0;
}
-// Add materials to inventory
export function addMaterials(materials: Record, materialId: string, amount: number): Record {
const newMaterials = { ...materials };
newMaterials[materialId] = (newMaterials[materialId] || 0) + amount;
diff --git a/src/lib/game/crafting-prep.ts b/src/lib/game/crafting-prep.ts
index cb447ee..b69f2ce 100644
--- a/src/lib/game/crafting-prep.ts
+++ b/src/lib/game/crafting-prep.ts
@@ -3,26 +3,21 @@
import type { EquipmentInstance, PreparationProgress } from './types';
import { calculatePrepTime, calculatePrepManaCost, calculateManaPerHourForPrep } from './crafting-utils';
+import { HOURS_PER_TICK } from './constants';
// βββ Preparation Validation βββββββββββββββββββββββββββββββββββββββββββββββββ
-// Check if an equipment instance can be prepared
export function canPrepareEquipment(
instance: EquipmentInstance | undefined,
currentTags: string[]
): { canPrepare: boolean; reason?: string } {
- if (!instance) {
- return { canPrepare: false, reason: 'Equipment instance not found' };
- }
-
- if (currentTags.includes('Ready for Enchantment')) {
- return { canPrepare: false, reason: 'Equipment is already prepared for enchanting' };
- }
-
+ if (!instance) return { canPrepare: false, reason: 'Equipment instance not found' };
+ if (currentTags.includes('Ready for Enchantment')) return { canPrepare: false, reason: 'Equipment is already prepared for enchanting' };
return { canPrepare: true };
}
-// Calculate preparation resource costs
+// βββ Preparation Costs ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
export interface PreparationCosts {
time: number;
manaTotal: number;
@@ -34,30 +29,20 @@ export function calculatePreparationCosts(totalCapacity: number): PreparationCos
const time = calculatePrepTime(totalCapacity);
const manaTotal = calculatePrepManaCost(totalCapacity);
const manaPerHour = calculateManaPerHourForPrep(totalCapacity, time);
- const manaPerTick = manaPerHour * 0.04; // HOURS_PER_TICK
-
- return { time, manaTotal, manaPerHour, manaPerTick };
+ return { time, manaTotal, manaPerHour, manaPerTick: manaPerHour * HOURS_PER_TICK };
}
-// βββ Preparation Progress βββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-// Initialize preparation progress
export function initializePreparationProgress(
equipmentInstanceId: string,
totalCapacity: number,
manaCostPaid: number = 0
): PreparationProgress {
const costs = calculatePreparationCosts(totalCapacity);
-
- return {
- equipmentInstanceId,
- progress: 0,
- required: costs.time,
- manaCostPaid,
- };
+ return { equipmentInstanceId, progress: 0, required: costs.time, manaCostPaid };
}
-// Calculate updated preparation progress after a tick
+// βββ Preparation Tick βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
export interface PreparationTickResult {
progress: number;
manaCostPaid: number;
@@ -68,15 +53,14 @@ export interface PreparationTickResult {
export function calculatePreparationTick(
currentProgress: number,
required: number,
+ currentManaCostPaid: number,
manaPerTick: number
): PreparationTickResult {
- const progress = currentProgress + 0.04; // HOURS_PER_TICK
+ const progress = currentProgress + HOURS_PER_TICK;
const manaConsumed = manaPerTick;
- const manaCostPaid = manaPerTick; // Accumulated
-
return {
progress,
- manaCostPaid,
+ manaCostPaid: currentManaCostPaid + manaConsumed,
manaConsumed,
isComplete: progress >= required,
};
@@ -84,51 +68,40 @@ export function calculatePreparationTick(
// βββ Preparation Completion βββββββββββββββββββββββββββββββββββββββββββββββββ
-// Apply preparation completion to equipment instance
+const BASE_DISENCHANT_RECOVERY_RATE = 0.1;
+const DISENCHANT_RECOVERY_PER_LEVEL = 0.2;
+
export function completePreparation(
instance: EquipmentInstance,
- _manaSpent: number
-): {
- updatedInstance: EquipmentInstance;
- manaRecovered: number;
- logMessage: string;
-} {
- // Calculate mana recovery from disenchanting (disenchanting skill removed - Bug 13)
- const disenchantLevel = 0;
- const recoveryRate = 0.1 + disenchantLevel * 0.2; // 10% base + 20% per level
-
+ disenchantLevel: number = 0
+): { updatedInstance: EquipmentInstance; manaRecovered: number; logMessage: string } {
+ const recoveryRate = BASE_DISENCHANT_RECOVERY_RATE + disenchantLevel * DISENCHANT_RECOVERY_PER_LEVEL;
let totalRecovered = 0;
for (const ench of instance.enchantments) {
totalRecovered += Math.floor(ench.actualCost * recoveryRate);
}
- const updatedInstance: EquipmentInstance = {
- ...instance,
- enchantments: [],
- usedCapacity: 0,
- rarity: 'common',
- tags: [...(instance.tags || []), 'Ready for Enchantment'],
- };
-
return {
- updatedInstance,
+ updatedInstance: {
+ ...instance,
+ enchantments: [],
+ usedCapacity: 0,
+ rarity: 'common',
+ tags: [...(instance.tags || []), 'Ready for Enchantment'],
+ },
manaRecovered: totalRecovered,
logMessage: `β
Equipment prepared for enchanting! Recovered ${totalRecovered} mana.`,
};
}
-// Cancel preparation (no resource recovery for preparation itself)
export function cancelPreparation() {
- return {
- logMessage: 'Preparation cancelled.',
- };
+ return { logMessage: 'Preparation cancelled.' };
}
// βββ Preparation State Calculations βββββββββββββββββββββββββββββββββββββββββ
export function getPreparationManaCostForTick(instance: EquipmentInstance): number {
- const costs = calculatePreparationCosts(instance.totalCapacity);
- return costs.manaPerTick;
+ return calculatePreparationCosts(instance.totalCapacity).manaPerTick;
}
export function getPreparationRemainingTime(currentProgress: number, required: number): number {
diff --git a/src/lib/game/effects/dynamic-compute.ts b/src/lib/game/effects/dynamic-compute.ts
index cf73b22..ec9eeb3 100644
--- a/src/lib/game/effects/dynamic-compute.ts
+++ b/src/lib/game/effects/dynamic-compute.ts
@@ -2,8 +2,22 @@
// Dynamic computation functions that depend on special effects
import type { ComputedEffects } from './upgrade-effects.types';
+
import { SPECIAL_EFFECTS, hasSpecial } from './special-effects';
+// Threshold ratios for mana-dependent effects (currentMana / maxMana)
+const MANA_HIGH_THRESHOLD = 0.75;
+const MANA_OVERPOWER_THRESHOLD = 0.8;
+const MANA_BERSERKER_THRESHOLD = 0.5;
+const MANA_LOW_THRESHOLD = 0.25;
+const MANA_CRITICAL_THRESHOLD = 0.1;
+
+// Regen multipliers for mana-dependent thresholds
+const MANA_TORRENT_MULTIPLIER = 1.5;
+const MANA_CRISIS_MULTIPLIER = 1.5; // Desperate / Despair Wells
+const PANIC_RESERVE_MULTIPLIER = 2.0;
+const REGEN_BOOST_PER_100_MANA = 0.1;
+
/**
* Compute regen with special effects that depend on dynamic values
*/
@@ -15,60 +29,51 @@ export function computeDynamicRegen(
incursionStrength: number
): number {
let regen = baseRegen;
-
- // Mana Cascade: +0.1 regen per 100 max mana
+
+ // Per-100-max-mana regen bonuses
+ const manaHundreds = Math.floor(maxMana / 100);
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_CASCADE)) {
- regen += Math.floor(maxMana / 100) * 0.1;
+ regen += manaHundreds * REGEN_BOOST_PER_100_MANA;
}
-
- // Mana Waterfall: +0.25 regen per 100 max mana (upgraded cascade)
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_WATERFALL)) {
- regen += Math.floor(maxMana / 100) * 0.25;
+ regen += manaHundreds * 0.25;
}
-
- // Mana Torrent: +50% regen when above 75% mana
- if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_TORRENT) && currentMana > maxMana * 0.75) {
- regen *= 1.5;
- }
-
- // Desperate Wells / Despair Wells: +50% regen when below 25% mana
- if ((hasSpecial(effects, SPECIAL_EFFECTS.DESPERATE_WELLS) || hasSpecial(effects, SPECIAL_EFFECTS.DESPAIR_WELLS)) && currentMana < maxMana * 0.25) {
- regen *= 1.5;
- }
-
- // Panic Reserve: +100% regen when below 10% mana
- if (hasSpecial(effects, SPECIAL_EFFECTS.PANIC_RESERVE) && currentMana < maxMana * 0.1) {
- regen *= 2.0;
- }
-
- // Deep Reserve: +0.5 regen per 100 max mana
if (hasSpecial(effects, SPECIAL_EFFECTS.DEEP_RESERVE)) {
- regen += Math.floor(maxMana / 100) * 0.5;
+ regen += manaHundreds * 0.5;
}
-
- // Mana Core: 0.5% of max mana added as regen
+
+ // Fractional max-mana regen
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_CORE)) {
regen += maxMana * 0.005;
}
-
- // Mana Tide: Regen pulses Β±50% (sinusoidal based on time)
+
+ // Mana Tide: sinusoidal pulse
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_TIDE)) {
regen *= (1.0 + 0.5 * Math.sin(Date.now() / 10000));
}
-
- // Eternal Flow: Regen immune to ALL penalties
+
+ // Mana-ratio-dependent multipliers
+ const manaRatio = currentMana / maxMana;
+ if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_TORRENT) && manaRatio > MANA_HIGH_THRESHOLD) {
+ regen *= MANA_TORRENT_MULTIPLIER;
+ }
+ if ((hasSpecial(effects, SPECIAL_EFFECTS.DESPERATE_WELLS) || hasSpecial(effects, SPECIAL_EFFECTS.DESPAIR_WELLS)) && manaRatio < MANA_LOW_THRESHOLD) {
+ regen *= MANA_CRISIS_MULTIPLIER;
+ }
+ if (hasSpecial(effects, SPECIAL_EFFECTS.PANIC_RESERVE) && manaRatio < MANA_CRITICAL_THRESHOLD) {
+ regen *= PANIC_RESERVE_MULTIPLIER;
+ }
+
+ // Eternal Flow: skip incursion + multiplier below
if (hasSpecial(effects, SPECIAL_EFFECTS.ETERNAL_FLOW)) {
return regen * effects.regenMultiplier;
}
-
- // Steady Stream: Regen immune to incursion (skip incursion penalty only)
- if (hasSpecial(effects, SPECIAL_EFFECTS.STEADY_STREAM)) {
- // incursion penalty is skipped, but regenMultiplier still applies below
- } else {
- // Apply incursion penalty
+
+ // Steady Stream: skip incursion only
+ if (!hasSpecial(effects, SPECIAL_EFFECTS.STEADY_STREAM)) {
regen *= (1 - incursionStrength);
}
-
+
return regen * effects.regenMultiplier;
}
@@ -79,46 +84,7 @@ export function computeDynamicClickMana(
effects: ComputedEffects,
baseClickMana: number
): number {
- let clickMana = baseClickMana;
-
- // Mana Echo: 10% chance to gain double mana from clicks
- // Note: The chance is handled in the click handler, this just returns the base
- // The click handler should check hasSpecial and apply the 10% chance
-
- // Mana Genesis: Generate 1% of max mana per hour passively
- // This is handled in the game loop (store.ts), not here
-
- // Mana Heart: +10% max mana per loop (permanent)
- // This is applied during loop reset in store.ts
-
- return Math.floor((clickMana + effects.clickManaBonus) * effects.clickManaMultiplier);
+ return Math.floor((baseClickMana + effects.clickManaBonus) * effects.clickManaMultiplier);
}
-/**
- * Compute damage with special effects
- */
-export function computeDynamicDamage(
- effects: ComputedEffects,
- baseDamage: number,
- _floorHPPct: number,
- currentMana: number,
- maxMana: number
-): number {
- let damage = baseDamage * effects.baseDamageMultiplier;
-
- // Overpower: +50% damage when mana above 80%
- if (hasSpecial(effects, SPECIAL_EFFECTS.OVERPOWER) && currentMana >= maxMana * 0.8) {
- damage *= 1.5;
- }
-
- // Berserker: +50% damage when below 50% mana
- if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && currentMana < maxMana * 0.5) {
- damage *= 1.5;
- }
-
- // Combo Master: Every 5th attack deals 3x damage
- // Note: The hit counter is tracked in game state, this just returns the multiplier
- // The combat handler should check hasSpecial and the hit count
-
- return damage + effects.baseDamageBonus;
-}
+
diff --git a/src/lib/game/stores/gameStore.ts b/src/lib/game/stores/gameStore.ts
index 0c6c965..9677ec6 100644
--- a/src/lib/game/stores/gameStore.ts
+++ b/src/lib/game/stores/gameStore.ts
@@ -82,277 +82,258 @@ export const useGameStore = create()(
if (ctx.ui.gameOver || ctx.ui.paused) return;
- // ββ Phase 2: Compute β derive all updates βββββββββββββββββββββββββββ
- const writes: TickWrites = { logs: [] };
- const addLog = (msg: string) => writes.logs.push(msg);
-
- // Compute equipment and discipline effects
- const equipmentEffects = computeEquipmentEffects(
- ctx.crafting.equipmentInstances || {},
- ctx.crafting.equippedInstances || {}
- );
- const disciplineEffects = computeDisciplineEffects();
- const allSpecials = new Set([
- ...equipmentEffects.specials,
- ...disciplineEffects.specials,
- ]);
- const effects = { specials: allSpecials } as ComputedEffects;
-
- const maxMana = computeMaxMana(
- { skills: {}, prestigeUpgrades: ctx.prestige.prestigeUpgrades, skillUpgrades: {}, skillTiers: {} },
- undefined,
- disciplineEffects,
- );
- const baseRegen = computeRegen(
- { skills: {}, prestigeUpgrades: ctx.prestige.prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, attunements: {} },
- undefined,
- disciplineEffects,
- );
-
- // Time progression
- let hour = ctx.game.hour + HOURS_PER_TICK;
- let day = ctx.game.day;
- if (hour >= 24) {
- hour -= 24;
- day += 1;
- }
-
- // Check for loop end
- if (day > MAX_DAY) {
- const insightGained = calcInsight({
- maxFloorReached: ctx.combat.maxFloorReached,
- totalManaGathered: ctx.mana.totalManaGathered,
- signedPacts: ctx.prestige.signedPacts,
- prestigeUpgrades: ctx.prestige.prestigeUpgrades,
- skills: {},
- }, disciplineEffects);
-
- addLog(`β° The loop ends. Gained ${insightGained} Insight.`);
- writes.ui = { ...(writes.ui || {}), gameOver: true, victory: false };
- writes.prestige = { ...(writes.prestige || {}), loopInsight: insightGained };
- writes.game = { day, hour };
- applyTickWrites(writes, {
+ // Shared setters object β used by every applyTickWrites call below
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const storeSetters = {
setGame: set,
- setUI: (w) => useUIStore.setState(w),
- setPrestige: (w) => usePrestigeStore.setState(w),
- setMana: (w) => useManaStore.setState(w),
- setCombat: (w) => useCombatStore.setState(w),
- setCrafting: (w) => useCraftingStore.setState(w),
- setAttunement: (w) => useAttunementStore.setState(w),
- setDiscipline: (w) => useDisciplineStore.setState(w),
- addLogs: (msgs) => msgs.forEach((m) => useUIStore.getState().addLog(m)),
- });
- return;
- }
+ setUI: (w: any) => useUIStore.setState(w),
+ setPrestige: (w: any) => usePrestigeStore.setState(w),
+ setMana: (w: any) => useManaStore.setState(w),
+ setCombat: (w: any) => useCombatStore.setState(w),
+ setCrafting: (w: any) => useCraftingStore.setState(w),
+ setAttunement: (w: any) => useAttunementStore.setState(w),
+ setDiscipline: (w: any) => useDisciplineStore.setState(w),
+ addLogs: (msgs: string[]) => msgs.forEach((m) => useUIStore.getState().addLog(m)),
+ };
- // Check for victory
- if (ctx.combat.maxFloorReached >= 100 && ctx.prestige.signedPacts.includes(100)) {
- const insightGained = calcInsight({
- maxFloorReached: ctx.combat.maxFloorReached,
- totalManaGathered: ctx.mana.totalManaGathered,
- signedPacts: ctx.prestige.signedPacts,
- prestigeUpgrades: ctx.prestige.prestigeUpgrades,
- skills: {},
- }, disciplineEffects) * 3;
+ // ββ Phase 2: Compute β derive all updates βββββββββββββββββββββββββββ
+ const writes: TickWrites = { logs: [] };
+ const addLog = (msg: string) => writes.logs.push(msg);
- addLog(`π VICTORY! The Awakened One falls! Gained ${insightGained} Insight!`);
- writes.ui = { ...(writes.ui || {}), gameOver: true, victory: true };
- writes.prestige = { ...(writes.prestige || {}), loopInsight: insightGained };
- applyTickWrites(writes, {
- setGame: set,
- setUI: (w) => useUIStore.setState(w),
- setPrestige: (w) => usePrestigeStore.setState(w),
- setMana: (w) => useManaStore.setState(w),
- setCombat: (w) => useCombatStore.setState(w),
- setCrafting: (w) => useCraftingStore.setState(w),
- setAttunement: (w) => useAttunementStore.setState(w),
- setDiscipline: (w) => useDisciplineStore.setState(w),
- addLogs: (msgs) => msgs.forEach((m) => useUIStore.getState().addLog(m)),
- });
- return;
- }
+ // Compute equipment and discipline effects
+ const equipmentEffects = computeEquipmentEffects(
+ ctx.crafting.equipmentInstances || {},
+ ctx.crafting.equippedInstances || {}
+ );
+ const disciplineEffects = computeDisciplineEffects();
+ const allSpecials = new Set([
+ ...equipmentEffects.specials,
+ ...disciplineEffects.specials,
+ ]);
+ const effects = { specials: allSpecials } as ComputedEffects;
- // Incursion
- const incursionStrength = getIncursionStrength(day, hour);
-
- // Meditation bonus tracking
- let meditateTicks = ctx.mana.meditateTicks;
- let meditationMultiplier = 1;
-
- if (ctx.combat.currentAction === 'meditate') {
- meditateTicks++;
- meditationMultiplier = getMeditationBonus(meditateTicks, {}, 1);
- } else {
- meditateTicks = 0;
- }
-
- // Calculate total attunement conversion per tick
- let totalConversionPerTick = 0;
- Object.entries(ctx.attunement.attunements).forEach(([id, state]) => {
- if (!state.active) return;
- const def = ATTUNEMENTS_DEF[id];
- if (!def || def.conversionRate <= 0 || !def.primaryManaType) return;
- const scaledRate = getAttunementConversionRate(id, state.level || 1);
- totalConversionPerTick += scaledRate * HOURS_PER_TICK;
- });
-
- // Calculate effective regen
- const effectiveRegen = Math.max(0, baseRegen * (1 - incursionStrength) * meditationMultiplier - totalConversionPerTick);
-
- // Mana regeneration
- let rawMana = Math.min(ctx.mana.rawMana + effectiveRegen * HOURS_PER_TICK, maxMana);
- let elements = { ...ctx.mana.elements };
-
- // Apply attunement conversion
- Object.entries(ctx.attunement.attunements).forEach(([id, state]) => {
- if (!state.active) return;
- const def = ATTUNEMENTS_DEF[id];
- if (!def || def.conversionRate <= 0 || !def.primaryManaType) return;
- const scaledRate = getAttunementConversionRate(id, state.level || 1);
- const conversionThisTick = scaledRate * HOURS_PER_TICK;
- if (elements[def.primaryManaType]) {
- elements[def.primaryManaType].current = Math.min(
- elements[def.primaryManaType].max,
- elements[def.primaryManaType].current + conversionThisTick
- );
- }
- });
- let totalManaGathered = ctx.mana.totalManaGathered;
-
- // Convert action β delegate to manaStore
- if (ctx.combat.currentAction === 'convert') {
- const convertResult = useManaStore.getState().processConvertAction(rawMana);
- if (convertResult) {
- rawMana = convertResult.rawMana;
- elements = convertResult.elements;
- }
- }
-
- // Pact ritual progress
- if (ctx.prestige.pactRitualFloor !== null) {
- const guardian = getGuardianForFloor(ctx.prestige.pactRitualFloor);
- if (guardian) {
- const pactAffinityBonus = 1 - (ctx.prestige.prestigeUpgrades.pactAffinity || 0) * 0.1;
- const requiredTime = guardian.pactTime * pactAffinityBonus;
- const newProgress = ctx.prestige.pactRitualProgress + HOURS_PER_TICK;
-
- if (newProgress >= requiredTime) {
- addLog(`π Pact signed with ${guardian.name}! You have gained their boons.`);
- writes.prestige = {
- ...(writes.prestige || {}),
- signedPacts: [...ctx.prestige.signedPacts, ctx.prestige.pactRitualFloor],
- defeatedGuardians: ctx.prestige.defeatedGuardians.filter(f => f !== ctx.prestige.pactRitualFloor),
- pactRitualFloor: null,
- pactRitualProgress: 0,
- };
- } else {
- writes.prestige = {
- ...(writes.prestige || {}),
- pactRitualProgress: newProgress,
- };
- }
- }
- }
-
- // Discipline tick β process active disciplines (XP accrual + mana drain)
- const disciplineResult = useDisciplineStore.getState().processTick({
- rawMana,
- elements,
- });
- rawMana = disciplineResult.rawMana;
- elements = disciplineResult.elements;
-
- // Apply per-element regen from discipline effects (regen_{element})
- for (const [key, value] of Object.entries(disciplineEffects.bonuses)) {
- if (key.startsWith('regen_') && key !== 'regenBonus') {
- const element = key.replace('regen_', '');
- if (elements[element]) {
- elements[element] = {
- ...elements[element],
- current: Math.min(
- elements[element].max,
- elements[element].current + value * HOURS_PER_TICK,
- ),
- };
- }
- }
- }
-
- // Unlock enchantment effects from newly unlocked discipline perks
- if (disciplineResult.unlockedEffects.length > 0) {
- useCraftingStore.getState().unlockEffects(disciplineResult.unlockedEffects);
- for (const effectId of disciplineResult.unlockedEffects) {
- addLog(`β¨ Discipline insight unlocked: ${effectId}`);
- }
- }
-
- // Combat β delegate to combatStore
- if (ctx.combat.currentAction === 'climb') {
- const combatResult = useCombatStore.getState().processCombatTick(
- rawMana,
- elements,
- maxMana,
- 1,
- (floor, wasGuardian) => {
- if (wasGuardian) {
- const defeatedGuardian = getGuardianForFloor(floor);
- addLog(`\u2694\ufe0f ${defeatedGuardian?.name || 'Guardian'} defeated! Visit the Grimoire to sign a pact.`);
- } else if (floor % 5 === 0) {
- addLog(`π° Floor ${floor} cleared!`);
- }
- },
- (damage) => {
- let dmg = damage;
- if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && ctx.combat.floorHP / ctx.combat.floorMaxHP < 0.25) {
- dmg *= 2;
- }
- if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
- dmg *= 1.5;
- }
- return { rawMana, elements, modifiedDamage: dmg };
- },
- ctx.prestige.signedPacts,
+ const maxMana = computeMaxMana(
+ { skills: {}, prestigeUpgrades: ctx.prestige.prestigeUpgrades, skillUpgrades: {}, skillTiers: {} },
+ undefined,
+ disciplineEffects,
+ );
+ const baseRegen = computeRegen(
+ { skills: {}, prestigeUpgrades: ctx.prestige.prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, attunements: {} },
+ undefined,
+ disciplineEffects,
);
- rawMana = combatResult.rawMana;
- elements = combatResult.elements;
- totalManaGathered += combatResult.totalManaGathered || 0;
-
- if (combatResult.logMessages) {
- combatResult.logMessages.forEach(msg => addLog(msg));
+ // Time progression
+ let hour = ctx.game.hour + HOURS_PER_TICK;
+ let day = ctx.game.day;
+ if (hour >= 24) {
+ hour -= 24;
+ day += 1;
}
- writes.combat = {
- ...(writes.combat || {}),
- currentFloor: combatResult.currentFloor,
- floorHP: combatResult.floorHP,
- floorMaxHP: combatResult.floorMaxHP,
- maxFloorReached: combatResult.maxFloorReached,
- castProgress: combatResult.castProgress,
- equipmentSpellStates: combatResult.equipmentSpellStates,
+ // Shared insight params β reused for both loop-end and victory
+ const insightParams = {
+ maxFloorReached: ctx.combat.maxFloorReached,
+ totalManaGathered: ctx.mana.totalManaGathered,
+ signedPacts: ctx.prestige.signedPacts,
+ prestigeUpgrades: ctx.prestige.prestigeUpgrades,
+ skills: {} as Record,
};
- }
- // ββ Phase 3: Write β batch all state updates βββββββββββββββββββββββββ
- writes.game = { day, hour, incursionStrength };
- writes.mana = {
- rawMana,
- meditateTicks,
- totalManaGathered,
- elements,
- };
+ // Check for loop end
+ if (day > MAX_DAY) {
+ const insightGained = calcInsight(insightParams, disciplineEffects);
- applyTickWrites(writes, {
- setGame: set,
- setUI: (w) => useUIStore.setState(w),
- setPrestige: (w) => usePrestigeStore.setState(w),
- setMana: (w) => useManaStore.setState(w),
- setCombat: (w) => useCombatStore.setState(w),
- setCrafting: (w) => useCraftingStore.setState(w),
- setAttunement: (w) => useAttunementStore.setState(w),
- setDiscipline: (w) => useDisciplineStore.setState(w),
- addLogs: (msgs) => msgs.forEach((m) => useUIStore.getState().addLog(m)),
- });
+ addLog(`β° The loop ends. Gained ${insightGained} Insight.`);
+ writes.ui = { ...(writes.ui || {}), gameOver: true, victory: false };
+ writes.prestige = { ...(writes.prestige || {}), loopInsight: insightGained };
+ writes.game = { day, hour };
+ applyTickWrites(writes, storeSetters);
+ return;
+ }
+
+ // Check for victory (3Γ insight multiplier)
+ if (ctx.combat.maxFloorReached >= 100 && ctx.prestige.signedPacts.includes(100)) {
+ const insightGained = calcInsight(insightParams, disciplineEffects) * 3;
+
+ addLog(`π VICTORY! The Awakened One falls! Gained ${insightGained} Insight!`);
+ writes.ui = { ...(writes.ui || {}), gameOver: true, victory: true };
+ writes.prestige = { ...(writes.prestige || {}), loopInsight: insightGained };
+ applyTickWrites(writes, storeSetters);
+ return;
+ }
+
+ // Incursion
+ const incursionStrength = getIncursionStrength(day, hour);
+
+ // Meditation bonus tracking
+ let meditateTicks = ctx.mana.meditateTicks;
+ let meditationMultiplier = 1;
+
+ if (ctx.combat.currentAction === 'meditate') {
+ meditateTicks++;
+ meditationMultiplier = getMeditationBonus(meditateTicks, {}, 1);
+ } else {
+ meditateTicks = 0;
+ }
+
+ // Calculate total attunement conversion per tick
+ let totalConversionPerTick = 0;
+ Object.entries(ctx.attunement.attunements).forEach(([id, state]) => {
+ if (!state.active) return;
+ const def = ATTUNEMENTS_DEF[id];
+ if (!def || def.conversionRate <= 0 || !def.primaryManaType) return;
+ const scaledRate = getAttunementConversionRate(id, state.level || 1);
+ totalConversionPerTick += scaledRate * HOURS_PER_TICK;
+ });
+
+ // Calculate effective regen
+ const effectiveRegen = Math.max(0, baseRegen * (1 - incursionStrength) * meditationMultiplier - totalConversionPerTick);
+
+ // Mana regeneration
+ let rawMana = Math.min(ctx.mana.rawMana + effectiveRegen * HOURS_PER_TICK, maxMana);
+ let elements = { ...ctx.mana.elements };
+
+ // Apply attunement conversion
+ Object.entries(ctx.attunement.attunements).forEach(([id, state]) => {
+ if (!state.active) return;
+ const def = ATTUNEMENTS_DEF[id];
+ if (!def || def.conversionRate <= 0 || !def.primaryManaType) return;
+ const scaledRate = getAttunementConversionRate(id, state.level || 1);
+ const conversionThisTick = scaledRate * HOURS_PER_TICK;
+ if (elements[def.primaryManaType]) {
+ elements[def.primaryManaType].current = Math.min(
+ elements[def.primaryManaType].max,
+ elements[def.primaryManaType].current + conversionThisTick
+ );
+ }
+ });
+ let totalManaGathered = ctx.mana.totalManaGathered;
+
+ // Convert action β delegate to manaStore
+ if (ctx.combat.currentAction === 'convert') {
+ const convertResult = useManaStore.getState().processConvertAction(rawMana);
+ if (convertResult) {
+ rawMana = convertResult.rawMana;
+ elements = convertResult.elements;
+ }
+ }
+
+ // Pact ritual progress
+ if (ctx.prestige.pactRitualFloor !== null) {
+ const guardian = getGuardianForFloor(ctx.prestige.pactRitualFloor);
+ if (guardian) {
+ const pactAffinityBonus = 1 - (ctx.prestige.prestigeUpgrades.pactAffinity || 0) * 0.1;
+ const requiredTime = guardian.pactTime * pactAffinityBonus;
+ const newProgress = ctx.prestige.pactRitualProgress + HOURS_PER_TICK;
+
+ if (newProgress >= requiredTime) {
+ addLog(`π Pact signed with ${guardian.name}! You have gained their boons.`);
+ writes.prestige = {
+ ...(writes.prestige || {}),
+ signedPacts: [...ctx.prestige.signedPacts, ctx.prestige.pactRitualFloor],
+ defeatedGuardians: ctx.prestige.defeatedGuardians.filter(f => f !== ctx.prestige.pactRitualFloor),
+ pactRitualFloor: null,
+ pactRitualProgress: 0,
+ };
+ } else {
+ writes.prestige = {
+ ...(writes.prestige || {}),
+ pactRitualProgress: newProgress,
+ };
+ }
+ }
+ }
+
+ // Discipline tick β process active disciplines (XP accrual + mana drain)
+ const disciplineResult = useDisciplineStore.getState().processTick({
+ rawMana,
+ elements,
+ });
+ rawMana = disciplineResult.rawMana;
+ elements = disciplineResult.elements;
+
+ // Apply per-element regen from discipline effects (regen_{element})
+ for (const [key, value] of Object.entries(disciplineEffects.bonuses)) {
+ if (key.startsWith('regen_') && key !== 'regenBonus') {
+ const element = key.replace('regen_', '');
+ if (elements[element]) {
+ elements[element] = {
+ ...elements[element],
+ current: Math.min(
+ elements[element].max,
+ elements[element].current + value * HOURS_PER_TICK,
+ ),
+ };
+ }
+ }
+ }
+
+ // Unlock enchantment effects from newly unlocked discipline perks
+ if (disciplineResult.unlockedEffects.length > 0) {
+ useCraftingStore.getState().unlockEffects(disciplineResult.unlockedEffects);
+ for (const effectId of disciplineResult.unlockedEffects) {
+ addLog(`β¨ Discipline insight unlocked: ${effectId}`);
+ }
+ }
+
+ // Combat β delegate to combatStore
+ if (ctx.combat.currentAction === 'climb') {
+ const combatResult = useCombatStore.getState().processCombatTick(
+ rawMana,
+ elements,
+ maxMana,
+ 1,
+ (floor, wasGuardian) => {
+ if (wasGuardian) {
+ const defeatedGuardian = getGuardianForFloor(floor);
+ addLog(`\u2694\ufe0f ${defeatedGuardian?.name || 'Guardian'} defeated! Visit the Grimoire to sign a pact.`);
+ } else if (floor % 5 === 0) {
+ addLog(`π° Floor ${floor} cleared!`);
+ }
+ },
+ (damage) => {
+ let dmg = damage;
+ if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && ctx.combat.floorHP / ctx.combat.floorMaxHP < 0.25) {
+ dmg *= 2;
+ }
+ if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
+ dmg *= 1.5;
+ }
+ return { rawMana, elements, modifiedDamage: dmg };
+ },
+ ctx.prestige.signedPacts,
+ );
+
+ rawMana = combatResult.rawMana;
+ elements = combatResult.elements;
+ totalManaGathered += combatResult.totalManaGathered || 0;
+
+ if (combatResult.logMessages) {
+ combatResult.logMessages.forEach(msg => addLog(msg));
+ }
+
+ writes.combat = {
+ ...(writes.combat || {}),
+ currentFloor: combatResult.currentFloor,
+ floorHP: combatResult.floorHP,
+ floorMaxHP: combatResult.floorMaxHP,
+ maxFloorReached: combatResult.maxFloorReached,
+ castProgress: combatResult.castProgress,
+ equipmentSpellStates: combatResult.equipmentSpellStates,
+ };
+ }
+
+ // ββ Phase 3: Write β batch all state updates βββββββββββββββββββββββββ
+ writes.game = { day, hour, incursionStrength };
+ writes.mana = {
+ rawMana,
+ meditateTicks,
+ totalManaGathered,
+ elements,
+ };
+
+ applyTickWrites(writes, storeSetters);
} catch (error: unknown) {
// Log error to UI store if available, otherwise console error
try {
diff --git a/src/lib/game/stores/manaStore.ts b/src/lib/game/stores/manaStore.ts
index 4911b34..c5ecca6 100755
--- a/src/lib/game/stores/manaStore.ts
+++ b/src/lib/game/stores/manaStore.ts
@@ -86,7 +86,6 @@ export const useManaStore = create()(
spendRawMana: (amount: number) => {
const state = get();
if (state.rawMana < amount) return false;
-
set({ rawMana: state.rawMana - amount });
return true;
},
@@ -98,71 +97,35 @@ export const useManaStore = create()(
}));
},
- setMeditateTicks: (ticks: number) => {
- set({ meditateTicks: ticks });
- },
-
- incrementMeditateTicks: () => {
- set((state) => ({ meditateTicks: state.meditateTicks + 1 }));
- },
-
- resetMeditateTicks: () => {
- set({ meditateTicks: 0 });
- },
+ setMeditateTicks: (ticks: number) => set({ meditateTicks: ticks }),
+ incrementMeditateTicks: () => set((s) => ({ meditateTicks: s.meditateTicks + 1 })),
+ resetMeditateTicks: () => set({ meditateTicks: 0 }),
convertMana: (element: string, amount: number) => {
const state = get();
const elem = state.elements[element];
- if (!elem?.unlocked) {
- return fail(ErrorCode.ELEMENT_NOT_UNLOCKED, `Element ${element} is not unlocked`);
- }
+ if (!elem?.unlocked) return fail(ErrorCode.ELEMENT_NOT_UNLOCKED, `Element ${element} is not unlocked`);
const cost = MANA_PER_ELEMENT * amount;
- if (state.rawMana < cost) {
- return fail(ErrorCode.INSUFFICIENT_MANA, `Need ${cost} raw mana, have ${state.rawMana}`);
- }
- if (elem.current >= elem.max) {
- return fail(ErrorCode.ELEMENT_MAX_CAPACITY, `Element ${element} is at max capacity`);
- }
+ if (state.rawMana < cost) return fail(ErrorCode.INSUFFICIENT_MANA, `Need ${cost} raw mana, have ${state.rawMana}`);
+ if (elem.current >= elem.max) return fail(ErrorCode.ELEMENT_MAX_CAPACITY, `Element ${element} is at max capacity`);
- const canConvert = Math.min(
- amount,
- Math.floor(state.rawMana / MANA_PER_ELEMENT),
- elem.max - elem.current
- );
-
- if (canConvert <= 0) {
- return fail(ErrorCode.INVALID_INPUT, 'Cannot convert 0 or negative amount');
- }
+ const canConvert = Math.min(amount, Math.floor(state.rawMana / MANA_PER_ELEMENT), elem.max - elem.current);
+ if (canConvert <= 0) return fail(ErrorCode.INVALID_INPUT, 'Cannot convert 0 or negative amount');
set({
rawMana: state.rawMana - canConvert * MANA_PER_ELEMENT,
- elements: {
- ...state.elements,
- [element]: { ...elem, current: elem.current + canConvert },
- },
+ elements: { ...state.elements, [element]: { ...elem, current: elem.current + canConvert } },
});
-
return ok({ converted: canConvert });
},
unlockElement: (element: string, cost: number) => {
const state = get();
- if (state.elements[element]?.unlocked) {
- return fail(ErrorCode.INVALID_INPUT, `Element ${element} is already unlocked`);
- }
- if (state.rawMana < cost) {
- return fail(ErrorCode.INSUFFICIENT_MANA, `Need ${cost} raw mana, have ${state.rawMana}`);
- }
-
- set({
- rawMana: state.rawMana - cost,
- elements: {
- ...state.elements,
- [element]: { ...state.elements[element], unlocked: true },
- },
- });
+ if (state.elements[element]?.unlocked) return fail(ErrorCode.INVALID_INPUT, `Element ${element} is already unlocked`);
+ if (state.rawMana < cost) return fail(ErrorCode.INSUFFICIENT_MANA, `Need ${cost} raw mana, have ${state.rawMana}`);
+ set({ rawMana: state.rawMana - cost, elements: { ...state.elements, [element]: { ...state.elements[element], unlocked: true } } });
return okVoid();
},
@@ -170,15 +133,8 @@ export const useManaStore = create()(
set((state) => {
const elem = state.elements[element];
if (!elem) return state;
-
return {
- elements: {
- ...state.elements,
- [element]: {
- ...elem,
- current: Math.min(elem.current + amount, max),
- },
- },
+ elements: { ...state.elements, [element]: { ...elem, current: Math.min(elem.current + amount, max) } },
};
});
},
@@ -186,64 +142,35 @@ export const useManaStore = create()(
spendElementMana: (element: string, amount: number) => {
const state = get();
const elem = state.elements[element];
- if (!elem) {
- return fail(ErrorCode.INVALID_ELEMENT, `Element ${element} does not exist`);
- }
- if (elem.current < amount) {
- return fail(ErrorCode.INSUFFICIENT_MANA, `Need ${amount} ${element} mana, have ${elem.current}`);
- }
-
- set({
- elements: {
- ...state.elements,
- [element]: { ...elem, current: elem.current - amount },
- },
- });
+ if (!elem) return fail(ErrorCode.INVALID_ELEMENT, `Element ${element} does not exist`);
+ if (elem.current < amount) return fail(ErrorCode.INSUFFICIENT_MANA, `Need ${amount} ${element} mana, have ${elem.current}`);
+ set({ elements: { ...state.elements, [element]: { ...elem, current: elem.current - amount } } });
return okVoid();
},
setElementMax: (max: number) => {
set((state) => ({
- elements: Object.fromEntries(
- Object.entries(state.elements).map(([k, v]) => [k, { ...v, max }])
- ) as Record,
+ elements: Object.fromEntries(Object.entries(state.elements).map(([k, v]) => [k, { ...v, max }])) as Record,
}));
},
craftComposite: (target: string, recipe: string[]) => {
const state = get();
-
- // Count required ingredients
const costs: Record = {};
- recipe.forEach(r => {
- costs[r] = (costs[r] || 0) + 1;
- });
+ recipe.forEach(r => { costs[r] = (costs[r] || 0) + 1; });
- // Check if we have all ingredients
for (const [r, amt] of Object.entries(costs)) {
- if ((state.elements[r]?.current || 0) < amt) {
- return fail(ErrorCode.INSUFFICIENT_MANA, `Need ${amt} ${r} mana, have ${state.elements[r]?.current || 0}`);
- }
+ if ((state.elements[r]?.current || 0) < amt) return fail(ErrorCode.INSUFFICIENT_MANA, `Need ${amt} ${r} mana, have ${state.elements[r]?.current || 0}`);
}
- // Deduct ingredients
const newElems = { ...state.elements };
for (const [r, amt] of Object.entries(costs)) {
- newElems[r] = {
- ...newElems[r],
- current: newElems[r].current - amt,
- };
+ newElems[r] = { ...newElems[r], current: newElems[r].current - amt };
}
- // Add crafted element
const targetElem = newElems[target];
- newElems[target] = {
- ...(targetElem || { current: 0, max: 10, unlocked: false }),
- current: (targetElem?.current || 0) + 1,
- unlocked: true,
- };
-
+ newElems[target] = { ...(targetElem || { current: 0, max: 10, unlocked: false }), current: (targetElem?.current || 0) + 1, unlocked: true };
set({ elements: newElems });
return okVoid();
},
@@ -252,27 +179,16 @@ export const useManaStore = create()(
const state = get();
const elements = { ...state.elements };
- const unlockedElements = Object.entries(elements)
- .filter(([, e]) => e.unlocked && e.current < e.max);
-
- if (unlockedElements.length === 0 || rawMana < 100) return null;
+ const unlockedElements = Object.entries(elements).filter(([, e]) => e.unlocked && e.current < e.max);
+ if (unlockedElements.length === 0 || rawMana < MANA_PER_ELEMENT) return null;
unlockedElements.sort((a, b) => (b[1].max - b[1].current) - (a[1].max - a[1].current));
const [targetId, targetState] = unlockedElements[0];
- const canConvert = Math.min(
- Math.floor(rawMana / 100),
- targetState.max - targetState.current
- );
-
+ const canConvert = Math.min(Math.floor(rawMana / MANA_PER_ELEMENT), targetState.max - targetState.current);
if (canConvert <= 0) return null;
- rawMana -= canConvert * 100;
- const updatedElements = {
- ...elements,
- [targetId]: { ...targetState, current: targetState.current + canConvert }
- };
-
- return { rawMana, elements: updatedElements };
+ rawMana -= canConvert * MANA_PER_ELEMENT;
+ return { rawMana, elements: { ...elements, [targetId]: { ...targetState, current: targetState.current + canConvert } } };
},
resetMana: (
@@ -283,24 +199,13 @@ export const useManaStore = create()(
) => {
const elementMax = 10 + (prestigeUpgrades.elemMax || 0) * 5;
const startingMana = 10 + (prestigeUpgrades.manaStart || 0) * 10;
- const elements = makeInitialElements(elementMax, prestigeUpgrades);
-
- set({
- rawMana: startingMana,
- meditateTicks: 0,
- totalManaGathered: 0,
- elements,
- });
+ set({ rawMana: startingMana, meditateTicks: 0, totalManaGathered: 0, elements: makeInitialElements(elementMax, prestigeUpgrades) });
},
}),
{
storage: createSafeStorage(),
name: 'mana-loop-mana',
- partialize: (state) => ({
- rawMana: state.rawMana,
- totalManaGathered: state.totalManaGathered,
- elements: state.elements,
- }),
+ partialize: (state) => ({ rawMana: state.rawMana, totalManaGathered: state.totalManaGathered, elements: state.elements }),
}
)
);
@@ -311,16 +216,10 @@ export function makeInitialElements(
prestigeUpgrades: Record = {}
): Record {
const elemStart = (prestigeUpgrades.elemStart || 0) * 5;
-
const elements: Record = {};
- Object.keys(ELEMENTS).forEach(k => {
+ for (const k of Object.keys(ELEMENTS)) {
const isUnlocked = BASE_UNLOCKED_ELEMENTS.includes(k);
- elements[k] = {
- current: isUnlocked ? elemStart : 0,
- max: elementMax,
- unlocked: isUnlocked,
- };
- });
-
+ elements[k] = { current: isUnlocked ? elemStart : 0, max: elementMax, unlocked: isUnlocked };
+ }
return elements;
}