Phase 4: Combat special effects
This commit is contained in:
@@ -44,4 +44,34 @@ export const SPECIAL_EFFECTS: Record<string, EnchantmentEffectDef> = {
|
|||||||
allowedEquipmentCategories: CASTER_AND_HANDS,
|
allowedEquipmentCategories: CASTER_AND_HANDS,
|
||||||
effect: { type: 'special', specialId: 'overpower' }
|
effect: { type: 'special', specialId: 'overpower' }
|
||||||
},
|
},
|
||||||
|
first_strike: {
|
||||||
|
id: 'first_strike',
|
||||||
|
name: 'First Strike',
|
||||||
|
description: '+15% damage on first attack each floor',
|
||||||
|
category: 'special',
|
||||||
|
baseCapacityCost: 45,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: CASTER_AND_HANDS,
|
||||||
|
effect: { type: 'special', specialId: 'firstStrike' }
|
||||||
|
},
|
||||||
|
combo_master: {
|
||||||
|
id: 'combo_master',
|
||||||
|
name: 'Combo Master',
|
||||||
|
description: 'Every 5th attack deals 3x damage',
|
||||||
|
category: 'special',
|
||||||
|
baseCapacityCost: 65,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: CASTER_AND_HANDS,
|
||||||
|
effect: { type: 'special', specialId: 'comboMaster' }
|
||||||
|
},
|
||||||
|
adrenaline_rush: {
|
||||||
|
id: 'adrenaline_rush',
|
||||||
|
name: 'Adrenaline Rush',
|
||||||
|
description: 'Defeating enemy restores 5% mana',
|
||||||
|
category: 'special',
|
||||||
|
baseCapacityCost: 50,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: CASTER_AND_HANDS,
|
||||||
|
effect: { type: 'special', specialId: 'adrenalineRush' }
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+48
-3
@@ -684,6 +684,10 @@ function makeInitial(overrides: Partial<GameState> = {}): GameState {
|
|||||||
totalDamageDealt: 0,
|
totalDamageDealt: 0,
|
||||||
totalCraftsCompleted: 0,
|
totalCraftsCompleted: 0,
|
||||||
|
|
||||||
|
// Combat special effect tracking
|
||||||
|
comboHitCount: 0, // Hit counter for COMBO_MASTER (every 5th attack)
|
||||||
|
floorHitCount: 0, // Hit counter for current floor (for FIRST_STRIKE)
|
||||||
|
|
||||||
// New equipment system
|
// New equipment system
|
||||||
equippedInstances: startingEquipment.equippedInstances,
|
equippedInstances: startingEquipment.equippedInstances,
|
||||||
equipmentInstances: startingEquipment.equipmentInstances,
|
equipmentInstances: startingEquipment.equipmentInstances,
|
||||||
@@ -1013,7 +1017,9 @@ export const useGameStore = create<GameStore>()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Combat - uses cast speed and spell casting
|
// Combat - uses cast speed and spell casting
|
||||||
let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, castProgress, currentRoom } = state;
|
let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, castProgress, currentRoom, comboHitCount, floorHitCount } = state;
|
||||||
|
comboHitCount = comboHitCount || 0;
|
||||||
|
floorHitCount = floorHitCount || 0;
|
||||||
const floorElement = getFloorElement(currentFloor);
|
const floorElement = getFloorElement(currentFloor);
|
||||||
|
|
||||||
// Handle puzzle rooms separately
|
// Handle puzzle rooms separately
|
||||||
@@ -1120,14 +1126,32 @@ export const useGameStore = create<GameStore>()(
|
|||||||
const effectiveArmor = Math.max(0, enemy.armor - armorPierce);
|
const effectiveArmor = Math.max(0, enemy.armor - armorPierce);
|
||||||
dmg *= (1 - effectiveArmor);
|
dmg *= (1 - effectiveArmor);
|
||||||
|
|
||||||
|
// Increment hit counters
|
||||||
|
comboHitCount += 1;
|
||||||
|
floorHitCount += 1;
|
||||||
|
|
||||||
|
// First Strike: +15% damage on first attack each floor
|
||||||
|
if (hasSpecial(effects, SPECIAL_EFFECTS.FIRST_STRIKE) && floorHitCount === 1) {
|
||||||
|
dmg *= 1.15;
|
||||||
|
log = [`⚡ First Strike! +15% damage!`, ...log.slice(0, 49)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combo Master: Every 5th attack deals 3x damage
|
||||||
|
if (hasSpecial(effects, SPECIAL_EFFECTS.COMBO_MASTER) && comboHitCount % 5 === 0) {
|
||||||
|
dmg *= 3;
|
||||||
|
log = [`🌀 Combo Master! Triple damage!`, ...log.slice(0, 49)];
|
||||||
|
}
|
||||||
|
|
||||||
// Executioner: +100% damage to enemies below 25% HP
|
// Executioner: +100% damage to enemies below 25% HP
|
||||||
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && enemy.hp / enemy.maxHP < 0.25) {
|
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && enemy.hp / enemy.maxHP < 0.25) {
|
||||||
dmg *= 2;
|
dmg *= 2;
|
||||||
|
log = [`💀 Executioner! Double damage!`, ...log.slice(0, 49)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Berserker: +50% damage when below 50% mana
|
// Berserker: +50% damage when below 50% mana
|
||||||
if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
|
if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
|
||||||
dmg *= 1.5;
|
dmg *= 1.5;
|
||||||
|
log = [`🔥 Berserker! +50% damage!`, ...log.slice(0, 49)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spell echo - chance to cast again
|
// Spell echo - chance to cast again
|
||||||
@@ -1153,6 +1177,14 @@ export const useGameStore = create<GameStore>()(
|
|||||||
if (allDead) {
|
if (allDead) {
|
||||||
// Floor cleared
|
// Floor cleared
|
||||||
const wasGuardian = GUARDIANS[currentFloor];
|
const wasGuardian = GUARDIANS[currentFloor];
|
||||||
|
|
||||||
|
// Adrenaline Rush: Defeating enemy restores 5% mana
|
||||||
|
if (hasSpecial(effects, SPECIAL_EFFECTS.ADRENALINE_RUSH)) {
|
||||||
|
const manaRestore = Math.floor(maxMana * 0.05);
|
||||||
|
rawMana = Math.min(rawMana + manaRestore, maxMana);
|
||||||
|
log = [`💚 Adrenaline Rush! Restored ${manaRestore} mana!`, ...log.slice(0, 49)];
|
||||||
|
}
|
||||||
|
|
||||||
if (wasGuardian && !signedPacts.includes(currentFloor)) {
|
if (wasGuardian && !signedPacts.includes(currentFloor)) {
|
||||||
signedPacts = [...signedPacts, currentFloor];
|
signedPacts = [...signedPacts, currentFloor];
|
||||||
log = [`⚔️ ${wasGuardian.name} defeated! Pact signed! (${wasGuardian.pact}x)`, ...log.slice(0, 49)];
|
log = [`⚔️ ${wasGuardian.name} defeated! Pact signed! (${wasGuardian.pact}x)`, ...log.slice(0, 49)];
|
||||||
@@ -1175,8 +1207,9 @@ export const useGameStore = create<GameStore>()(
|
|||||||
floorHP = currentRoom.enemies[0]?.hp || floorMaxHP;
|
floorHP = currentRoom.enemies[0]?.hp || floorMaxHP;
|
||||||
maxFloorReached = Math.max(maxFloorReached, currentFloor);
|
maxFloorReached = Math.max(maxFloorReached, currentFloor);
|
||||||
|
|
||||||
// Reset cast progress on floor change
|
// Reset cast progress and floor hit counter on floor change
|
||||||
castProgress = 0;
|
castProgress = 0;
|
||||||
|
floorHitCount = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1445,6 +1478,8 @@ export const useGameStore = create<GameStore>()(
|
|||||||
castProgress,
|
castProgress,
|
||||||
golemancy,
|
golemancy,
|
||||||
flowSurgeEndTime,
|
flowSurgeEndTime,
|
||||||
|
comboHitCount,
|
||||||
|
floorHitCount,
|
||||||
...craftingUpdates,
|
...craftingUpdates,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -2449,7 +2484,7 @@ export const useGameStore = create<GameStore>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'mana-loop-storage',
|
name: 'mana-loop-storage',
|
||||||
version: 2,
|
version: 3,
|
||||||
migrate: (persistedState: unknown, version: number) => {
|
migrate: (persistedState: unknown, version: number) => {
|
||||||
const state = persistedState as Record<string, unknown>;
|
const state = persistedState as Record<string, unknown>;
|
||||||
// Migration from version 0/1 to version 2 - add missing fields
|
// Migration from version 0/1 to version 2 - add missing fields
|
||||||
@@ -2462,6 +2497,14 @@ export const useGameStore = create<GameStore>()(
|
|||||||
parallelStudyTarget: state.parallelStudyTarget ?? null,
|
parallelStudyTarget: state.parallelStudyTarget ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// Migration to version 3 - add combo hit counters
|
||||||
|
if (version < 3) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
comboHitCount: state.comboHitCount ?? 0,
|
||||||
|
floorHitCount: state.floorHitCount ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
return state;
|
return state;
|
||||||
},
|
},
|
||||||
partialize: (state) => ({
|
partialize: (state) => ({
|
||||||
@@ -2493,6 +2536,8 @@ export const useGameStore = create<GameStore>()(
|
|||||||
totalSpellsCast: state.totalSpellsCast,
|
totalSpellsCast: state.totalSpellsCast,
|
||||||
totalDamageDealt: state.totalDamageDealt,
|
totalDamageDealt: state.totalDamageDealt,
|
||||||
totalCraftsCompleted: state.totalCraftsCompleted,
|
totalCraftsCompleted: state.totalCraftsCompleted,
|
||||||
|
comboHitCount: state.comboHitCount,
|
||||||
|
floorHitCount: state.floorHitCount,
|
||||||
insight: state.insight,
|
insight: state.insight,
|
||||||
totalInsight: state.totalInsight,
|
totalInsight: state.totalInsight,
|
||||||
prestigeUpgrades: state.prestigeUpgrades,
|
prestigeUpgrades: state.prestigeUpgrades,
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ export const createCombatSlice = (
|
|||||||
let pendingPactOffer = state.pendingPactOffer;
|
let pendingPactOffer = state.pendingPactOffer;
|
||||||
const log = [...state.log];
|
const log = [...state.log];
|
||||||
const skills = state.skills;
|
const skills = state.skills;
|
||||||
|
let comboHitCount = state.comboHitCount || 0;
|
||||||
|
let floorHitCount = state.floorHitCount || 0;
|
||||||
|
|
||||||
const floorElement = getFloorElement(currentFloor);
|
const floorElement = getFloorElement(currentFloor);
|
||||||
|
|
||||||
@@ -98,15 +100,33 @@ export const createCombatSlice = (
|
|||||||
let dmg = calcDamage(state, spellId, floorElement);
|
let dmg = calcDamage(state, spellId, floorElement);
|
||||||
dmg = dmg * effects.baseDamageMultiplier + effects.baseDamageBonus;
|
dmg = dmg * effects.baseDamageMultiplier + effects.baseDamageBonus;
|
||||||
|
|
||||||
|
// Increment hit counters
|
||||||
|
comboHitCount += 1;
|
||||||
|
floorHitCount += 1;
|
||||||
|
|
||||||
|
// First Strike: +15% damage on first attack each floor
|
||||||
|
if (hasSpecial(effects, SPECIAL_EFFECTS.FIRST_STRIKE) && floorHitCount === 1) {
|
||||||
|
dmg *= 1.15;
|
||||||
|
log.unshift('⚡ First Strike! +15% damage!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combo Master: Every 5th attack deals 3x damage
|
||||||
|
if (hasSpecial(effects, SPECIAL_EFFECTS.COMBO_MASTER) && comboHitCount % 5 === 0) {
|
||||||
|
dmg *= 3;
|
||||||
|
log.unshift('🌀 Combo Master! Triple damage!');
|
||||||
|
}
|
||||||
|
|
||||||
// Executioner: +100% damage to enemies below 25% HP
|
// Executioner: +100% damage to enemies below 25% HP
|
||||||
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && floorHP / floorMaxHP < 0.25) {
|
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && floorHP / floorMaxHP < 0.25) {
|
||||||
dmg *= 2;
|
dmg *= 2;
|
||||||
|
log.unshift('💀 Executioner! Double damage!');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Berserker: +50% damage when below 50% mana
|
// Berserker: +50% damage when below 50% mana
|
||||||
const maxMana = 100; // Would need proper max mana calculation
|
const maxMana = 100; // Would need proper max mana calculation
|
||||||
if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
|
if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
|
||||||
dmg *= 1.5;
|
dmg *= 1.5;
|
||||||
|
log.unshift('🔥 Berserker! +50% damage!');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spell echo - chance to cast again
|
// Spell echo - chance to cast again
|
||||||
@@ -122,6 +142,14 @@ export const createCombatSlice = (
|
|||||||
|
|
||||||
if (floorHP <= 0) {
|
if (floorHP <= 0) {
|
||||||
const wasGuardian = GUARDIANS[currentFloor];
|
const wasGuardian = GUARDIANS[currentFloor];
|
||||||
|
|
||||||
|
// Adrenaline Rush: Defeating enemy restores 5% mana
|
||||||
|
if (hasSpecial(effects, SPECIAL_EFFECTS.ADRENALINE_RUSH)) {
|
||||||
|
const manaRestore = Math.floor(maxMana * 0.05);
|
||||||
|
rawMana = Math.min(rawMana + manaRestore, maxMana);
|
||||||
|
log.unshift(`💚 Adrenaline Rush! Restored ${manaRestore} mana!`);
|
||||||
|
}
|
||||||
|
|
||||||
if (wasGuardian && !signedPacts.includes(currentFloor)) {
|
if (wasGuardian && !signedPacts.includes(currentFloor)) {
|
||||||
pendingPactOffer = currentFloor;
|
pendingPactOffer = currentFloor;
|
||||||
log.unshift(`⚔️ ${wasGuardian.name} defeated! They offer a pact...`);
|
log.unshift(`⚔️ ${wasGuardian.name} defeated! They offer a pact...`);
|
||||||
@@ -137,6 +165,7 @@ export const createCombatSlice = (
|
|||||||
floorHP = floorMaxHP;
|
floorHP = floorMaxHP;
|
||||||
maxFloorReached = Math.max(maxFloorReached, currentFloor);
|
maxFloorReached = Math.max(maxFloorReached, currentFloor);
|
||||||
castProgress = 0;
|
castProgress = 0;
|
||||||
|
floorHitCount = 0; // Reset floor hit counter for new floor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +181,8 @@ export const createCombatSlice = (
|
|||||||
pendingPactOffer,
|
pendingPactOffer,
|
||||||
castProgress,
|
castProgress,
|
||||||
log,
|
log,
|
||||||
|
comboHitCount,
|
||||||
|
floorHitCount,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -185,6 +185,10 @@ export interface GameState {
|
|||||||
totalDamageDealt: number;
|
totalDamageDealt: number;
|
||||||
totalCraftsCompleted: number;
|
totalCraftsCompleted: number;
|
||||||
|
|
||||||
|
// Combat special effect tracking
|
||||||
|
comboHitCount: number; // Hit counter for COMBO_MASTER (every 5th attack)
|
||||||
|
floorHitCount: number; // Hit counter for current floor (for FIRST_STRIKE)
|
||||||
|
|
||||||
// Prestige
|
// Prestige
|
||||||
insight: number;
|
insight: number;
|
||||||
totalInsight: number;
|
totalInsight: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user