From cd10918a3bcb5a36954b5ac4fc0a03b9e8c7e91e Mon Sep 17 00:00:00 2001 From: Z User Date: Mon, 30 Mar 2026 15:35:09 +0000 Subject: [PATCH] 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 --- src/lib/game/store.ts | 154 ++++++++++++++++++++++++++++++++---------- 1 file changed, 120 insertions(+), 34 deletions(-) diff --git a/src/lib/game/store.ts b/src/lib/game/store.ts index ce95476..9fc5af1 100755 --- a/src/lib/game/store.ts +++ b/src/lib/game/store.ts @@ -962,10 +962,35 @@ export const useGameStore = create()( } // 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]; @@ -977,8 +1002,11 @@ export const useGameStore = create()( // 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; @@ -992,50 +1020,105 @@ export const useGameStore = create()( 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)]; } } @@ -1043,8 +1126,9 @@ export const useGameStore = create()( 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 @@ -1096,6 +1180,7 @@ export const useGameStore = create()( floorMaxHP, maxFloorReached, signedPacts, + currentRoom, incursionStrength, currentStudyTarget, skills, @@ -1119,6 +1204,7 @@ export const useGameStore = create()( floorMaxHP, maxFloorReached, signedPacts, + currentRoom, incursionStrength, currentStudyTarget, skills,