Compare commits

...

4 Commits

Author SHA1 Message Date
Z User
2edce9680a Add magic swords, weapon mana enchantments, and elemental sword enchantments
Some checks failed
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m12s
- Added 'sword' equipment category with multiple blade types
- Magic swords have low base damage but high cast speed
- Added weapon mana capacity enchantments (Mana Cell, Mana Vessel, Mana Core)
- Added weapon mana regeneration enchantments (Mana Wick, Mana Siphon, Mana Well)
- Added elemental sword enchantments (Fire, Frost, Lightning, Void)
- Updated equipment helper functions to handle sword category
2026-03-30 15:37:47 +00:00
Z User
cd10918a3b Update combat system with AOE, armor, dodge, and puzzle rooms
- Updated combat tick to handle multiple enemies (swarm rooms)
- Added armor reduction calculation with armor pierce
- Added dodge chance handling for speed rooms
- Lightning spells have 30% faster cast and 50% reduced dodge chance
- Frost Blade enchantment prevents dodge (freeze effect)
- Puzzle rooms progress based on relevant attunement levels
- Room state is now tracked in currentRoom field
2026-03-30 15:35:09 +00:00
Z User
dd9528a418 Add Lightning mana type, AOE spells, room types, and monster armor
Features added:
- Lightning mana type (Fire + Air composite)
- AOE spells (Chain Lightning, Fireball AOE, Blizzard, etc.)
- Magic sword enchantments (Fire Blade, Frost Blade, Lightning Blade)
- Monster armor system (all guardians have armor, scales with floors)
- Room types (puzzle, swarm, speed) with generation logic
- FloorState and EnemyState types for room handling
- Weapon mana storage fields for future magic sword system
2026-03-30 15:32:40 +00:00
Z User
0bc2b45b96 Merge remote changes 2026-03-30 15:21:08 +00:00
7 changed files with 840 additions and 42 deletions

0
src/lib/game/__tests__/bug-fixes.test.ts Normal file → Executable file
View File

View File

@@ -39,10 +39,11 @@ export const ELEMENTS: Record<string, ElementDef> = {
force: { name: "Force", sym: "💨", color: "#E74C3C", glow: "#E74C3C40", cat: "utility" },
// Composite Elements
blood: { name: "Blood", sym: "🩸", color: "#C0392B", glow: "#C0392B40", cat: "composite", recipe: ["life", "water"] },
metal: { name: "Metal", sym: "⚙️", color: "#BDC3C7", glow: "#BDC3C740", cat: "composite", recipe: ["fire", "earth"] },
wood: { name: "Wood", sym: "🪵", color: "#8B5E3C", glow: "#8B5E3C40", cat: "composite", recipe: ["life", "earth"] },
sand: { name: "Sand", sym: "⏳", color: "#D4AC0D", glow: "#D4AC0D40", cat: "composite", recipe: ["earth", "water"] },
blood: { name: "Blood", sym: "🩸", color: "#C0392B", glow: "#C0392B40", cat: "composite", recipe: ["life", "water"] },
metal: { name: "Metal", sym: "⚙️", color: "#BDC3C7", glow: "#BDC3C740", cat: "composite", recipe: ["fire", "earth"] },
wood: { name: "Wood", sym: "🪵", color: "#8B5E3C", glow: "#8B5E3C40", cat: "composite", recipe: ["life", "earth"] },
sand: { name: "Sand", sym: "⏳", color: "#D4AC0D", glow: "#D4AC0D40", cat: "composite", recipe: ["earth", "water"] },
lightning: { name: "Lightning", sym: "⚡", color: "#FFEB3B", glow: "#FFEB3B40", cat: "composite", recipe: ["fire", "air"] },
// Exotic Elements
crystal: { name: "Crystal", sym: "💎", color: "#85C1E9", glow: "#85C1E940", cat: "exotic", recipe: ["sand", "sand", "mental"] },
@@ -52,10 +53,103 @@ export const ELEMENTS: Record<string, ElementDef> = {
export const FLOOR_ELEM_CYCLE = ["fire", "water", "air", "earth", "light", "dark", "life", "death"];
// ─── Room Types ────────────────────────────────────────────────────────────────
// Room types for spire floors
export type RoomType = 'combat' | 'puzzle' | 'swarm' | 'speed' | 'guardian';
// Room generation rules:
// - Guardian floors (10, 20, 30, etc.) are ALWAYS guardian type
// - Every 5th floor (5, 15, 25, etc.) has a chance for special rooms
// - Other floors are combat with chance for swarm/speed
export const PUZZLE_ROOM_INTERVAL = 7; // Every 7 floors, chance for puzzle
export const SWARM_ROOM_CHANCE = 0.15; // 15% chance for swarm room
export const SPEED_ROOM_CHANCE = 0.10; // 10% chance for speed room
export const PUZZLE_ROOM_CHANCE = 0.20; // 20% chance for puzzle room on puzzle floors
// Puzzle room definitions - themed around attunements
export const PUZZLE_ROOMS: Record<string, {
name: string;
attunements: string[]; // Which attunements speed up progress
baseProgressPerTick: number; // Base progress rate
attunementBonus: number; // Multiplier per relevant attunement level
description: string;
}> = {
enchanter_trial: {
name: "Enchanter's Trial",
attunements: ['enchanter'],
baseProgressPerTick: 0.02,
attunementBonus: 0.03,
description: "Decipher ancient enchantment runes."
},
fabricator_trial: {
name: "Fabricator's Trial",
attunements: ['fabricator'],
baseProgressPerTick: 0.02,
attunementBonus: 0.03,
description: "Construct a mana-powered mechanism."
},
invoker_trial: {
name: "Invoker's Trial",
attunements: ['invoker'],
baseProgressPerTick: 0.02,
attunementBonus: 0.03,
description: "Commune with guardian spirits."
},
hybrid_enchanter_fabricator: {
name: "Fusion Workshop",
attunements: ['enchanter', 'fabricator'],
baseProgressPerTick: 0.015,
attunementBonus: 0.025,
description: "Enchant and construct in harmony."
},
hybrid_enchanter_invoker: {
name: "Ritual Circle",
attunements: ['enchanter', 'invoker'],
baseProgressPerTick: 0.015,
attunementBonus: 0.025,
description: "Bind pact energies into enchantments."
},
hybrid_fabricator_invoker: {
name: "Golem Forge",
attunements: ['fabricator', 'invoker'],
baseProgressPerTick: 0.015,
attunementBonus: 0.025,
description: "Channel guardian power into constructs."
},
};
// Swarm room configuration
export const SWARM_CONFIG = {
minEnemies: 3,
maxEnemies: 6,
hpMultiplier: 0.4, // Each enemy has 40% of normal floor HP
armorBase: 0, // Swarm enemies start with no armor
armorPerFloor: 0.01, // Gain 1% armor per 10 floors
};
// Speed room configuration (dodging enemies)
export const SPEED_ROOM_CONFIG = {
baseDodgeChance: 0.25, // 25% base dodge chance
dodgePerFloor: 0.005, // +0.5% dodge per floor
maxDodge: 0.50, // Max 50% dodge
speedBonus: 0.5, // 50% less time to complete if dodged
};
// Armor scaling for normal floors
export const FLOOR_ARMOR_CONFIG = {
baseChance: 0, // No armor on floor 1-9
chancePerFloor: 0.01, // +1% chance per floor after 10
maxArmorChance: 0.5, // Max 50% of floors have armor
minArmor: 0.05, // Min 5% armor
maxArmor: 0.25, // Max 25% armor on non-guardians
};
// ─── Guardians ────────────────────────────────────────────────────────────────
// All guardians have armor - damage reduction percentage
export const GUARDIANS: Record<number, GuardianDef> = {
10: {
name: "Ignis Prime", element: "fire", hp: 5000, pact: 1.5, color: "#FF6B35",
armor: 0.10, // 10% damage reduction
boons: [
{ type: 'elementalDamage', value: 5, desc: '+5% Fire damage' },
{ type: 'maxMana', value: 50, desc: '+50 max mana' },
@@ -66,6 +160,7 @@ export const GUARDIANS: Record<number, GuardianDef> = {
},
20: {
name: "Aqua Regia", element: "water", hp: 15000, pact: 1.75, color: "#4ECDC4",
armor: 0.15,
boons: [
{ type: 'elementalDamage', value: 5, desc: '+5% Water damage' },
{ type: 'manaRegen', value: 0.5, desc: '+0.5 mana regen' },
@@ -76,6 +171,7 @@ export const GUARDIANS: Record<number, GuardianDef> = {
},
30: {
name: "Ventus Rex", element: "air", hp: 30000, pact: 2.0, color: "#00D4FF",
armor: 0.18,
boons: [
{ type: 'elementalDamage', value: 5, desc: '+5% Air damage' },
{ type: 'castingSpeed', value: 5, desc: '+5% cast speed' },
@@ -86,6 +182,7 @@ export const GUARDIANS: Record<number, GuardianDef> = {
},
40: {
name: "Terra Firma", element: "earth", hp: 50000, pact: 2.25, color: "#F4A261",
armor: 0.25, // Earth guardian - highest armor
boons: [
{ type: 'elementalDamage', value: 5, desc: '+5% Earth damage' },
{ type: 'maxMana', value: 100, desc: '+100 max mana' },
@@ -96,6 +193,7 @@ export const GUARDIANS: Record<number, GuardianDef> = {
},
50: {
name: "Lux Aeterna", element: "light", hp: 80000, pact: 2.5, color: "#FFD700",
armor: 0.20,
boons: [
{ type: 'elementalDamage', value: 10, desc: '+10% Light damage' },
{ type: 'insightGain', value: 10, desc: '+10% insight gain' },
@@ -106,6 +204,7 @@ export const GUARDIANS: Record<number, GuardianDef> = {
},
60: {
name: "Umbra Mortis", element: "dark", hp: 120000, pact: 2.75, color: "#9B59B6",
armor: 0.22,
boons: [
{ type: 'elementalDamage', value: 10, desc: '+10% Dark damage' },
{ type: 'critDamage', value: 15, desc: '+15% crit damage' },
@@ -116,6 +215,7 @@ export const GUARDIANS: Record<number, GuardianDef> = {
},
70: {
name: "Vita Sempiterna", element: "life", hp: 180000, pact: 3.0, color: "#2ECC71",
armor: 0.20,
boons: [
{ type: 'elementalDamage', value: 10, desc: '+10% Life damage' },
{ type: 'manaRegen', value: 1, desc: '+1 mana regen' },
@@ -126,6 +226,7 @@ export const GUARDIANS: Record<number, GuardianDef> = {
},
80: {
name: "Mors Ultima", element: "death", hp: 250000, pact: 3.25, color: "#778CA3",
armor: 0.25,
boons: [
{ type: 'elementalDamage', value: 10, desc: '+10% Death damage' },
{ type: 'rawDamage', value: 10, desc: '+10% raw damage' },
@@ -136,6 +237,7 @@ export const GUARDIANS: Record<number, GuardianDef> = {
},
90: {
name: "Primordialis", element: "void", hp: 400000, pact: 4.0, color: "#4A235A",
armor: 0.30,
boons: [
{ type: 'elementalDamage', value: 15, desc: '+15% Void damage' },
{ type: 'maxMana', value: 200, desc: '+200 max mana' },
@@ -147,6 +249,7 @@ export const GUARDIANS: Record<number, GuardianDef> = {
},
100: {
name: "The Awakened One", element: "stellar", hp: 1000000, pact: 5.0, color: "#F0E68C",
armor: 0.35, // Final boss has highest armor
boons: [
{ type: 'elementalDamage', value: 20, desc: '+20% Stellar damage' },
{ type: 'maxMana', value: 500, desc: '+500 max mana' },
@@ -648,6 +751,228 @@ export const SPELLS_DEF: Record<string, SpellDef> = {
studyTime: 36,
desc: "Shatter crystalline energy."
},
// ═══════════════════════════════════════════════════════════════════════════
// LIGHTNING SPELLS - Fast, armor-piercing, harder to dodge
// ═══════════════════════════════════════════════════════════════════════════
// Tier 1 - Basic Lightning
spark: {
name: "Spark",
elem: "lightning",
dmg: 8,
cost: elemCost("lightning", 1),
tier: 1,
castSpeed: 4,
unlock: 120,
studyTime: 2,
desc: "A quick spark of lightning. Very fast and hard to dodge.",
effects: [{ type: 'armor_pierce', value: 0.2 }]
},
lightningBolt: {
name: "Lightning Bolt",
elem: "lightning",
dmg: 14,
cost: elemCost("lightning", 2),
tier: 1,
castSpeed: 3,
unlock: 150,
studyTime: 3,
desc: "A bolt of lightning that pierces armor.",
effects: [{ type: 'armor_pierce', value: 0.3 }]
},
// Tier 2 - Advanced Lightning
chainLightning: {
name: "Chain Lightning",
elem: "lightning",
dmg: 25,
cost: elemCost("lightning", 5),
tier: 2,
castSpeed: 2,
unlock: 900,
studyTime: 8,
desc: "Lightning that arcs between enemies. Hits 3 targets.",
isAoe: true,
aoeTargets: 3,
effects: [{ type: 'chain', value: 3 }]
},
stormCall: {
name: "Storm Call",
elem: "lightning",
dmg: 40,
cost: elemCost("lightning", 6),
tier: 2,
castSpeed: 1.5,
unlock: 1100,
studyTime: 10,
desc: "Call down a storm. Hits 2 targets with armor pierce.",
isAoe: true,
aoeTargets: 2,
effects: [{ type: 'armor_pierce', value: 0.4 }]
},
// Tier 3 - Master Lightning
thunderStrike: {
name: "Thunder Strike",
elem: "lightning",
dmg: 150,
cost: elemCost("lightning", 15),
tier: 3,
castSpeed: 0.8,
unlock: 10000,
studyTime: 24,
desc: "Devastating lightning that ignores 50% armor.",
effects: [{ type: 'armor_pierce', value: 0.5 }]
},
// ═══════════════════════════════════════════════════════════════════════════
// AOE SPELLS - Hit multiple enemies, less damage per target
// ═══════════════════════════════════════════════════════════════════════════
// Tier 1 AOE
fireballAoe: {
name: "Fireball (AOE)",
elem: "fire",
dmg: 8,
cost: elemCost("fire", 3),
tier: 1,
castSpeed: 2,
unlock: 150,
studyTime: 3,
desc: "An explosive fireball that hits 3 enemies.",
isAoe: true,
aoeTargets: 3,
effects: [{ type: 'aoe', value: 3 }]
},
frostNova: {
name: "Frost Nova",
elem: "water",
dmg: 6,
cost: elemCost("water", 3),
tier: 1,
castSpeed: 2,
unlock: 140,
studyTime: 3,
desc: "A burst of frost hitting 4 enemies. May freeze.",
isAoe: true,
aoeTargets: 4,
effects: [{ type: 'freeze', value: 0.15, chance: 0.2 }]
},
// Tier 2 AOE
meteorShower: {
name: "Meteor Shower",
elem: "fire",
dmg: 20,
cost: elemCost("fire", 8),
tier: 2,
castSpeed: 1,
unlock: 1200,
studyTime: 10,
desc: "Rain meteors on 5 enemies.",
isAoe: true,
aoeTargets: 5
},
blizzard: {
name: "Blizzard",
elem: "water",
dmg: 18,
cost: elemCost("water", 7),
tier: 2,
castSpeed: 1.2,
unlock: 1000,
studyTime: 9,
desc: "A freezing blizzard hitting 4 enemies.",
isAoe: true,
aoeTargets: 4,
effects: [{ type: 'freeze', value: 0.1, chance: 0.15 }]
},
earthquakeAoe: {
name: "Earth Tremor",
elem: "earth",
dmg: 25,
cost: elemCost("earth", 8),
tier: 2,
castSpeed: 0.8,
unlock: 1400,
studyTime: 10,
desc: "Shake the ground, hitting 3 enemies with high damage.",
isAoe: true,
aoeTargets: 3
},
// Tier 3 AOE
apocalypse: {
name: "Apocalypse",
elem: "fire",
dmg: 80,
cost: elemCost("fire", 20),
tier: 3,
castSpeed: 0.5,
unlock: 15000,
studyTime: 30,
desc: "End times. Hits ALL enemies with devastating fire.",
isAoe: true,
aoeTargets: 10
},
// ═══════════════════════════════════════════════════════════════════════════
// MAGIC SWORD ENCHANTMENTS - For weapon enchanting system
// ═══════════════════════════════════════════════════════════════════════════
fireBlade: {
name: "Fire Blade",
elem: "fire",
dmg: 3,
cost: rawCost(1),
tier: 1,
castSpeed: 4,
unlock: 100,
studyTime: 2,
desc: "Enchant a blade with fire. Burns enemies over time.",
isWeaponEnchant: true,
effects: [{ type: 'burn', value: 2, duration: 3 }]
},
frostBlade: {
name: "Frost Blade",
elem: "water",
dmg: 3,
cost: rawCost(1),
tier: 1,
castSpeed: 4,
unlock: 100,
studyTime: 2,
desc: "Enchant a blade with frost. Prevents enemy dodge.",
isWeaponEnchant: true,
effects: [{ type: 'freeze', value: 0, chance: 1 }] // 100% freeze = no dodge
},
lightningBlade: {
name: "Lightning Blade",
elem: "lightning",
dmg: 4,
cost: rawCost(1),
tier: 1,
castSpeed: 5,
unlock: 150,
studyTime: 3,
desc: "Enchant a blade with lightning. Pierces 30% armor.",
isWeaponEnchant: true,
effects: [{ type: 'armor_pierce', value: 0.3 }]
},
voidBlade: {
name: "Void Blade",
elem: "dark",
dmg: 5,
cost: rawCost(2),
tier: 2,
castSpeed: 3,
unlock: 800,
studyTime: 8,
desc: "Enchant a blade with void. 10% lifesteal.",
isWeaponEnchant: true,
effects: [{ type: 'lifesteal', value: 0.1 }]
},
};
// ─── Skills ───────────────────────────────────────────────────────────────────
@@ -848,6 +1173,7 @@ export const ELEMENT_OPPOSITES: Record<string, string> = {
air: 'earth', earth: 'air',
light: 'dark', dark: 'light',
life: 'death', death: 'life',
lightning: 'earth', // Lightning is weak to earth (grounding)
};
// ─── Element Icon Mapping (Lucide Icons) ──────────────────────────────────────
@@ -869,6 +1195,7 @@ export const ELEMENT_ICON_NAMES: Record<string, string> = {
metal: 'Target',
wood: 'TreeDeciduous',
sand: 'Hourglass',
lightning: 'Zap',
crystal: 'Gem',
stellar: 'Star',
void: 'CircleDot',

View File

@@ -5,12 +5,14 @@ import type { EquipmentCategory } from './equipment'
// Helper to define allowed equipment categories for each effect type
const ALL_CASTER: EquipmentCategory[] = ['caster']
const CASTER_AND_SWORD: EquipmentCategory[] = ['caster', 'sword']
const WEAPON_EQUIPMENT: EquipmentCategory[] = ['caster', 'catalyst', 'sword'] // All main hand equipment
const CASTER_AND_HANDS: EquipmentCategory[] = ['caster', 'hands']
const BODY_AND_SHIELD: EquipmentCategory[] = ['body', 'shield']
const CASTER_CATALYST_ACCESSORY: EquipmentCategory[] = ['caster', 'catalyst', 'accessory']
const MANA_EQUIPMENT: EquipmentCategory[] = ['caster', 'catalyst', 'head', 'body', 'accessory']
const UTILITY_EQUIPMENT: EquipmentCategory[] = ['caster', 'catalyst', 'head', 'body', 'hands', 'feet', 'accessory']
const ALL_EQUIPMENT: EquipmentCategory[] = ['caster', 'shield', 'catalyst', 'head', 'body', 'hands', 'feet', 'accessory']
const ALL_EQUIPMENT: EquipmentCategory[] = ['caster', 'shield', 'catalyst', 'sword', 'head', 'body', 'hands', 'feet', 'accessory']
export type EnchantmentEffectCategory = 'spell' | 'mana' | 'combat' | 'elemental' | 'defense' | 'utility' | 'special'
@@ -567,6 +569,117 @@ export const ENCHANTMENT_EFFECTS: Record<string, EnchantmentEffectDef> = {
allowedEquipmentCategories: CASTER_AND_HANDS,
effect: { type: 'special', specialId: 'overpower' }
},
// ═══════════════════════════════════════════════════════════════════════════
// WEAPON MANA EFFECTS - Enchanter level 3+ unlocks these
// These add mana capacity and regeneration to weapons for their enchantments
// ═══════════════════════════════════════════════════════════════════════════
weapon_mana_cap_20: {
id: 'weapon_mana_cap_20',
name: 'Mana Cell',
description: '+20 weapon mana capacity (for weapon enchantments)',
category: 'mana',
baseCapacityCost: 25,
maxStacks: 5,
allowedEquipmentCategories: WEAPON_EQUIPMENT,
effect: { type: 'bonus', stat: 'weaponManaMax', value: 20 }
},
weapon_mana_cap_50: {
id: 'weapon_mana_cap_50',
name: 'Mana Vessel',
description: '+50 weapon mana capacity (for weapon enchantments)',
category: 'mana',
baseCapacityCost: 50,
maxStacks: 3,
allowedEquipmentCategories: WEAPON_EQUIPMENT,
effect: { type: 'bonus', stat: 'weaponManaMax', value: 50 }
},
weapon_mana_cap_100: {
id: 'weapon_mana_cap_100',
name: 'Mana Core',
description: '+100 weapon mana capacity (for weapon enchantments)',
category: 'mana',
baseCapacityCost: 80,
maxStacks: 2,
allowedEquipmentCategories: WEAPON_EQUIPMENT,
effect: { type: 'bonus', stat: 'weaponManaMax', value: 100 }
},
weapon_mana_regen_1: {
id: 'weapon_mana_regen_1',
name: 'Mana Wick',
description: '+1 weapon mana regeneration per hour',
category: 'mana',
baseCapacityCost: 20,
maxStacks: 5,
allowedEquipmentCategories: WEAPON_EQUIPMENT,
effect: { type: 'bonus', stat: 'weaponManaRegen', value: 1 }
},
weapon_mana_regen_2: {
id: 'weapon_mana_regen_2',
name: 'Mana Siphon',
description: '+2 weapon mana regeneration per hour',
category: 'mana',
baseCapacityCost: 35,
maxStacks: 3,
allowedEquipmentCategories: WEAPON_EQUIPMENT,
effect: { type: 'bonus', stat: 'weaponManaRegen', value: 2 }
},
weapon_mana_regen_5: {
id: 'weapon_mana_regen_5',
name: 'Mana Well',
description: '+5 weapon mana regeneration per hour',
category: 'mana',
baseCapacityCost: 60,
maxStacks: 2,
allowedEquipmentCategories: WEAPON_EQUIPMENT,
effect: { type: 'bonus', stat: 'weaponManaRegen', value: 5 }
},
// ═══════════════════════════════════════════════════════════════════════════
// MAGIC SWORD ENCHANTMENTS - Elemental weapon effects
// ═══════════════════════════════════════════════════════════════════════════
sword_fire: {
id: 'sword_fire',
name: 'Fire Enchant',
description: 'Enchant blade with fire. Burns enemies over time.',
category: 'elemental',
baseCapacityCost: 40,
maxStacks: 1,
allowedEquipmentCategories: CASTER_AND_SWORD,
effect: { type: 'special', specialId: 'fireBlade' }
},
sword_frost: {
id: 'sword_frost',
name: 'Frost Enchant',
description: 'Enchant blade with frost. Prevents enemy dodge.',
category: 'elemental',
baseCapacityCost: 40,
maxStacks: 1,
allowedEquipmentCategories: CASTER_AND_SWORD,
effect: { type: 'special', specialId: 'frostBlade' }
},
sword_lightning: {
id: 'sword_lightning',
name: 'Lightning Enchant',
description: 'Enchant blade with lightning. Pierces 30% armor.',
category: 'elemental',
baseCapacityCost: 50,
maxStacks: 1,
allowedEquipmentCategories: CASTER_AND_SWORD,
effect: { type: 'special', specialId: 'lightningBlade' }
},
sword_void: {
id: 'sword_void',
name: 'Void Enchant',
description: 'Enchant blade with void. 10% lifesteal.',
category: 'elemental',
baseCapacityCost: 60,
maxStacks: 1,
allowedEquipmentCategories: CASTER_AND_SWORD,
effect: { type: 'special', specialId: 'voidBlade' }
},
};
// ─── Helper Functions ────────────────────────────────────────────────────────────

View File

@@ -1,7 +1,7 @@
// ─── Equipment Types ─────────────────────────────────────────────────────────
export type EquipmentSlot = 'mainHand' | 'offHand' | 'head' | 'body' | 'hands' | 'feet' | 'accessory1' | 'accessory2';
export type EquipmentCategory = 'caster' | 'shield' | 'catalyst' | 'head' | 'body' | 'hands' | 'feet' | 'accessory';
export type EquipmentCategory = 'caster' | 'shield' | 'catalyst' | 'sword' | 'head' | 'body' | 'hands' | 'feet' | 'accessory';
// All equipment slots in order
export const EQUIPMENT_SLOTS: EquipmentSlot[] = ['mainHand', 'offHand', 'head', 'body', 'hands', 'feet', 'accessory1', 'accessory2'];
@@ -13,6 +13,8 @@ export interface EquipmentType {
slot: EquipmentSlot;
baseCapacity: number;
description: string;
baseDamage?: number; // For swords
baseCastSpeed?: number; // For swords (higher = faster)
}
// ─── Equipment Types Definition ─────────────────────────────────────────────
@@ -94,6 +96,60 @@ export const EQUIPMENT_TYPES: Record<string, EquipmentType> = {
description: 'A rare catalyst touched by void energy. High capacity but volatile.',
},
// ─── Main Hand - Magic Swords ─────────────────────────────────────────────
// Magic swords have low base damage but high cast speed
// They can be enchanted with elemental effects that use mana over time
ironBlade: {
id: 'ironBlade',
name: 'Iron Blade',
category: 'sword',
slot: 'mainHand',
baseCapacity: 30,
baseDamage: 3,
baseCastSpeed: 4,
description: 'A simple iron sword. Can be enchanted with elemental effects.',
},
steelBlade: {
id: 'steelBlade',
name: 'Steel Blade',
category: 'sword',
slot: 'mainHand',
baseCapacity: 40,
baseDamage: 4,
baseCastSpeed: 4,
description: 'A well-crafted steel sword. Balanced for combat and enchanting.',
},
crystalBlade: {
id: 'crystalBlade',
name: 'Crystal Blade',
category: 'sword',
slot: 'mainHand',
baseCapacity: 55,
baseDamage: 3,
baseCastSpeed: 5,
description: 'A blade made of crystallized mana. Excellent for elemental enchantments.',
},
arcanistBlade: {
id: 'arcanistBlade',
name: 'Arcanist Blade',
category: 'sword',
slot: 'mainHand',
baseCapacity: 65,
baseDamage: 5,
baseCastSpeed: 4,
description: 'A sword forged for battle mages. High capacity for powerful enchantments.',
},
voidBlade: {
id: 'voidBlade',
name: 'Void-Touched Blade',
category: 'sword',
slot: 'mainHand',
baseCapacity: 50,
baseDamage: 6,
baseCastSpeed: 3,
description: 'A blade corrupted by void energy. Powerful but consumes more mana.',
},
// ─── Off Hand - Shields ───────────────────────────────────────────────────
basicShield: {
id: 'basicShield',
@@ -386,6 +442,7 @@ export function getValidSlotsForCategory(category: EquipmentCategory): Equipment
switch (category) {
case 'caster':
case 'catalyst':
case 'sword':
return ['mainHand'];
case 'shield':
return ['offHand'];

0
src/lib/game/debug-context.tsx Normal file → Executable file
View File

View File

@@ -2,7 +2,7 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { GameState, GameAction, StudyTarget, SpellCost, SkillUpgradeChoice, EquipmentSlot, EquipmentInstance, EnchantmentDesign, DesignEffect, AttunementState } from './types';
import type { GameState, GameAction, StudyTarget, SpellCost, SkillUpgradeChoice, EquipmentSlot, EquipmentInstance, EnchantmentDesign, DesignEffect, AttunementState, FloorState, EnemyState, RoomType } from './types';
import {
ELEMENTS,
GUARDIANS,
@@ -22,6 +22,14 @@ import {
EFFECT_RESEARCH_MAPPING,
BASE_UNLOCKED_EFFECTS,
ENCHANTING_UNLOCK_EFFECTS,
PUZZLE_ROOMS,
PUZZLE_ROOM_INTERVAL,
PUZZLE_ROOM_CHANCE,
SWARM_ROOM_CHANCE,
SPEED_ROOM_CHANCE,
SWARM_CONFIG,
SPEED_ROOM_CONFIG,
FLOOR_ARMOR_CONFIG,
} from './constants';
import { computeEffects, hasSpecial, SPECIAL_EFFECTS, type ComputedEffects } from './upgrade-effects';
import {
@@ -99,6 +107,174 @@ export function getFloorElement(floor: number): string {
return FLOOR_ELEM_CYCLE[(floor - 1) % 8];
}
// ─── Room Generation Functions ────────────────────────────────────────────────
// Generate room type for a floor
export function generateRoomType(floor: number): RoomType {
// Guardian floors are always guardian type
if (GUARDIANS[floor]) {
return 'guardian';
}
// Check for puzzle room (every PUZZLE_ROOM_INTERVAL floors)
if (floor % PUZZLE_ROOM_INTERVAL === 0 && Math.random() < PUZZLE_ROOM_CHANCE) {
return 'puzzle';
}
// Check for swarm room
if (Math.random() < SWARM_ROOM_CHANCE) {
return 'swarm';
}
// Check for speed room
if (Math.random() < SPEED_ROOM_CHANCE) {
return 'speed';
}
// Default to combat
return 'combat';
}
// Get armor for a non-guardian floor
export function getFloorArmor(floor: number): number {
if (GUARDIANS[floor]) {
return GUARDIANS[floor].armor || 0;
}
// Armor becomes more common on higher floors
if (floor < 10) return 0;
const armorChance = Math.min(FLOOR_ARMOR_CONFIG.maxArmorChance,
FLOOR_ARMOR_CONFIG.baseChance + (floor - 10) * FLOOR_ARMOR_CONFIG.chancePerFloor);
if (Math.random() > armorChance) return 0;
// Scale armor with floor
const armorRange = FLOOR_ARMOR_CONFIG.maxArmor - FLOOR_ARMOR_CONFIG.minArmor;
const floorProgress = Math.min(1, (floor - 10) / 90);
return FLOOR_ARMOR_CONFIG.minArmor + armorRange * floorProgress * Math.random();
}
// Get dodge chance for a speed room
export function getDodgeChance(floor: number): number {
return Math.min(
SPEED_ROOM_CONFIG.maxDodge,
SPEED_ROOM_CONFIG.baseDodgeChance + floor * SPEED_ROOM_CONFIG.dodgePerFloor
);
}
// Generate enemies for a swarm room
export function generateSwarmEnemies(floor: number): EnemyState[] {
const baseHP = getFloorMaxHP(floor);
const element = getFloorElement(floor);
const numEnemies = SWARM_CONFIG.minEnemies +
Math.floor(Math.random() * (SWARM_CONFIG.maxEnemies - SWARM_CONFIG.minEnemies + 1));
const enemies: EnemyState[] = [];
for (let i = 0; i < numEnemies; i++) {
enemies.push({
id: `enemy_${i}`,
hp: Math.floor(baseHP * SWARM_CONFIG.hpMultiplier),
maxHP: Math.floor(baseHP * SWARM_CONFIG.hpMultiplier),
armor: SWARM_CONFIG.armorBase + Math.floor(floor / 10) * SWARM_CONFIG.armorPerFloor,
dodgeChance: 0,
element,
});
}
return enemies;
}
// Generate initial floor state
export function generateFloorState(floor: number): FloorState {
const roomType = generateRoomType(floor);
const element = getFloorElement(floor);
const baseHP = getFloorMaxHP(floor);
const guardian = GUARDIANS[floor];
switch (roomType) {
case 'guardian':
return {
roomType: 'guardian',
enemies: [{
id: 'guardian',
hp: guardian.hp,
maxHP: guardian.hp,
armor: guardian.armor || 0,
dodgeChance: 0,
element: guardian.element,
}],
};
case 'swarm':
return {
roomType: 'swarm',
enemies: generateSwarmEnemies(floor),
};
case 'speed':
return {
roomType: 'speed',
enemies: [{
id: 'speed_enemy',
hp: baseHP,
maxHP: baseHP,
armor: getFloorArmor(floor),
dodgeChance: getDodgeChance(floor),
element,
}],
};
case 'puzzle': {
// Select a puzzle type based on player's attunements
const puzzleKeys = Object.keys(PUZZLE_ROOMS);
const selectedPuzzle = puzzleKeys[Math.floor(Math.random() * puzzleKeys.length)];
const puzzle = PUZZLE_ROOMS[selectedPuzzle];
return {
roomType: 'puzzle',
enemies: [],
puzzleProgress: 0,
puzzleRequired: 1,
puzzleId: selectedPuzzle,
puzzleAttunements: puzzle.attunements,
};
}
default: // combat
return {
roomType: 'combat',
enemies: [{
id: 'enemy',
hp: baseHP,
maxHP: baseHP,
armor: getFloorArmor(floor),
dodgeChance: 0,
element,
}],
};
}
}
// Get puzzle progress speed based on attunements
export function getPuzzleProgressSpeed(
puzzleId: string,
attunements: Record<string, AttunementState>
): number {
const puzzle = PUZZLE_ROOMS[puzzleId];
if (!puzzle) return 0.02; // Default slow progress
let speed = puzzle.baseProgressPerTick;
// Add bonus for each relevant attunement level
for (const attId of puzzle.attunements) {
const attState = attunements[attId];
if (attState?.active) {
speed += puzzle.attunementBonus * (attState.level || 1);
}
}
return speed;
}
// ─── Computed Stats Functions ─────────────────────────────────────────────────
// Helper to get effective skill level accounting for tiers
@@ -466,6 +642,9 @@ function makeInitial(overrides: Partial<GameState> = {}): GameState {
activeSpell: 'manaBolt',
currentAction: 'meditate',
castProgress: 0,
// Initialize room state
currentRoom: generateFloorState(startFloor),
spells: startSpells,
skills: overrides.skills || {},
@@ -783,10 +962,35 @@ export const useGameStore = create<GameStore>()(
}
// Combat - uses cast speed and spell casting
let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, castProgress } = state;
let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, castProgress, currentRoom } = state;
const floorElement = getFloorElement(currentFloor);
if (state.currentAction === 'climb') {
// Handle puzzle rooms separately
if (state.currentAction === 'climb' && currentRoom.roomType === 'puzzle') {
const progressSpeed = getPuzzleProgressSpeed(
currentRoom.puzzleId || '',
state.attunements
);
currentRoom = {
...currentRoom,
puzzleProgress: (currentRoom.puzzleProgress || 0) + progressSpeed * HOURS_PER_TICK,
};
// Check if puzzle is complete
if (currentRoom.puzzleProgress >= (currentRoom.puzzleRequired || 1)) {
const puzzle = PUZZLE_ROOMS[currentRoom.puzzleId || ''];
log = [`🧩 ${puzzle?.name || 'Puzzle'} solved! Proceeding to floor ${currentFloor + 1}.`, ...log.slice(0, 49)];
currentFloor = currentFloor + 1;
if (currentFloor > 100) currentFloor = 100;
currentRoom = generateFloorState(currentFloor);
floorMaxHP = getFloorMaxHP(currentFloor);
floorHP = currentRoom.enemies[0]?.hp || floorMaxHP;
maxFloorReached = Math.max(maxFloorReached, currentFloor);
castProgress = 0;
}
} else if (state.currentAction === 'climb') {
const spellId = state.activeSpell;
const spellDef = SPELLS_DEF[spellId];
@@ -798,8 +1002,11 @@ export const useGameStore = create<GameStore>()(
// Get spell cast speed (casts per hour, default 1)
const spellCastSpeed = spellDef.castSpeed || 1;
// Lightning is faster and harder to dodge
const lightningBonus = spellDef.elem === 'lightning' ? 0.3 : 0;
// Effective casts per tick = spellCastSpeed * totalAttackSpeed
const progressPerTick = HOURS_PER_TICK * spellCastSpeed * totalAttackSpeed;
const progressPerTick = HOURS_PER_TICK * spellCastSpeed * totalAttackSpeed * (1 + lightningBonus);
// Accumulate cast progress
castProgress = (castProgress || 0) + progressPerTick;
@@ -813,50 +1020,105 @@ export const useGameStore = create<GameStore>()(
totalManaGathered += spellDef.cost.amount;
// Calculate damage
let dmg = calcDamage(state, spellId, floorElement);
let baseDmg = calcDamage(state, spellId, floorElement);
// Apply upgrade damage multipliers and bonuses
dmg = dmg * effects.baseDamageMultiplier + effects.baseDamageBonus;
baseDmg = baseDmg * effects.baseDamageMultiplier + effects.baseDamageBonus;
// Executioner: +100% damage to enemies below 25% HP
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && floorHP / floorMaxHP < 0.25) {
dmg *= 2;
// Determine how many targets to hit
const isAoe = spellDef.isAoe || spellDef.aoeTargets;
const numTargets = Math.min(
isAoe ? (spellDef.aoeTargets || 3) : 1,
currentRoom.enemies.filter(e => e.hp > 0).length
);
// Get armor pierce from spell effects
const armorPierceEffect = spellDef.effects?.find(e => e.type === 'armor_pierce');
const armorPierce = armorPierceEffect?.value || 0;
// Hit targets
const aliveEnemies = currentRoom.enemies.filter(e => e.hp > 0);
const targetsToHit = aliveEnemies.slice(0, numTargets);
for (const enemy of targetsToHit) {
let dmg = baseDmg;
// AOE does less damage per target
if (isAoe && numTargets > 1) {
dmg *= (1 - 0.1 * (numTargets - 1)); // 10% less per additional target
}
// Check for freeze (prevents dodge)
const freezeEffect = spellDef.effects?.find(e => e.type === 'freeze');
const isFrozen = freezeEffect && freezeEffect.chance === 1;
// Check for dodge (speed rooms)
if (!isFrozen && enemy.dodgeChance > 0) {
// Lightning has lower chance to be dodged
const effectiveDodge = spellDef.elem === 'lightning'
? enemy.dodgeChance * 0.5
: enemy.dodgeChance;
if (Math.random() < effectiveDodge) {
log = [`💨 Enemy dodged the attack!`, ...log.slice(0, 49)];
continue;
}
}
// Apply armor reduction
const effectiveArmor = Math.max(0, enemy.armor - armorPierce);
dmg *= (1 - effectiveArmor);
// Executioner: +100% damage to enemies below 25% HP
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && enemy.hp / enemy.maxHP < 0.25) {
dmg *= 2;
}
// Berserker: +50% damage when below 50% mana
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 = [`✨ Spell Echo! Double damage!`, ...log.slice(0, 49)];
}
// Lifesteal effect
const lifestealEffect = spellDef.effects?.find(e => e.type === 'lifesteal');
if (lifestealEffect) {
const healAmount = dmg * lifestealEffect.value;
rawMana = Math.min(rawMana + healAmount, maxMana);
}
// Apply damage to enemy
enemy.hp = Math.max(0, enemy.hp - Math.floor(dmg));
}
// Berserker: +50% damage when below 50% mana
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 = [`✨ Spell Echo! Double damage!`, ...log.slice(0, 49)];
}
// Lifesteal effect
const lifestealEffect = spellDef.effects?.find(e => e.type === 'lifesteal');
if (lifestealEffect) {
const healAmount = dmg * lifestealEffect.value;
rawMana = Math.min(rawMana + healAmount, maxMana);
}
// Apply damage
floorHP = Math.max(0, floorHP - dmg);
// Update currentRoom with damaged enemies
currentRoom = { ...currentRoom, enemies: [...currentRoom.enemies] };
// Reduce cast progress by 1 (one cast completed)
castProgress -= 1;
if (floorHP <= 0) {
// Check if all enemies are dead
const allDead = currentRoom.enemies.every(e => e.hp <= 0);
if (allDead) {
// Floor cleared
const wasGuardian = GUARDIANS[currentFloor];
if (wasGuardian && !signedPacts.includes(currentFloor)) {
signedPacts = [...signedPacts, currentFloor];
log = [`⚔️ ${wasGuardian.name} defeated! Pact signed! (${wasGuardian.pact}x)`, ...log.slice(0, 49)];
} else if (!wasGuardian) {
if (currentFloor % 5 === 0) {
log = [`🏰 Floor ${currentFloor} cleared!`, ...log.slice(0, 49)];
const roomTypeName = currentRoom.roomType === 'swarm' ? 'Swarm'
: currentRoom.roomType === 'speed' ? 'Speed floor'
: currentRoom.roomType === 'puzzle' ? 'Puzzle'
: 'Floor';
if (currentFloor % 5 === 0 || currentRoom.roomType !== 'combat') {
log = [`🏰 ${roomTypeName} ${currentFloor} cleared!`, ...log.slice(0, 49)];
}
}
@@ -864,8 +1126,9 @@ export const useGameStore = create<GameStore>()(
if (currentFloor > 100) {
currentFloor = 100;
}
currentRoom = generateFloorState(currentFloor);
floorMaxHP = getFloorMaxHP(currentFloor);
floorHP = floorMaxHP;
floorHP = currentRoom.enemies[0]?.hp || floorMaxHP;
maxFloorReached = Math.max(maxFloorReached, currentFloor);
// Reset cast progress on floor change
@@ -917,6 +1180,7 @@ export const useGameStore = create<GameStore>()(
floorMaxHP,
maxFloorReached,
signedPacts,
currentRoom,
incursionStrength,
currentStudyTarget,
skills,
@@ -940,6 +1204,7 @@ export const useGameStore = create<GameStore>()(
floorMaxHP,
maxFloorReached,
signedPacts,
currentRoom,
incursionStrength,
currentStudyTarget,
skills,

View File

@@ -64,6 +64,7 @@ export interface GuardianDef {
pactCost: number; // Mana cost to perform pact ritual
pactTime: number; // Hours required for pact ritual
uniquePerk: string; // Description of unique perk
armor?: number; // Damage reduction (0-1, e.g., 0.2 = 20% reduction)
}
// Spell cost can be raw mana or elemental mana
@@ -84,12 +85,17 @@ export interface SpellDef {
castSpeed?: number; // Casts per hour (default 1, higher = faster)
desc?: string; // Optional spell description
effects?: SpellEffect[]; // Optional special effects
isAoe?: boolean; // AOE spell that hits multiple enemies
aoeTargets?: number; // Number of enemies hit by AOE
isWeaponEnchant?: boolean; // Can be used as weapon enchantment (magic swords)
}
export interface SpellEffect {
type: 'lifesteal' | 'burn' | 'freeze' | 'stun' | 'pierce' | 'multicast' | 'shield' | 'buff';
type: 'lifesteal' | 'burn' | 'freeze' | 'stun' | 'pierce' | 'multicast' | 'shield' | 'buff' | 'chain' | 'aoe' | 'armor_pierce';
value: number; // Effect potency
duration?: number; // Duration in hours for timed effects
targets?: number; // For AOE: number of targets
chance?: number; // For chance-based effects (e.g., stun chance)
}
export interface SpellState {
@@ -98,6 +104,28 @@ export interface SpellState {
studyProgress?: number; // Hours studied so far (for in-progress spells)
}
// ─── Room and Enemy Types ─────────────────────────────────────────────────────
export type RoomType = 'combat' | 'puzzle' | 'swarm' | 'speed' | 'guardian';
export interface EnemyState {
id: string;
hp: number;
maxHP: number;
armor: number; // Damage reduction (0-1)
dodgeChance: number; // For speed rooms (0-1)
element: string;
}
export interface FloorState {
roomType: RoomType;
enemies: EnemyState[]; // For swarm rooms, multiple enemies
puzzleProgress?: number; // For puzzle rooms (0-1)
puzzleRequired?: number; // Total progress needed
puzzleId?: string; // Which puzzle type
puzzleAttunements?: string[]; // Which attunements speed up this puzzle
}
export interface SkillDef {
name: string;
desc: string;
@@ -183,6 +211,11 @@ export interface EquipmentInstance {
totalCapacity: number; // Base capacity + bonuses
rarity: 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary' | 'mythic';
quality: number; // 0-100, affects capacity efficiency
weaponMana?: number; // Current mana stored in weapon (for weapon enchantments)
weaponManaMax?: number; // Max mana the weapon can store
weaponManaRegen?: number; // Mana regen per hour for weapon
weaponManaType?: string; // Type of mana the weapon stores
activeWeaponEnchant?: string; // Active weapon enchantment (for magic swords)
}
export interface AppliedEnchantment {
@@ -346,6 +379,9 @@ export interface GameState {
activeSpell: string;
currentAction: GameAction;
castProgress: number; // Progress towards next spell cast (0-1)
// Room system for special floors
currentRoom: FloorState; // Current room state (swarm, puzzle, speed, etc.)
// Spells
spells: Record<string, SpellState>;