-
+
+ {/* Single Enemy Display (Combat/Speed/Guardian) */}
+ {!isGuardianFloor && primaryEnemy && roomType !== 'swarm' && (
+
+
+
+
+
+ {primaryEnemy.name || 'Unknown Enemy'}
+
+
+
+ {ELEMENTS[primaryEnemy.element]?.sym} {ELEMENTS[primaryEnemy.element]?.name}
+
+
+
+ {/* Enemy HP Bar */}
+
+
+
+ {fmt(primaryEnemy.hp)} / {fmt(primaryEnemy.maxHP)} HP
+
+
+
+ {/* Enemy Properties */}
+
+ {primaryEnemy.armor > 0 && (
+
+
+
+
+ {(primaryEnemy.armor * 100).toFixed(0)}% Armor
+
+
+
+ Reduces incoming damage by {(primaryEnemy.armor * 100).toFixed(0)}%
+
+
+ )}
+ {primaryEnemy.dodgeChance > 0 && (
+
+
+
+
+ {(primaryEnemy.dodgeChance * 100).toFixed(0)}% Dodge
+
+
+
+ Chance to dodge attacks and reduce progress
+
+
+ )}
+
-
-
{fmt(store.floorHP)} / {fmt(store.floorMaxHP)} HP
-
DPS: {store.currentAction === 'climb' && activeEquipmentSpells.length > 0 ? fmtDec(totalDPS) : '—'}
+ )}
+
+ {/* Swarm Enemies Display */}
+ {roomType === 'swarm' && swarmEnemies.length > 0 && (
+
+
+ Swarm Enemies ({swarmEnemies.length})
+
+ {swarmEnemies.map((enemy, index) => (
+
+
+
+
+
+ {enemy.name || `Enemy ${index + 1}`}
+
+
+
+ {ELEMENTS[enemy.element]?.sym} {fmt(enemy.hp)}/{fmt(enemy.maxHP)} HP
+
+
+
+
+ ))}
-
-
+ )}
+
+ {/* Puzzle Room Display */}
+ {roomType === 'puzzle' && (
+
+
+ 🧩
+
+ {currentRoom.puzzleId ? currentRoom.puzzleId.replace(/_/g, ' ').toUpperCase() : 'Puzzle Room'}
+
+
+
+
+ Progress
+ {((currentRoom.puzzleProgress || 0) * 100).toFixed(0)}%
+
+
+
+
+ )}
+
+ {/* Floor HP Bar (for non-swarm, non-puzzle) */}
+ {roomType !== 'swarm' && roomType !== 'puzzle' && (
+
+
+
+ {fmt(store.floorHP)} / {fmt(store.floorMaxHP)} HP
+ DPS: {store.currentAction === 'climb' && activeEquipmentSpells.length > 0 ? fmtDec(totalDPS) : '—'}
+
+
+ )}
+
Best: Floor {store.maxFloorReached} •
Pacts: {store.signedPacts.length}
diff --git a/src/lib/game/store.ts b/src/lib/game/store.ts
index e9e257d..46c6788 100755
--- a/src/lib/game/store.ts
+++ b/src/lib/game/store.ts
@@ -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 = {
+ 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 {
// 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()(
}));
},
+ 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()(
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()(
}
// 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()(
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()(
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()(
}
// 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()(
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()(
: '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()(
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()(
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()(
// 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()(
elements,
unlockedEffects,
log,
+ activityLog,
castProgress,
equipmentSpellStates,
golemancy,
@@ -2068,16 +2195,27 @@ export const useGameStore = create()(
// 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()(
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()(
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)],
};
});
},