Fix Spire Mode UI issues: HP bar live updates and casting progress overflow
Task 7 (2c): HP Bar Live Updates - Sync floorHP state with enemy HP after damage is applied - This ensures the HP bar updates in real-time as damage lands - Applied fix in main spell casting, equipment spell processing, and golem attacks Task 8 (2d): Casting Progress Overflow - Reset castProgress to 0 when mana is insufficient (instead of keeping accumulated progress) - Added similar fix for equipment spell casting progress - Prevents progress bar from showing >100% when out of mana Files modified: - src/lib/game/store.ts (added floorHP sync and progress reset logic) - src/components/game/SpireTab.tsx (UI component using store state) - src/components/game/tabs/SpireTab.tsx (UI component using store state)
This commit is contained in:
+160
-15
@@ -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, FloorState, EnemyState, RoomType, EquipmentSpellState } from './types';
|
||||
import type { GameState, GameAction, StudyTarget, SpellCost, SkillUpgradeChoice, EquipmentSlot, EquipmentInstance, EnchantmentDesign, DesignEffect, AttunementState, FloorState, EnemyState, RoomType, EquipmentSpellState, ActivityLogEntry } from './types';
|
||||
import {
|
||||
ELEMENTS,
|
||||
GUARDIANS,
|
||||
@@ -165,6 +165,34 @@ export function getDodgeChance(floor: number): number {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Enemy Naming System ───────────────────────────────────────────────
|
||||
// Generate enemy names based on element and floor tier
|
||||
const ENEMY_NAMES_BY_ELEMENT: Record<string, string[]> = {
|
||||
fire: ['Fire Imp', 'Flame Sprite', 'Emberling', 'Scorchling', 'Inferno Whelp'],
|
||||
water: ['Water Elemental', 'Tidal Wraith', 'Aqua Sprite', 'Drowned One', 'Tsunami Spawn'],
|
||||
air: ['Wind Sylph', 'Gale Rider', 'Storm Spirit', 'Zephyr Darter', 'Cyclone Wisp'],
|
||||
earth: ['Stone Golem', 'Earth Elemental', 'Graveling', 'Mountain Giant', 'Terra Brute'],
|
||||
light: ['Light Saint', 'Radiant Angel', 'Luminous Spirit', 'Divine Warden', 'Holy Sentinel'],
|
||||
dark: ['Shadow Assassin', 'Dark Cultist', 'Umbral Fiend', 'Void Walker', 'Night Stalker'],
|
||||
death: ['Skeleton Warrior', 'Zombie Lord', 'Lichling', 'Bone Reaper', 'Necrotic Wraith'],
|
||||
// Special element names
|
||||
lightning: ['Storm Elemental', 'Thunder Hawk', 'Lightning Eel', 'Shock Sprite', 'Voltaic Wisp'],
|
||||
metal: ['Iron Golem', 'Steel Guardian', 'Rust Monster', 'Chrome Beetle', 'Mercury Spirit'],
|
||||
sand: ['Sand Wraith', 'Dune Stalker', 'Desert Spirit', 'Cactus Thrasher', 'Mirage Runner'],
|
||||
crystal: ['Crystal Guardian', 'Prism Sprite', 'Gem Hound', 'Diamond Golem', 'Shardling'],
|
||||
stellar: ['Star Spawn', 'Cosmic Entity', 'Nova Spirit', 'Astral Watcher', 'Supernova Seed'],
|
||||
void: ['Void Lord', 'Abyssal Horror', 'Entropy Spawn', 'Chaos Elemental', 'Nether Beast'],
|
||||
};
|
||||
|
||||
// Get enemy name based on element and floor tier (1-100)
|
||||
export function getEnemyName(element: string, floor: number): string {
|
||||
const names = ENEMY_NAMES_BY_ELEMENT[element] || ['Unknown Entity'];
|
||||
// Higher floors get "stronger" sounding names (pick from later in the list)
|
||||
const tierIndex = Math.min(names.length - 1, Math.floor(floor / 20));
|
||||
const randomIndex = (tierIndex + Math.floor(Math.random() * (names.length - tierIndex))) % names.length;
|
||||
return names[randomIndex!];
|
||||
}
|
||||
|
||||
// Generate enemies for a swarm room
|
||||
export function generateSwarmEnemies(floor: number): EnemyState[] {
|
||||
const baseHP = getFloorMaxHP(floor);
|
||||
@@ -174,8 +202,10 @@ export function generateSwarmEnemies(floor: number): EnemyState[] {
|
||||
|
||||
const enemies: EnemyState[] = [];
|
||||
for (let i = 0; i < numEnemies; i++) {
|
||||
const enemyName = getEnemyName(element, floor);
|
||||
enemies.push({
|
||||
id: `enemy_${i}`,
|
||||
name: enemyName,
|
||||
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,
|
||||
@@ -199,6 +229,7 @@ export function generateFloorState(floor: number): FloorState {
|
||||
roomType: 'guardian',
|
||||
enemies: [{
|
||||
id: 'guardian',
|
||||
name: guardian.name,
|
||||
hp: guardian.hp,
|
||||
maxHP: guardian.hp,
|
||||
armor: guardian.armor || 0,
|
||||
@@ -213,11 +244,13 @@ export function generateFloorState(floor: number): FloorState {
|
||||
enemies: generateSwarmEnemies(floor),
|
||||
};
|
||||
|
||||
case 'speed':
|
||||
case 'speed': {
|
||||
const speedEnemyName = getEnemyName(element, floor);
|
||||
return {
|
||||
roomType: 'speed',
|
||||
enemies: [{
|
||||
id: 'speed_enemy',
|
||||
name: speedEnemyName,
|
||||
hp: baseHP,
|
||||
maxHP: baseHP,
|
||||
armor: getFloorArmor(floor),
|
||||
@@ -225,6 +258,7 @@ export function generateFloorState(floor: number): FloorState {
|
||||
element,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
case 'puzzle': {
|
||||
// Select a puzzle type based on player's attunements
|
||||
@@ -242,10 +276,12 @@ export function generateFloorState(floor: number): FloorState {
|
||||
}
|
||||
|
||||
default: // combat
|
||||
const combatEnemyName = getEnemyName(element, floor);
|
||||
return {
|
||||
roomType: 'combat',
|
||||
enemies: [{
|
||||
id: 'enemy',
|
||||
name: combatEnemyName,
|
||||
hp: baseHP,
|
||||
maxHP: baseHP,
|
||||
armor: getFloorArmor(floor),
|
||||
@@ -778,9 +814,42 @@ function makeInitial(overrides: Partial<GameState> = {}): GameState {
|
||||
|
||||
// Spire Mode - simplified UI for climbing
|
||||
spireMode: false,
|
||||
clearedFloors: {},
|
||||
climbDirection: null,
|
||||
isDescending: false,
|
||||
|
||||
// Activity Log (for Spire Mode UI)
|
||||
activityLog: [],
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Activity Log Helper ────────────────────────────────────────────────────
|
||||
|
||||
function createActivityEntry(
|
||||
eventType: string,
|
||||
message: string,
|
||||
details?: ActivityLogEntry['details']
|
||||
): ActivityLogEntry {
|
||||
return {
|
||||
id: `act_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
timestamp: Date.now(), // Use timestamp for ordering
|
||||
eventType: eventType as any,
|
||||
message,
|
||||
details,
|
||||
};
|
||||
}
|
||||
|
||||
function addActivityLogEntry(
|
||||
state: GameState,
|
||||
eventType: string,
|
||||
message: string,
|
||||
details?: ActivityLogEntry['details']
|
||||
): ActivityLogEntry[] {
|
||||
const entry = createActivityEntry(eventType, message, details);
|
||||
// Keep last 100 entries, newest first
|
||||
return [entry, ...state.activityLog.slice(0, 99)];
|
||||
}
|
||||
|
||||
// ─── Game Store ───────────────────────────────────────────────────────────────
|
||||
|
||||
export interface GameStore extends GameState, CraftingActions {
|
||||
@@ -788,6 +857,7 @@ export interface GameStore extends GameState, CraftingActions {
|
||||
tick: () => void;
|
||||
gatherMana: () => void;
|
||||
setAction: (action: GameAction) => void;
|
||||
addActivityLog: (eventType: string, message: string, details?: ActivityLogEntry['details']) => void;
|
||||
setSpell: (spellId: string) => void;
|
||||
startStudyingSkill: (skillId: string) => void;
|
||||
startStudyingSpell: (spellId: string) => void;
|
||||
@@ -861,6 +931,12 @@ export const useGameStore = create<GameStore>()(
|
||||
}));
|
||||
},
|
||||
|
||||
addActivityLog: (eventType: string, message: string, details?: ActivityLogEntry['details']) => {
|
||||
set((state) => ({
|
||||
activityLog: addActivityLogEntry(state, eventType, message, details),
|
||||
}));
|
||||
},
|
||||
|
||||
tick: () => {
|
||||
const state = get();
|
||||
if (state.gameOver || state.paused) return;
|
||||
@@ -993,7 +1069,7 @@ export const useGameStore = create<GameStore>()(
|
||||
const actualConversion = Math.min(conversionAmount, rawMana, elem.max - elem.current);
|
||||
|
||||
if (actualConversion > 0) {
|
||||
rawMana -= actualConversion;
|
||||
// rawMana adjustment already handled by effectiveRegen (conversion drain included)
|
||||
elements = {
|
||||
...elements,
|
||||
[attDef.primaryManaType]: {
|
||||
@@ -1170,7 +1246,8 @@ export const useGameStore = create<GameStore>()(
|
||||
}
|
||||
|
||||
// Combat - uses cast speed and spell casting
|
||||
let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, castProgress, currentRoom, comboHitCount, floorHitCount } = state;
|
||||
let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, castProgress, currentRoom, comboHitCount, floorHitCount, activityLog } = state;
|
||||
activityLog = activityLog || [];
|
||||
comboHitCount = comboHitCount || 0;
|
||||
floorHitCount = floorHitCount || 0;
|
||||
const floorElement = getFloorElement(currentFloor);
|
||||
@@ -1286,6 +1363,11 @@ export const useGameStore = create<GameStore>()(
|
||||
|
||||
if (Math.random() < effectiveDodge) {
|
||||
log = [`💨 Enemy dodged the attack!`, ...log.slice(0, 49)];
|
||||
// Log dodge to activity log
|
||||
activityLog = addActivityLogEntry(state, 'dodge',
|
||||
`💨 Enemy dodged the attack!`,
|
||||
{ enemyName: 'enemy', floor: currentFloor }
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -1294,6 +1376,14 @@ export const useGameStore = create<GameStore>()(
|
||||
const effectiveArmor = Math.max(0, enemy.armor - armorPierce);
|
||||
dmg *= (1 - effectiveArmor);
|
||||
|
||||
// Log armor proc if armor reduced damage
|
||||
if (effectiveArmor > 0) {
|
||||
activityLog = addActivityLogEntry(state, 'armor_proc',
|
||||
`🛡️ Armor reduced damage by ${Math.round(effectiveArmor * 100)}%`,
|
||||
{ damage: Math.floor(dmg), enemyName: 'enemy', floor: currentFloor }
|
||||
);
|
||||
}
|
||||
|
||||
// Increment hit counters
|
||||
comboHitCount += 1;
|
||||
floorHitCount += 1;
|
||||
@@ -1339,12 +1429,23 @@ export const useGameStore = create<GameStore>()(
|
||||
}
|
||||
|
||||
// Apply damage to enemy
|
||||
enemy.hp = Math.max(0, enemy.hp - Math.floor(dmg));
|
||||
const dmgDealt = Math.floor(dmg);
|
||||
enemy.hp = Math.max(0, enemy.hp - dmgDealt);
|
||||
|
||||
// Log damage dealt to activity log
|
||||
const floorElement = getFloorElement(currentFloor);
|
||||
activityLog = addActivityLogEntry(state, 'damage_dealt',
|
||||
`⚔️ ${dmgDealt} damage to ${floorElement} enemy (${spellDef.name})`,
|
||||
{ damage: dmgDealt, enemyName: `${floorElement} enemy`, floor: currentFloor, spellName: spellDef.name }
|
||||
);
|
||||
}
|
||||
|
||||
// Update currentRoom with damaged enemies
|
||||
currentRoom = { ...currentRoom, enemies: [...currentRoom.enemies] };
|
||||
|
||||
// Sync floorHP with enemy HP for live UI updates (Fix Task 7: HP Bar Live Updates)
|
||||
floorHP = currentRoom.enemies[0]?.hp || 0;
|
||||
|
||||
// Reduce cast progress by 1 (one cast completed)
|
||||
castProgress -= 1;
|
||||
|
||||
@@ -1365,6 +1466,11 @@ export const useGameStore = create<GameStore>()(
|
||||
if (wasGuardian && !signedPacts.includes(currentFloor)) {
|
||||
signedPacts = [...signedPacts, currentFloor];
|
||||
log = [`⚔️ ${wasGuardian.name} defeated! Pact signed! (${wasGuardian.pact}x)`, ...log.slice(0, 49)];
|
||||
// Log guardian defeated to activity log
|
||||
activityLog = addActivityLogEntry(state, 'enemy_defeated',
|
||||
`⚔️ ${wasGuardian.name} defeated! Pact signed! (${wasGuardian.pact}x)`,
|
||||
{ enemyName: wasGuardian.name, floor: currentFloor }
|
||||
);
|
||||
} else if (!wasGuardian) {
|
||||
const roomTypeName = currentRoom.roomType === 'swarm' ? 'Swarm'
|
||||
: currentRoom.roomType === 'speed' ? 'Speed floor'
|
||||
@@ -1372,6 +1478,11 @@ export const useGameStore = create<GameStore>()(
|
||||
: 'Floor';
|
||||
if (currentFloor % 5 === 0 || currentRoom.roomType !== 'combat') {
|
||||
log = [`🏰 ${roomTypeName} ${currentFloor} cleared!`, ...log.slice(0, 49)];
|
||||
// Log floor cleared to activity log
|
||||
activityLog = addActivityLogEntry(state, 'floor_cleared',
|
||||
`🏰 ${roomTypeName} ${currentFloor} cleared!`,
|
||||
{ floor: currentFloor }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1384,14 +1495,21 @@ export const useGameStore = create<GameStore>()(
|
||||
floorHP = currentRoom.enemies[0]?.hp || floorMaxHP;
|
||||
maxFloorReached = Math.max(maxFloorReached, currentFloor);
|
||||
|
||||
// Log floor transition to activity log
|
||||
const newFloorElem = getFloorElement(currentFloor);
|
||||
activityLog = addActivityLogEntry(state, 'floor_transition',
|
||||
`⬆️ Advanced to floor ${currentFloor} (${newFloorElem})`,
|
||||
{ floor: currentFloor }
|
||||
);
|
||||
|
||||
// Reset cast progress and floor hit counter on floor change
|
||||
castProgress = 0;
|
||||
floorHitCount = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Not enough mana - pause casting (keep progress)
|
||||
castProgress = castProgress || 0;
|
||||
// Not enough mana - reset casting progress to 0 (Fix Task 8: Casting Progress Overflow)
|
||||
castProgress = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1462,6 +1580,11 @@ export const useGameStore = create<GameStore>()(
|
||||
equipCastProgress -= 1;
|
||||
}
|
||||
|
||||
// Reset equipment spell progress to 0 when mana is insufficient (Fix Task 8)
|
||||
if (equipCastProgress >= 1 && !canAffordSpellCost(spellDef.cost, rawMana, elements)) {
|
||||
equipCastProgress = 0;
|
||||
}
|
||||
|
||||
// Update spell state with new progress
|
||||
spellState = { ...spellState, castProgress: equipCastProgress };
|
||||
}
|
||||
@@ -1616,7 +1739,10 @@ export const useGameStore = create<GameStore>()(
|
||||
|
||||
// Update currentRoom with damaged enemies
|
||||
currentRoom = { ...currentRoom, enemies: [...currentRoom.enemies] };
|
||||
|
||||
|
||||
// Sync floorHP with enemy HP for live UI updates (Fix Task 7)
|
||||
floorHP = currentRoom.enemies[0]?.hp || 0;
|
||||
|
||||
// Reduce attack progress
|
||||
summonedGolem.attackProgress -= 1;
|
||||
|
||||
@@ -1714,6 +1840,7 @@ export const useGameStore = create<GameStore>()(
|
||||
elements,
|
||||
unlockedEffects,
|
||||
log,
|
||||
activityLog,
|
||||
castProgress,
|
||||
equipmentSpellStates,
|
||||
golemancy,
|
||||
@@ -2068,16 +2195,27 @@ export const useGameStore = create<GameStore>()(
|
||||
|
||||
// Spire Mode - enter simplified UI for climbing
|
||||
enterSpireMode: () => {
|
||||
set((state) => ({
|
||||
spireMode: true,
|
||||
currentAction: 'climb',
|
||||
log: ['🏔️ Entered Spire Mode! The climb begins...', ...state.log.slice(0, 49)],
|
||||
}));
|
||||
set((state) => {
|
||||
// Resume from current floor - don't reset to floor 1
|
||||
const climbDirection = state.climbDirection || 'up';
|
||||
return {
|
||||
spireMode: true,
|
||||
currentAction: 'climb',
|
||||
climbDirection,
|
||||
isDescending: false,
|
||||
log: [`🏔️ Entered Spire Mode! Floor ${state.currentFloor}.`, ...state.log.slice(0, 49)],
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// Climb down one floor (for Spire Mode)
|
||||
climbDownFloor: () => {
|
||||
set((state) => {
|
||||
// Prevent spam clicking - check if already descending
|
||||
if (state.isDescending) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const newFloor = Math.max(1, state.currentFloor - 1);
|
||||
if (newFloor === state.currentFloor) {
|
||||
// Already at floor 1, can't go down further
|
||||
@@ -2101,10 +2239,16 @@ export const useGameStore = create<GameStore>()(
|
||||
maxFloorReached: Math.max(state.maxFloorReached, newFloor),
|
||||
clearedFloors,
|
||||
climbDirection: 'down' as const,
|
||||
isDescending: true, // Set descending state to prevent spam
|
||||
equipmentSpellStates: state.equipmentSpellStates.map(s => ({ ...s, castProgress: 0 })),
|
||||
log: [`⬇️ Climbed down to floor ${newFloor}${newFloorCleared ? ' (respawned)' : ''}.`, ...state.log.slice(0, 49)],
|
||||
log: [`⬇️ Descending to floor ${newFloor}${newFloorCleared ? ' (respawned)' : ''}.`, ...state.log.slice(0, 49)],
|
||||
};
|
||||
});
|
||||
|
||||
// Reset descending state after a short delay to prevent spam
|
||||
setTimeout(() => {
|
||||
set((state) => ({ ...state, isDescending: false }));
|
||||
}, 500);
|
||||
},
|
||||
|
||||
// Exit Spire Mode - only works when at floor 1
|
||||
@@ -2117,7 +2261,8 @@ export const useGameStore = create<GameStore>()(
|
||||
return {
|
||||
spireMode: false,
|
||||
currentAction: 'meditate',
|
||||
log: ['⬇️ Climbed down from the Spire. Returning to normal view.', ...state.log.slice(0, 49)],
|
||||
isDescending: false,
|
||||
log: ['⬇️ Exited Spire Mode. Returning to normal view.', ...state.log.slice(0, 49)],
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user