feat: add prestige system and skill upgrades with comprehensive documentation
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 5m57s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 5m57s
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
# Task 11: Fix Spell Info Display - Context Summary
|
||||
|
||||
## Task Status
|
||||
**Partially done** - dmg/cast is shown correctly, total DPS is shown, but per-spell DPS display is incorrect.
|
||||
|
||||
## Files Analyzed
|
||||
|
||||
### 1. `/home/user/repos/Mana-Loop/src/components/game/tabs/SpireTab.tsx`
|
||||
- **Lines 356-360**: Spell info display for active equipment spells
|
||||
- Shows: `⚔️ {fmt(calcDamage(store, spellId))} dmg/cast`
|
||||
- Shows: `⚡ {fmt(Math.floor(calcDamage(store, spellId) * (spellDef.castSpeed || 1)))} dmg/hr`
|
||||
- **ISSUE**: The "dmg/hr" calculation is incorrect. It multiplies damage by `castSpeed` (which is casts/hour), but doesn't account for:
|
||||
1. `quickCast` skill bonus (`1 + (skills.quickCast || 0) * 0.05`)
|
||||
2. Equipment `attackSpeedMultiplier` from upgrade effects
|
||||
3. The actual conversion from casts/hour to DPS
|
||||
|
||||
### 2. `/home/user/repos/Mana-Loop/src/lib/game/constants/spells.ts`
|
||||
- **Spell Definition Structure** (`SpellDef` interface in `/src/lib/game/types/spells.ts`):
|
||||
- `castSpeed?: number` - **Casts per hour** (default 1, higher = faster)
|
||||
- `dmg: number` - Base damage per cast
|
||||
- Examples: `manaBolt` has `castSpeed: 3` (3 casts per hour), `fireball` has `castSpeed: 2`
|
||||
|
||||
### 3. `/home/user/repos/Mana-Loop/src/lib/game/utils/combat-utils.ts`
|
||||
- **`calcDamage(state, spellId, floorElem)`** (lines 84-131):
|
||||
- Calculates damage per cast
|
||||
- Factors: `baseDmg = sp.dmg + (skills.combatTrain || 0) * 5`
|
||||
- Multipliers: `arcaneFury`, `elementalMastery`, `guardianBane`, `rawDamage`, `elemDamage`
|
||||
- Elemental bonus: `getElementalBonus(sp.elem, floorElem)`
|
||||
- Crit: `precision` skill + boon critChance
|
||||
|
||||
- **`getTotalDPS(state, upgradeEffects, floorElem)`** (lines 265-300):
|
||||
- Calculates TRUE total DPS from all active equipment spells
|
||||
- Uses `baseCastTime` (not `castSpeed`) - **BUG**: `baseCastTime` is not defined in `SpellDef`!
|
||||
- Actual formula used:
|
||||
```typescript
|
||||
const baseCastTime = spellDef.baseCastTime || 1.0;
|
||||
const castingSpeedBonus = 1 + (state.skills.castingSpeed || 0) * 0.1;
|
||||
const equipmentAttackSpeed = upgradeEffects.attackSpeedMultiplier || 1;
|
||||
const castTime = baseCastTime / (castingSpeedBonus * equipmentAttackSpeed);
|
||||
const spellDPS = damage / castTime;
|
||||
```
|
||||
- **ISSUE**: `baseCastTime` is always 1.0 (fallback), so the formula doesn't use `castSpeed` at all!
|
||||
|
||||
### 4. `/home/user/repos/Mana-Loop/src/lib/game/hooks/useGameDerived.ts`
|
||||
- **DPS calculation for active spell** (lines 145-152):
|
||||
```typescript
|
||||
const spellCastSpeed = activeSpellDef.castSpeed || 1; // casts per hour
|
||||
const quickCastBonus = 1 + (store.skills.quickCast || 0) * 0.05;
|
||||
const attackSpeedMult = upgradeEffects.attackSpeedMultiplier;
|
||||
const totalCastSpeed = spellCastSpeed * quickCastBonus * attackSpeedMult;
|
||||
const damagePerCast = calcDamage(store, store.activeSpell, floorElem);
|
||||
const castsPerSecond = totalCastSpeed * HOURS_PER_TICK / (TICK_MS / 1000);
|
||||
return damagePerCast * castsPerSecond;
|
||||
```
|
||||
- **This is the CORRECT formula for DPS**:
|
||||
- `castsPerSecond = castSpeed * quickCastBonus * attackSpeedMult * (HOURS_PER_TICK / (TICK_MS / 1000))`
|
||||
- With `HOURS_PER_TICK = 0.04` and `TICK_MS = 200`:
|
||||
- `castsPerSecond = castSpeed * quickCastBonus * attackSpeedMult * 0.04 / 0.2 = castSpeed * quickCastBonus * attackSpeedMult * 0.2`
|
||||
|
||||
### 5. `/home/user/repos/Mana-Loop/src/lib/game/store/combatSlice.ts`
|
||||
- **`processCombat`** (lines 60-75): Actual combat uses `castSpeed` correctly:
|
||||
```typescript
|
||||
const baseAttackSpeed = 1 + (state.skills.quickCast || 0) * 0.05;
|
||||
const totalAttackSpeed = baseAttackSpeed * effects.attackSpeedMultiplier;
|
||||
const spellCastSpeed = spellDef.castSpeed || 1;
|
||||
const progressPerTick = deltaHours * spellCastSpeed * totalAttackSpeed;
|
||||
```
|
||||
- Note: Uses `quickCast` skill (not `castingSpeed` which is from boons)
|
||||
|
||||
## Summary of Issues
|
||||
|
||||
### Issue 1: Per-spell "dmg/hr" display is wrong in SpireTab.tsx
|
||||
- **Current**: `calcDamage(store, spellId) * (spellDef.castSpeed || 1)`
|
||||
- **Problem**: This just multiplies damage by casts/hour, but doesn't convert to actual DPS correctly
|
||||
- **Correct formula** (from `useGameDerived.ts`):
|
||||
- `dps = damagePerCast * castSpeed * quickCastBonus * attackSpeedMult * HOURS_PER_TICK / (TICK_MS / 1000)`
|
||||
- Or simpler: `dps = damagePerCast * castSpeed * 0.2` (when no bonuses)
|
||||
|
||||
### Issue 2: `getTotalDPS` uses wrong field
|
||||
- **Current**: Uses `baseCastTime` which doesn't exist in `SpellDef`
|
||||
- **Fix needed**: Should use `castSpeed` (casts per hour) and convert to DPS properly
|
||||
- **Correct formula**:
|
||||
```typescript
|
||||
const castsPerHour = spellDef.castSpeed || 1;
|
||||
const quickCastBonus = 1 + (state.skills.quickCast || 0) * 0.05;
|
||||
const attackSpeedMult = upgradeEffects.attackSpeedMultiplier || 1;
|
||||
const totalCastsPerHour = castsPerHour * quickCastBonus * attackSpeedMult;
|
||||
const spellDPS = damage * totalCastsPerHour / 3600; // Convert casts/hour to casts/second
|
||||
```
|
||||
|
||||
### Issue 3: Inconsistent skill usage
|
||||
- `combatSlice.ts` and `useGameDerived.ts` use `quickCast` skill for cast speed bonus
|
||||
- `getTotalDPS` uses `castingSpeed` (which is from boons, not player skills)
|
||||
- Need to verify which is correct or use both
|
||||
|
||||
## Correct Formulas
|
||||
|
||||
### True DPS for a spell:
|
||||
```
|
||||
DPS = damage_per_cast × (castSpeed × quickCast_bonus × attackSpeed_multiplier) / 3600
|
||||
```
|
||||
|
||||
Where:
|
||||
- `damage_per_cast` = `calcDamage(store, spellId, floorElem)`
|
||||
- `castSpeed` = `spellDef.castSpeed || 1` (casts per hour)
|
||||
- `quickCast_bonus` = `1 + (skills.quickCast || 0) * 0.05`
|
||||
- `attackSpeed_multiplier` = from equipment effects
|
||||
- Divide by 3600 to convert from casts/hour to casts/second
|
||||
|
||||
### For display in SpireTab.tsx per-spell:
|
||||
- **dmg/cast**: Already correct - uses `calcDamage(store, spellId)`
|
||||
- **DPS**: Should show `damage_per_cast × (castSpeed × quickCast × attackSpeed) / 3600`
|
||||
- Current "dmg/hr" is misleading - it's not actually damage per hour with bonuses
|
||||
|
||||
## Files to Modify
|
||||
1. `/home/user/repos/Mana-Loop/src/components/game/tabs/SpireTab.tsx` (lines 356-360) - Fix per-spell DPS display
|
||||
2. `/home/user/repos/Mana-Loop/src/lib/game/utils/combat-utils.ts` (lines 280-295) - Fix `getTotalDPS` to use `castSpeed` instead of `baseCastTime`
|
||||
@@ -0,0 +1,66 @@
|
||||
# Task 14: Fix AchievementsTab Nesting - Context Summary
|
||||
|
||||
## Current State (Problem)
|
||||
|
||||
### Redundant Nested Layers Found:
|
||||
|
||||
1. **Nested GameCards (Double Card Wrapper)**
|
||||
- `AchievementsTab.tsx` wraps everything in a `<GameCard>` (lines 16-42)
|
||||
- `AchievementsDisplay.tsx` ALSO wraps everything in a `<GameCard variant="default" className="w-full">` (line 63)
|
||||
- This creates nested cards - a card inside a card - which is redundant and causes visual/structural issues
|
||||
|
||||
2. **Duplicate Headings**
|
||||
- `AchievementsTab.tsx` has an `<h2>` heading "Achievements" with badge showing `{unlockedCount} unlocked` (lines 19-26)
|
||||
- `AchievementsDisplay.tsx` has an `<h3>` heading "Achievements" with badge showing `{unlockedCount} / {totalCount}` (lines 64-72)
|
||||
- Both components render their own heading - this is redundant
|
||||
|
||||
### File Analysis:
|
||||
|
||||
#### AchievementsTab.tsx Structure:
|
||||
```
|
||||
<div className="space-y-4">
|
||||
<GameCard> ← OUTER CARD (should be removed)
|
||||
<h2>Achievements</h2> ← OUTER HEADING (should be removed)
|
||||
<AchievementsDisplay /> ← This component brings its own Card + Heading
|
||||
</GameCard>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### AchievementsDisplay.tsx Structure:
|
||||
```
|
||||
<GameCard variant="default"> ← INNER CARD (should stay)
|
||||
<h3>Achievements</h3> ← INNER HEADING (should stay)
|
||||
<ScrollArea>
|
||||
{/* achievement categories */}
|
||||
</ScrollArea>
|
||||
</GameCard>
|
||||
```
|
||||
|
||||
## Correct Structure (After Fix)
|
||||
|
||||
The `AchievementsTab` should NOT wrap `AchievementsDisplay` in a GameCard or provide its own heading. The correct structure is:
|
||||
|
||||
### AchievementsTab.tsx (Fixed):
|
||||
```
|
||||
<div className="space-y-4">
|
||||
<AchievementsDisplay /> ← Only render the display component, no wrapping
|
||||
</div>
|
||||
```
|
||||
|
||||
### AchievementsDisplay.tsx (Unchanged):
|
||||
```
|
||||
<GameCard variant="default"> ← Single card wrapper
|
||||
<h3>Achievements</h3> ← Single heading
|
||||
<ScrollArea>
|
||||
{/* achievement categories */}
|
||||
</ScrollArea>
|
||||
</GameCard>
|
||||
```
|
||||
|
||||
## Summary of Changes Needed:
|
||||
|
||||
1. **Remove GameCard wrapper from AchievementsTab.tsx** - Let AchievementsDisplay handle the card
|
||||
2. **Remove the h2 heading and badge from AchievementsTab.tsx** - Let AchievementsDisplay handle the heading
|
||||
3. **Keep AchievementsDisplay.tsx as-is** - It already has the correct structure
|
||||
|
||||
This eliminates the double-nesting and duplicate headings issue.
|
||||
@@ -0,0 +1,165 @@
|
||||
# Task 15: Add Mana-Type Capacity Enchantment Effects Per Unlocked Mana Type
|
||||
|
||||
## Context Summary
|
||||
|
||||
### 1. Current Enchantment Effects for Mana Capacity
|
||||
|
||||
**Location**: `/home/user/repos/Mana-Loop/src/lib/game/data/enchantments/mana-effects.ts`
|
||||
|
||||
Current mana capacity effects (in `MANA_EFFECTS`):
|
||||
- `mana_cap_50` - +50 maximum mana (max 3 stacks)
|
||||
- `mana_cap_100` - +100 maximum mana (max 3 stacks)
|
||||
- `weapon_mana_cap_20` - +20 weapon mana capacity (max 5 stacks)
|
||||
- `weapon_mana_cap_50` - +50 weapon mana capacity (max 3 stacks)
|
||||
- `weapon_mana_cap_100` - +100 weapon mana capacity (max 2 stacks)
|
||||
|
||||
**Element Capacity Effects**: There are currently NO per-type element capacity enchantment effects. The `elementCap` stat exists in `ComputedEffects` (in `upgrade-effects.ts`) and is applied via:
|
||||
- `elementCapBonus` - additive bonus to element max
|
||||
- `elementCapMultiplier` - multiplier for element max
|
||||
|
||||
These apply globally to ALL elements equally (see `computeElementMax` in `store.ts`).
|
||||
|
||||
### 2. Mana Types That Are Unlockable
|
||||
|
||||
**Location**: `/home/user/repos/Mana-Loop/src/lib/game/constants/elements.ts`
|
||||
|
||||
**Base Elements** (cat: "base"):
|
||||
- `fire` - Fire 🔥
|
||||
- `water` - Water 💧
|
||||
- `air` - Air 🌬️
|
||||
- `earth` - Earth ⛰️
|
||||
- `light` - Light ☀️
|
||||
- `dark` - Dark 🌑
|
||||
- `death` - Death 💀
|
||||
|
||||
**Utility Elements** (cat: "utility"):
|
||||
- `transference` - Transference 🔗 (ALREADY UNLOCKED by default in `BASE_UNLOCKED_ELEMENTS`)
|
||||
|
||||
**Composite Elements** (cat: "composite", require recipe to craft):
|
||||
- `metal` - Metal ⚙️ (recipe: fire + earth)
|
||||
- `sand` - Sand ⏳ (recipe: earth + water)
|
||||
- `lightning` - Lightning ⚡ (recipe: fire + air)
|
||||
|
||||
**Exotic Elements** (cat: "exotic", require complex recipes):
|
||||
- `crystal` - Crystal 💎 (recipe: sand + sand + light)
|
||||
- `stellar` - Stellar ⭐ (recipe: fire + fire + light)
|
||||
- `void` - Void 🕳️ (recipe: dark + dark + death)
|
||||
|
||||
**Total unlockable mana types**: 12 (all except `transference` which starts unlocked)
|
||||
|
||||
### 3. How to Add Per-Type Capacity Effects
|
||||
|
||||
**Current Effect System Architecture**:
|
||||
|
||||
1. **Effect Definition** (`EnchantmentEffectDef` in `/src/lib/game/data/enchantment-types.ts`):
|
||||
- Effects have a `stat` field that identifies what they modify
|
||||
- Stats like `maxMana`, `regen`, `clickMana`, `elementCap` are supported
|
||||
- Effects can be of type: `bonus` (additive), `multiplier`, or `special`
|
||||
|
||||
2. **Current Element Cap System** (`/src/lib/game/upgrade-effects.ts` and `/src/lib/game/effects.ts`):
|
||||
- `elementCapBonus` - additive bonus in `ComputedEffects`
|
||||
- `elementCapMultiplier` - multiplier in `ComputedEffects`
|
||||
- Applied in `computeElementMax()` in `store.ts`:
|
||||
```typescript
|
||||
export function computeElementMax(state, effects?): number {
|
||||
const pu = state.prestigeUpgrades;
|
||||
const base = 10 + (state.skills.elemAttune || 0) * 50 + (pu.elementalAttune || 0) * 25;
|
||||
if (effects) {
|
||||
return Math.floor((base + effects.elementCapBonus) * effects.elementCapMultiplier);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
```
|
||||
|
||||
3. **Approach to Add Per-Type Capacity Effects**:
|
||||
|
||||
**Option A: Extend the stat system with per-element prefixes**
|
||||
- Add new stats like `elementCap_fire`, `elementCap_water`, etc.
|
||||
- Modify `computeElementMax` to accept element parameter
|
||||
- Modify `computeAllEffects` to handle per-element bonuses
|
||||
|
||||
**Option B: Use a single stat with element metadata (Recommended)**
|
||||
- Add effects with stat format: `elementCap_fire`, `elementCap_water`, etc.
|
||||
- In `computeEquipmentEffects`, parse the element from stat name
|
||||
- Store per-element bonuses in a `Record<string, number>` map
|
||||
- Modify `computeElementMax` to accept element and look up per-element bonus
|
||||
|
||||
**Option C: Add a new effect type for element-specific bonuses**
|
||||
- Add new effect structure: `{ type: 'elementBonus', element: string, stat: 'capacity', value: number }`
|
||||
- Requires modifying `EnchantmentEffectDef` type
|
||||
|
||||
4. **Implementation Steps** (using Option B):
|
||||
|
||||
a. **Define new enchantment effects** in `mana-effects.ts`:
|
||||
```typescript
|
||||
fire_cap_10: {
|
||||
id: 'fire_cap_10',
|
||||
name: 'Fire Reservoir',
|
||||
description: '+10 Fire mana capacity',
|
||||
category: 'mana',
|
||||
baseCapacityCost: 25,
|
||||
maxStacks: 5,
|
||||
allowedEquipmentCategories: MANA_EQUIPMENT,
|
||||
effect: { type: 'bonus', stat: 'elementCap_fire', value: 10 }
|
||||
}
|
||||
// Repeat for each element: water, air, earth, light, dark, death, metal, sand, lightning, crystal, stellar, void
|
||||
```
|
||||
|
||||
b. **Update `ComputedEffects`** in `upgrade-effects.ts` to add per-element storage:
|
||||
```typescript
|
||||
export interface ComputedEffects {
|
||||
// ... existing fields
|
||||
perElementCapBonus: Record<string, number>; // New: per-element capacity bonuses
|
||||
}
|
||||
```
|
||||
|
||||
c. **Update `computeEquipmentEffects`** in `effects.ts` to parse element-specific stats:
|
||||
```typescript
|
||||
// In the bonus processing:
|
||||
if (effect.stat.startsWith('elementCap_')) {
|
||||
const element = effect.stat.replace('elementCap_', '');
|
||||
bonuses.perElementCapBonus[element] = (bonuses.perElementCapBonus?.[element] || 0) + effect.value * ench.stacks;
|
||||
}
|
||||
```
|
||||
|
||||
d. **Update `computeElementMax`** in `store.ts` to use per-element bonuses:
|
||||
```typescript
|
||||
export function computeElementMax(state, effects?, element?: string): number {
|
||||
const base = 10 + (state.skills.elemAttune || 0) * 50 + ...;
|
||||
if (effects) {
|
||||
let bonus = effects.elementCapBonus; // Global bonus
|
||||
if (element && effects.perElementCapBonus?.[element]) {
|
||||
bonus += effects.perElementCapBonus[element];
|
||||
}
|
||||
return Math.floor((base + bonus) * effects.elementCapMultiplier);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
```
|
||||
|
||||
e. **Update `unlockElement`** in `store.ts` to check for per-element capacity bonuses when unlocking
|
||||
|
||||
5. **Effect Unlocking**: New effects should be unlocked via:
|
||||
- `BASE_UNLOCKED_EFFECTS` - effects available from game start
|
||||
- `EFFECT_RESEARCH_MAPPING` - effects unlocked by leveling specific skills
|
||||
- `ENCHANTING_UNLOCK_EFFECTS` - effects unlocked by leveling enchanting skill
|
||||
|
||||
### 4. Key Files to Modify
|
||||
|
||||
1. `/src/lib/game/data/enchantments/mana-effects.ts` - Add per-element capacity effects
|
||||
2. `/src/lib/game/upgrade-effects.ts` - Update `ComputedEffects` interface, add per-element cap handling
|
||||
3. `/src/lib/game/effects.ts` - Update `computeEquipmentEffects` to parse element-specific stats
|
||||
4. `/src/lib/game/store.ts` - Update `computeElementMax` to accept element parameter and use per-element bonuses
|
||||
5. `/src/lib/game/constants/index.ts` or relevant constants file - Add new effects to unlockable lists if needed
|
||||
|
||||
### 5. Allowed Equipment Categories
|
||||
|
||||
For reference, from `enchantment-types.ts` and existing effects:
|
||||
- `MANA_EQUIPMENT: EquipmentCategory[] = ['caster', 'catalyst', 'head', 'body', 'accessory']`
|
||||
- Per-type capacity effects should likely use `MANA_EQUIPMENT` or similar
|
||||
|
||||
### 6. Existing Related Special Effect
|
||||
|
||||
`ELEMENTAL_AFFINITY` special effect (`SPECIAL_EFFECTS.ELEMENTAL_AFFINITY`):
|
||||
- When unlocking a new element, starts with 10 capacity instead of 0
|
||||
- This is already implemented in `unlockElement` in `store.ts`
|
||||
@@ -0,0 +1,174 @@
|
||||
# Task 16: Gate Mana Capacity Research Visibility by Unlocked Mana Type (PRIORITY 4b)
|
||||
|
||||
## Context Summary
|
||||
|
||||
### 1. How Research Nodes Are Currently Filtered/Displayed
|
||||
|
||||
**Location:** `/home/user/repos/Mana-Loop/src/components/game/tabs/SkillsTab.tsx`
|
||||
|
||||
**Current Filtering Mechanism:**
|
||||
- Skills are organized by categories defined in `SKILL_CATEGORIES` (from `/home/user/repos/Mana-Loop/src/lib/game/constants/skills.ts`)
|
||||
- The `SkillsTab` component uses `getAvailableSkillCategories(store.attunements || {})` to determine which skill categories to display
|
||||
- `getAvailableSkillCategories` (from `/home/user/repos/Mana-Loop/src/lib/game/data/attunements.ts`) returns categories based on active attunements:
|
||||
- Always available: `'mana'`, `'study'`, `'research'`, `'ascension'`
|
||||
- Enchanter attunement adds: `'enchant'`, `'effectResearch'`
|
||||
- Invoker attunement adds: `'invocation'`, `'pact'`
|
||||
- Fabricator attunement adds: `'fabrication'`, `'golemancy'`
|
||||
- Legacy: `'craft'`
|
||||
|
||||
**Skill Display Logic (SkillsTab.tsx lines 200-220):**
|
||||
```typescript
|
||||
const availableCategories = getAvailableSkillCategories(store.attunements || {});
|
||||
return SKILL_CATEGORIES
|
||||
.filter(cat => availableCategories.includes(cat.id))
|
||||
.map((cat) => {
|
||||
const skillsInCat = Object.entries(SKILLS_DEF).filter(([, def]) => def.cat === cat.id);
|
||||
// ... render skills
|
||||
});
|
||||
```
|
||||
|
||||
**Prerequisites Checking (SkillsTab.tsx lines 269-280):**
|
||||
- Skills check `def.req` (skill prerequisites)
|
||||
- Skills check `def.attunementReq` (attunement level requirements)
|
||||
- Skills with element costs check if player has enough element mana (but skill is still VISIBLE)
|
||||
|
||||
**Key Point:** Mana capacity research skills (e.g., `fireManaCap`, `waterManaCap`) are currently ALWAYS visible in the "Mana" category (which is always available). They just show as "cannot study" if the player lacks the required element mana.
|
||||
|
||||
---
|
||||
|
||||
### 2. Unlocked Mana Types State
|
||||
|
||||
**Location:** `/home/user/repos/Mana-Loop/src/lib/game/store.ts` and `/home/user/repos/Mana-Loop/src/lib/game/types/elements.ts`
|
||||
|
||||
**GameState Elements Structure:**
|
||||
```typescript
|
||||
elements: Record<string, ElementState>;
|
||||
```
|
||||
|
||||
**ElementState Interface:**
|
||||
```typescript
|
||||
interface ElementState {
|
||||
current: number; // Current mana amount
|
||||
max: number; // Maximum capacity
|
||||
unlocked: boolean; // Whether this element type is unlocked
|
||||
}
|
||||
```
|
||||
|
||||
**Base Unlocked Elements:**
|
||||
- Defined in `/home/user/repos/Mana-Loop/src/lib/game/constants/elements.ts`
|
||||
- `BASE_UNLOCKED_ELEMENTS = ['transference']` - Only transference is unlocked at game start
|
||||
|
||||
**Element Unlocking Mechanisms:**
|
||||
1. **`unlockElement(element)` action** (store.ts line 2123): Costs 500 raw mana, sets `unlocked: true`
|
||||
2. **`craftComposite(target)` action**: Automatically unlocks composite elements when crafted
|
||||
3. **Enchanter attunement**: Auto-unlocks transference element (store.ts line 695)
|
||||
|
||||
**All Element Types (from ELEMENTS constant):**
|
||||
- Base: `fire`, `water`, `air`, `earth`, `light`, `dark`, `death`
|
||||
- Utility: `transference`
|
||||
- Composite: `metal` (fire+earth), `sand` (earth+water), `lightning` (fire+air)
|
||||
- Exotic: `crystal` (sand+sand+light), `stellar` (fire+fire+light), `void` (dark+dark+death)
|
||||
|
||||
---
|
||||
|
||||
### 3. Mana Capacity Research Skills
|
||||
|
||||
**Location:** `/home/user/repos/Mana-Loop/src/lib/game/constants/skills.ts`
|
||||
|
||||
**Mana Capacity Research Nodes (lines 9-21):**
|
||||
```typescript
|
||||
// Per-mana-type capacity upgrades (Bug 9)
|
||||
fireManaCap: { name: "Fire Mana Capacity +10%", desc: "...", cat: "mana", max: 10, base: 200, studyTime: 4, cost: { type: 'element', element: 'fire', amount: 100 } },
|
||||
waterManaCap: { name: "Water Mana Capacity +10%", cat: "mana", cost: { type: 'element', element: 'water', amount: 100 } },
|
||||
airManaCap: { name: "Air Mana Capacity +10%", cat: "mana", cost: { type: 'element', element: 'air', amount: 100 } },
|
||||
earthManaCap: { name: "Earth Mana Capacity +10%", cat: "mana", cost: { type: 'element', element: 'earth', amount: 100 } },
|
||||
lightManaCap: { name: "Light Mana Capacity +10%", cat: "mana", cost: { type: 'element', element: 'light', amount: 150 } },
|
||||
darkManaCap: { name: "Dark Mana Capacity +10%", cat: "mana", cost: { type: 'element', element: 'dark', amount: 150 } },
|
||||
deathManaCap: { name: "Death Mana Capacity +10%", cat: "mana", cost: { type: 'element', element: 'death', amount: 200 } },
|
||||
// Composite element capacity upgrades
|
||||
metalManaCap: { name: "Metal Mana Capacity +10%", cat: "mana", cost: { type: 'element', element: 'metal', amount: 250 } },
|
||||
sandManaCap: { name: "Sand Mana Capacity +10%", cat: "mana", cost: { type: 'element', element: 'sand', amount: 250 } },
|
||||
lightningManaCap: { name: "Lightning Mana Capacity +10%", cat: "mana", cost: { type: 'element', element: 'lightning', amount: 250 } },
|
||||
// Utility mana capacity upgrades
|
||||
transferenceManaCap: { name: "Transference Mana Capacity +10%", cat: "mana", cost: { type: 'element', element: 'transference', amount: 100 } },
|
||||
```
|
||||
|
||||
**Key Observation:** Each mana capacity skill has:
|
||||
- `cost.type: 'element'`
|
||||
- `cost.element`: The element type this skill applies to
|
||||
- `cost.amount`: The element mana required to study
|
||||
|
||||
---
|
||||
|
||||
### 4. How to Gate Capacity Research by Unlocked Mana Type
|
||||
|
||||
**Objective:** Hide mana capacity research skills unless the corresponding element type is unlocked.
|
||||
|
||||
**Implementation Approach:**
|
||||
|
||||
1. **In SkillsTab.tsx, add filtering logic for mana capacity skills:**
|
||||
|
||||
When rendering skills in the "mana" category, check if the skill:
|
||||
- Has `def.cost?.type === 'element'`
|
||||
- AND `store.elements[def.cost.element]?.unlocked === true`
|
||||
|
||||
2. **Modified SkillsTab rendering (around line 220):**
|
||||
```typescript
|
||||
{skillsInCat.map(([id, def]) => {
|
||||
// GATE MANA CAPACITY SKILLS BY UNLOCKED ELEMENT
|
||||
if (def.cost?.type === 'element') {
|
||||
const element = store.elements[def.cost.element];
|
||||
if (!element?.unlocked) {
|
||||
return null; // Don't render this skill
|
||||
}
|
||||
}
|
||||
// ... rest of skill rendering
|
||||
})}
|
||||
```
|
||||
|
||||
3. **Alternative: Filter at category level (less granular):**
|
||||
- Could filter `skillsInCat` before mapping:
|
||||
```typescript
|
||||
const visibleSkills = skillsInCat.filter(([id, def]) => {
|
||||
if (def.cost?.type === 'element') {
|
||||
return store.elements[def.cost.element]?.unlocked;
|
||||
}
|
||||
return true; // Show non-element-cost skills
|
||||
});
|
||||
```
|
||||
|
||||
4. **Optional: Show locked state instead of hiding:**
|
||||
- Could show a "locked" badge or tooltip explaining the element needs to be unlocked first
|
||||
- This would require modifying the skill rendering to handle a "locked due to element not unlocked" state
|
||||
|
||||
---
|
||||
|
||||
### 5. Files to Modify
|
||||
|
||||
1. **`/home/user/repos/Mana-Loop/src/components/game/tabs/SkillsTab.tsx`**
|
||||
- Add filtering logic to hide mana capacity research skills when the corresponding element is not unlocked
|
||||
- Location: Around line 220 in the `skillsInCat.map()` function
|
||||
|
||||
2. **No backend/store changes needed** - The `unlocked` state already exists in `store.elements`. This is purely a UI/display change.
|
||||
|
||||
---
|
||||
|
||||
### 6. Testing Considerations
|
||||
|
||||
- Test that `fireManaCap` is hidden when `store.elements['fire'].unlocked === false`
|
||||
- Test that `fireManaCap` becomes visible after calling `store.unlockElement('fire')`
|
||||
- Test that non-element-cost skills (like `manaWell`, `manaFlow`) are always visible
|
||||
- Test composite element skills (`metalManaCap`, etc.) hide/show correctly
|
||||
- Test that the "Mana" category still shows other non-gated skills even when some are hidden
|
||||
|
||||
---
|
||||
|
||||
### 7. Related Task Context
|
||||
|
||||
This task is related to:
|
||||
- **Task 9 (Bug 9)**: Per-mana-type capacity upgrades - the skills being gated were added in this task
|
||||
- **Task 12 (Bug 12)**: Research moved to Mana category - this is why capacity research is in the "mana" category
|
||||
|
||||
---
|
||||
|
||||
**Summary:** The gating logic needs to be added to `SkillsTab.tsx` to filter out mana capacity research skills (`*ManaCap`) when `store.elements[element].unlocked` is `false`. The state already tracks unlocked elements, so no store changes are needed.
|
||||
@@ -0,0 +1,111 @@
|
||||
# Task 17: Fix Skill Requirement Display Bug (undefined Lv.[object Object])
|
||||
|
||||
## Summary
|
||||
|
||||
This bug causes skills to display "Requires: [Skill Name] Lv.[object Object]" instead of the proper level requirement. The root cause is that `cost` objects are incorrectly placed INSIDE the `req` (requirements) object in `skills.ts`, causing the rendering code to iterate over the cost object as if it were a skill requirement.
|
||||
|
||||
## Root Cause
|
||||
|
||||
In `/home/user/repos/Mana-Loop/src/lib/game/constants/skills.ts`, several skill entries have the `cost` property incorrectly nested inside the `req` object.
|
||||
|
||||
### Correct Format (cost OUTSIDE req):
|
||||
```typescript
|
||||
researchFireSpells: {
|
||||
name: "Fire Spell Research",
|
||||
desc: "...",
|
||||
cat: "effectResearch",
|
||||
max: 1,
|
||||
base: 300,
|
||||
studyTime: 6,
|
||||
req: { enchanting: 2 },
|
||||
cost: { type: 'element', element: 'fire', amount: 100 },
|
||||
attunementReq: { enchanter: 1 }
|
||||
}
|
||||
```
|
||||
|
||||
### Malformed Format (cost INSIDE req - BUG):
|
||||
```typescript
|
||||
researchLifeDeathSpells: {
|
||||
name: "Death Research",
|
||||
desc: "...",
|
||||
cat: "effectResearch",
|
||||
max: 1,
|
||||
base: 400,
|
||||
studyTime: 8,
|
||||
req: { enchanting: 3 , cost: { type: 'element', element: 'death', amount: 100 }}, // BUG: cost inside req
|
||||
attunementReq: { enchanter: 2 }
|
||||
}
|
||||
```
|
||||
|
||||
## Malformed Skill Entries
|
||||
|
||||
The following skills have `cost` incorrectly placed inside `req` (lines in skills.ts):
|
||||
|
||||
1. **researchLifeDeathSpells** (line 51)
|
||||
2. **researchAdvancedFire** (line 54)
|
||||
3. **researchAdvancedWater** (line 55)
|
||||
4. **researchAdvancedAir** (line 56)
|
||||
5. **researchAdvancedEarth** (line 57)
|
||||
6. **researchAdvancedLight** (line 58)
|
||||
7. **researchAdvancedDark** (line 59)
|
||||
8. **researchMasterFire** (line 62)
|
||||
9. **researchMasterWater** (line 63)
|
||||
10. **researchMasterEarth** (line 64)
|
||||
11. **researchMetalSpells** (line 86)
|
||||
12. **researchSandSpells** (line 87)
|
||||
13. **researchLightningSpells** (line 88)
|
||||
14. **researchAdvancedMetal** (line 91)
|
||||
15. **researchAdvancedSand** (line 92)
|
||||
16. **researchAdvancedLightning** (line 93)
|
||||
17. **researchMasterMetal** (line 96)
|
||||
18. **researchMasterSand** (line 97)
|
||||
19. **researchMasterLightning** (line 98)
|
||||
|
||||
Total: **19 malformed skill entries**
|
||||
|
||||
## How Skill Requirements are Rendered
|
||||
|
||||
In `/home/user/repos/Mana-Loop/src/components/game/tabs/SkillsTab.tsx` (lines 233-235):
|
||||
|
||||
```tsx
|
||||
{!prereqMet && def.req && (
|
||||
<div className="text-xs text-red-400 mt-1">
|
||||
Requires: {Object.entries(def.req).map(([r, rl]) => `${SKILLS_DEF[r]?.name} Lv.${rl}`).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
The code expects `def.req` to be `Record<string, number>` (skill ID -> required level). When `cost` is inside `req`, the iteration produces entries like:
|
||||
- `[ "enchanting", 3 ]` → "Enchanting Lv.3" ✓
|
||||
- `[ "cost", { type: 'element', ... } ]` → "undefined Lv.[object Object]" ✗
|
||||
|
||||
## The Fix
|
||||
|
||||
For each malformed entry in `skills.ts`, move the `cost` property OUT of the `req` object:
|
||||
|
||||
**Before:**
|
||||
```typescript
|
||||
req: { enchanting: 3 , cost: { type: 'element', element: 'death', amount: 100 }}
|
||||
```
|
||||
|
||||
**After:**
|
||||
```typescript
|
||||
req: { enchanting: 3 }, cost: { type: 'element', element: 'death', amount: 100 }
|
||||
```
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. `/home/user/repos/Mana-Loop/src/lib/game/constants/skills.ts` - Fix 19 malformed skill entries
|
||||
|
||||
## No Changes Needed In
|
||||
|
||||
- `/home/user/repos/Mana-Loop/src/components/game/tabs/SkillsTab.tsx` - Rendering code is correct
|
||||
- `/home/user/repos/Mana-Loop/src/lib/game/formatting.ts` - Not related to this bug
|
||||
- `/home/user/repos/Mana-Loop/src/lib/game/types/skills.ts` - Type definitions are correct
|
||||
|
||||
## Steps to Fix
|
||||
|
||||
1. Edit `/home/user/repos/Mana-Loop/src/lib/game/constants/skills.ts`
|
||||
2. For each of the 19 malformed entries, move `cost` from inside `req` to be a separate property
|
||||
3. Verify the fix by checking that `Object.entries(def.req)` only returns skill ID/level pairs
|
||||
4. Run typecheck and lint to confirm no errors
|
||||
@@ -0,0 +1,330 @@
|
||||
# Subtask 18 Context: Enchantment Power Effect + Audit Stubs
|
||||
|
||||
## Current Enchantment Power Effect Status
|
||||
|
||||
### Definition Status
|
||||
- **"Enchantment Power" is NOT defined as an enchantment effect** in `src/lib/game/data/enchantments/` directory
|
||||
- The `ENCHANTMENT_EFFECTS` catalog (in `enchantment-effects.ts` and `enchantments/index.ts`) does not contain any effect with "Enchantment Power" as a defined effect
|
||||
- Searching for "Enchantment Power" in enchantment files returns no results
|
||||
|
||||
### How Enchantment Power is Referenced
|
||||
The `enchantPower` stat IS referenced in **skill upgrade perks** in `src/lib/game/skill-evolution.ts`:
|
||||
|
||||
| Perk ID | Name | Effect | Skill Tree |
|
||||
|---------|------|--------|------------|
|
||||
| `en_t1_l5_a` | Artisan's Touch | `+10% Enchantment Power` | Enchanting T1 |
|
||||
| `en_t1_l10_a` | Greater Artisan | `+15% Enchantment Power` | Enchanting T1 |
|
||||
| `en_t2_l5_a` | Expert Artisan | `+25% Enchantment Power` | Enchanting T2 |
|
||||
| `en_t2_l10_a` | Master Artisan | `+35% Enchantment Power` | Enchanting T2 |
|
||||
| `en_t3_l5_a` | Cosmic Artisan | `+50% Enchantment Power` | Enchanting T3 |
|
||||
| `en_t3_l10_a` | [ELITE] OMNI-ARTISAN | `2x enchantment power` | Enchanting T3 |
|
||||
| `en_t4_l5_a` | Astral Artisan | `+75% Enchantment Power` | Enchanting T4 |
|
||||
| `en_t4_l10_a` | Galactic Artisan | `+100% Enchantment Power` | Enchanting T4 |
|
||||
| `en_t5_l5_a` | Divine Artisan | `+150% Enchantment Power` | Enchanting T5 |
|
||||
| `en_t5_l10_a` | [ELITE] ASCENDED ARTISAN | `5x enchantment power` | Enchanting T5 |
|
||||
| `es_t1_l5_c` | Quick Work | `+5% Enchantment Power` | Essence Shaping T1 |
|
||||
| `es_t1_l10_c` | Superior Work | `+10% Enchantment Power` | Essence Shaping T1 |
|
||||
| `es_t2_l5_c` | Expert Work | `+15% Enchantment Power` | Essence Shaping T2 |
|
||||
| `es_t2_l10_c` | Master Work | `+20% Enchantment Power` | Essence Shaping T2 |
|
||||
| `es_t3_l5_c` | Divine Work | `+25% Enchantment Power` | Essence Shaping T3 |
|
||||
| `es_t3_l10_c` | [ELITE] OMNI-WORK | `2x enchantment power` | Essence Shaping T3 |
|
||||
| `ee_t1_l5_c` | Quality Work | `+5% Enchantment Power` | Elemental Evocation T1 |
|
||||
| `ee_t1_l10_c` | Superior Work | `+10% Enchantment Power` | Elemental Evocation T1 |
|
||||
| `ee_t2_l5_c` | Expert Work | `+15% Enchantment Power` | Elemental Evocation T2 |
|
||||
| `ee_t2_l10_c` | Master Work | `+20% Enchantment Power` | Elemental Evocation T2 |
|
||||
| `ee_t3_l5_c` | Divine Work | `+25% Enchantment Power` | Elemental Evocation T3 |
|
||||
| `ee_t3_l10_c` | [ELITE] OMNI-POWER | `2x enchantment power` | Elemental Evocation T3 |
|
||||
|
||||
### Effect Type in Skill Evolution
|
||||
All these perks use the effect structure:
|
||||
```typescript
|
||||
{ type: 'multiplier', stat: 'enchantPower', value: <decimal_value> }
|
||||
```
|
||||
|
||||
### Problem: `enchantPower` Stat Not Handled in Effects System
|
||||
In `src/lib/game/upgrade-effects.ts`, the `computeEffects` function has a switch statement for multiplier effects (lines 295-320) but **does NOT have a case for `enchantPower`**:
|
||||
|
||||
```typescript
|
||||
switch (effect.stat) {
|
||||
case 'maxMana':
|
||||
effects.maxManaMultiplier *= effect.value;
|
||||
break;
|
||||
case 'regen':
|
||||
effects.regenMultiplier *= effect.value;
|
||||
break;
|
||||
// ... other cases ...
|
||||
// NO CASE FOR 'enchantPower'!
|
||||
}
|
||||
```
|
||||
|
||||
Similarly, the `ComputedEffects` interface (lines 16-48) does NOT include an `enchantPower` or `enchantmentPowerMultiplier` field.
|
||||
|
||||
### StatsTab.tsx Attempts to Read `enchantPower`
|
||||
In `src/components/game/tabs/StatsTab.tsx` (lines 161-178), there's a placeholder that tries to read `enchantPower`:
|
||||
```typescript
|
||||
{upgradeEffects && 'enchantPower' in upgradeEffects
|
||||
? `${(upgradeEffects as Record<string, number>).enchantPower.toFixed(2)}×`
|
||||
: '1.0×'}
|
||||
```
|
||||
This is a type-safe workaround since `enchantPower` is not in the `ComputedEffects` interface.
|
||||
|
||||
---
|
||||
|
||||
## Stub Location in EquipmentTab
|
||||
|
||||
**File:** `/home/user/repos/Mana-Loop/src/components/game/tabs/EquipmentTab.tsx`
|
||||
|
||||
**Lines 516-531:**
|
||||
```typescript
|
||||
{/* Enchantment Power (placeholder for Task 5) */}
|
||||
<GameCard className="mt-4">
|
||||
<div className="pb-2">
|
||||
<h3 className="text-sm font-semibold text-[var(--text-primary)] flex items-center gap-2">
|
||||
✨ Enchantment Power
|
||||
</h3>
|
||||
</div>
|
||||
<div>
|
||||
<StatRow
|
||||
label="Enchantment Power:"
|
||||
value="1.0×"
|
||||
highlight="info"
|
||||
/>
|
||||
<p className="text-xs text-[var(--text-muted)] mt-2">
|
||||
Increases the power of all enchantments. Will be wired from Task 5 implementation.
|
||||
</p>
|
||||
</div>
|
||||
</GameCard>
|
||||
```
|
||||
|
||||
**Exact stub text:** "Increases the power of all enchantments. Will be wired from Task 5 implementation."
|
||||
|
||||
**Location:** Line 530 in EquipmentTab.tsx
|
||||
|
||||
---
|
||||
|
||||
## Other Unwired Stubs
|
||||
|
||||
### 1. AttunementsTab.tsx - Disenchanting TODO
|
||||
**File:** `/home/user/repos/Mana-Loop/src/components/game/tabs/AttunementsTab.tsx`
|
||||
**Line:** 198
|
||||
**Content:**
|
||||
```typescript
|
||||
{cap === 'disenchanting' && '🔄 Disenchant'} {/* TODO: Remove after bug 13 complete */}
|
||||
```
|
||||
**Description:** TODO comment indicating disenchanting cap should be removed after bug 13 is complete.
|
||||
|
||||
### 2. AttunementsTab.tsx - Research TODO
|
||||
**File:** `/home/user/repos/Mana-Loop/src/components/game/tabs/AttunementsTab.tsx`
|
||||
**Line:** 249
|
||||
**Content:**
|
||||
```typescript
|
||||
{cat === 'research' && '🔮 Research'} {/* TODO: Remove after Bug 12 - research moved to mana */}
|
||||
```
|
||||
**Description:** TODO comment indicating research category should be removed after Bug 12 (research moved to mana).
|
||||
|
||||
### 3. AttunementsTab.tsx.backup - Disenchanting TODO (backup file)
|
||||
**File:** `/home/user/repos/Mana-Loop/src/components/game/tabs/AttunementsTab.tsx.backup`
|
||||
**Line:** 198
|
||||
**Content:**
|
||||
```typescript
|
||||
{cap === 'disenchanting' && '🔄 Disenchant'} // TODO: Remove after bug 13 complete
|
||||
```
|
||||
**Description:** Same as #1 but in backup file (can likely be ignored).
|
||||
|
||||
### 4. AttunementsTab.tsx.backup - Research TODO (backup file)
|
||||
**File:** `/home/user/repos/Mana-Loop/src/components/game/tabs/AttunementsTab.tsx.backup`
|
||||
**Line:** 249
|
||||
**Content:**
|
||||
```typescript
|
||||
{cat === 'research' && '🔮 Research'} // TODO: Remove after Bug 12 - research moved to mana
|
||||
```
|
||||
**Description:** Same as #2 but in backup file (can likely be ignored).
|
||||
|
||||
### 5. skill-evolution.ts - Attunement Requirement Placeholder
|
||||
**File:** `/home/user/repos/Mana-Loop/src/lib/game/skill-evolution.ts`
|
||||
**Line:** 2218
|
||||
**Content:**
|
||||
```typescript
|
||||
// Check attunement requirement (placeholder - would need actual attunement check)
|
||||
```
|
||||
**Description:** Placeholder comment indicating attunement requirement check is not implemented.
|
||||
|
||||
### 6. StatsTab.tsx - Enchantment Power (Wired but Non-functional)
|
||||
**File:** `/home/user/repos/Mana-Loop/src/components/game/tabs/StatsTab.tsx`
|
||||
**Lines:** 161-178
|
||||
**Content:**
|
||||
```typescript
|
||||
{/* Enchantment Power (placeholder for Task 5) */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-blue-400 game-panel-title text-xs flex items-center gap-2">
|
||||
✨ Enchantment Power
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Enchantment Power:</span>
|
||||
<span className="text-blue-300 font-[var(--font-mono)]">
|
||||
{upgradeEffects && 'enchantPower' in upgradeEffects
|
||||
? `${(upgradeEffects as Record<string, number>).enchantPower.toFixed(2)}×`
|
||||
: '1.0×'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
Increases the power of all enchantments. Wired from Task 5 implementation.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
```
|
||||
**Description:** This is labeled "Wired from Task 5 implementation" but the underlying `enchantPower` stat is not actually computed in `upgrade-effects.ts`. The display uses a type cast workaround.
|
||||
|
||||
---
|
||||
|
||||
## Effects System Overview
|
||||
|
||||
### How Effects Are Applied
|
||||
|
||||
The effects system is implemented across two main files:
|
||||
|
||||
#### 1. `src/lib/game/upgrade-effects.ts` - Skill Upgrade Effects
|
||||
- **Interface:** `ComputedEffects` (lines 16-48)
|
||||
- **Function:** `computeEffects()` (lines 260-330)
|
||||
- Processes skill upgrade effects from `skillUpgrades` and `skillTiers`
|
||||
- Handles three effect types:
|
||||
- `multiplier` - multiplies a stat (e.g., `maxMana`, `regen`, `clickMana`)
|
||||
- `bonus` - adds to a stat (e.g., `maxMana`, `regen`, `baseDamage`)
|
||||
- `special` - adds a special effect ID to the `specials` set
|
||||
|
||||
#### 2. `src/lib/game/effects.ts` - Unified Effects (Skill + Equipment)
|
||||
- **Function:** `computeEquipmentEffects()` (lines 20-78)
|
||||
- Processes equipped item enchantments
|
||||
- Returns `bonuses`, `multipliers`, and `specials`
|
||||
- For each enchantment:
|
||||
- `bonus` type: adds `effect.value * ench.stacks` to `bonuses[effect.stat]`
|
||||
- `multiplier` type: multiplies `multipliers[effect.stat]` by `effect.value` for each stack
|
||||
- `special` type: adds `effect.specialId` to `specials` set
|
||||
|
||||
- **Function:** `computeAllEffects()` (lines 91-137)
|
||||
- Merges skill upgrade effects with equipment effects
|
||||
- Merging strategy:
|
||||
- Bonuses: ADD together
|
||||
- Multipliers: MULTIPLY together
|
||||
- Specials: UNION of sets
|
||||
|
||||
### Where Enchantment Power Multiplier Would Go
|
||||
|
||||
The `enchantmentPower` (or `enchantPower`) multiplier should:
|
||||
|
||||
1. **Be added to `ComputedEffects` interface** in `upgrade-effects.ts`:
|
||||
```typescript
|
||||
enchantmentPowerMultiplier: number; // defaults to 1
|
||||
```
|
||||
|
||||
2. **Be handled in `computeEffects()` switch statement** in `upgrade-effects.ts`:
|
||||
```typescript
|
||||
case 'enchantPower':
|
||||
effects.enchantmentPowerMultiplier *= effect.value;
|
||||
break;
|
||||
```
|
||||
|
||||
3. **Be applied in `computeEquipmentEffects()` or `computeAllEffects()`** in `effects.ts`:
|
||||
- Option A: Apply to `effect.value` when processing each enchantment
|
||||
- Option B: Apply as an additional multiplier in `computeAllEffects()`
|
||||
|
||||
The most logical place is in `computeEquipmentEffects()` where enchantment values are calculated:
|
||||
```typescript
|
||||
// When processing bonus effects:
|
||||
bonuses[effect.stat] = (bonuses[effect.stat] || 0) + effect.value * ench.stacks * enchantmentPowerMultiplier;
|
||||
|
||||
// When processing multiplier effects:
|
||||
// Each stack applies the multiplier, also modified by enchantmentPowerMultiplier
|
||||
for (let i = 0; i < ench.stacks; i++) {
|
||||
multipliers[key] *= (effect.value - 1) * enchantmentPowerMultiplier + 1;
|
||||
// OR simpler: just multiply the final multiplier by enchantmentPowerMultiplier
|
||||
}
|
||||
```
|
||||
|
||||
### Current Multiplier Application (from effects.ts lines 48-57):
|
||||
```typescript
|
||||
} else if (effect.type === 'multiplier' && effect.stat && effect.value) {
|
||||
// Multiplier effects multiply together
|
||||
const key = effect.stat;
|
||||
if (!multipliers[key]) {
|
||||
multipliers[key] = 1;
|
||||
}
|
||||
// Each stack applies the multiplier
|
||||
for (let i = 0; i < ench.stacks; i++) {
|
||||
multipliers[key] *= effect.value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** The `enchantmentPower` multiplier would need to be passed into `computeEquipmentEffects()` or applied after the fact in `computeAllEffects()`.
|
||||
|
||||
---
|
||||
|
||||
## Summary of Required Changes for Task 5 (Enchantment Power)
|
||||
|
||||
1. Add `enchantmentPowerMultiplier` field to `ComputedEffects` interface
|
||||
2. Handle `enchantPower` stat in `computeEffects()` switch statement
|
||||
3. Pass `enchantmentPowerMultiplier` to `computeEquipmentEffects()` or apply in `computeAllEffects()`
|
||||
4. Update EquipmentTab.tsx stub to display actual value
|
||||
5. Update StatsTab.tsx to use proper type-safe access for `enchantmentPowerMultiplier`
|
||||
|
||||
---
|
||||
|
||||
## Results (Task 18 Implementation)
|
||||
|
||||
### Implemented Changes
|
||||
|
||||
1. **Added `enchantmentPowerMultiplier` to `ComputedEffects` interface** in `src/lib/game/upgrade-effects.ts`:
|
||||
- Added field `enchantmentPowerMultiplier: number;` to the interface
|
||||
- Initialized to `1` in the `computeEffects()` function
|
||||
|
||||
2. **Handled `enchantPower` stat in `computeEffects()` switch statement** in `src/lib/game/upgrade-effects.ts`:
|
||||
- Added case for `enchantPower` in the multiplier effects switch statement
|
||||
- Multiplier is applied as: `effects.enchantmentPowerMultiplier *= effect.value;`
|
||||
|
||||
3. **Updated `computeEquipmentEffects()` to apply `enchantmentPowerMultiplier`** in `src/lib/game/effects.ts`:
|
||||
- Added optional parameter `enchantmentPowerMultiplier: number = 1.0`
|
||||
- Applied multiplier to both bonus and multiplier effect values:
|
||||
- `const adjustedValue = effect.value * enchantmentPowerMultiplier;`
|
||||
- Used `adjustedValue` instead of `effect.value` when computing enchantment effects
|
||||
|
||||
4. **Updated `computeAllEffects()` to pass the multiplier** in `src/lib/game/effects.ts`:
|
||||
- Now passes `upgradeEffects.enchantmentPowerMultiplier` to `computeEquipmentEffects()`
|
||||
|
||||
5. **Replaced stub in EquipmentTab.tsx**:
|
||||
- Changed from: "Increases the power of all enchantments. Will be wired from Task 5 implementation."
|
||||
- Changed to: "Increases the power of all enchantments by X%. Multiplier applied to all enchantment effects."
|
||||
- Now displays actual `enchantmentPowerMultiplier` value from `getUnifiedEffects(store)`
|
||||
- Added `getUnifiedEffects` import from `@/lib/game/effects`
|
||||
|
||||
6. **Updated StatsTab.tsx to use type-safe access**:
|
||||
- Changed from type cast workaround: `(upgradeEffects as Record<string, number>).enchantPower`
|
||||
- Changed to proper access: `upgradeEffects?.enchantmentPowerMultiplier`
|
||||
- Now displays the actual multiplier value and percentage
|
||||
|
||||
### Audit of Other Unwired Stubs
|
||||
|
||||
1. **AttunementsTab.tsx - Disenchanting TODO (Line 198)**: Logged in `docs/task5.md` under "Known Gaps" - waiting for Bug 13
|
||||
2. **AttunementsTab.tsx - Research TODO (Line 249)**: Logged in `docs/task5.md` under "Known Gaps" - waiting for Bug 12
|
||||
3. **AttunementsTab.tsx.backup - TODOs**: Backup file - ignored
|
||||
4. **skill-evolution.ts - Attunement Requirement Placeholder (Line 2218)**: Logged in `docs/task5.md` under "Known Gaps" - placeholder for future implementation
|
||||
5. **StatsTab.tsx - Enchantment Power**: ✅ Fixed (was already labeled "Wired" but now properly implemented)
|
||||
|
||||
### Files Modified
|
||||
- `src/lib/game/upgrade-effects.ts` - Added `enchantmentPowerMultiplier` field and handler
|
||||
- `src/lib/game/effects.ts` - Updated `computeEquipmentEffects()` and `computeAllEffects()` to apply multiplier
|
||||
- `src/components/game/tabs/EquipmentTab.tsx` - Replaced stub, added import, displays actual value
|
||||
- `src/components/game/tabs/StatsTab.tsx` - Updated to use type-safe access for `enchantmentPowerMultiplier`
|
||||
- `docs/task5.md` - Added "Known Gaps" section documenting unwired stubs
|
||||
|
||||
### Testing Notes
|
||||
- The `enchantmentPowerMultiplier` defaults to `1.0` (no effect)
|
||||
- Skill perks that use `{ type: 'multiplier', stat: 'enchantPower', value: <decimal> }` will now properly affect enchantment power
|
||||
- All enchantment effect values (both bonus and multiplier types) are multiplied by `enchantmentPowerMultiplier`
|
||||
- The multiplier is applied per stack (each stack's effect value is multiplied)
|
||||
|
||||
### Status
|
||||
✅ All tasks completed successfully
|
||||
@@ -0,0 +1,56 @@
|
||||
# Task 19 (5a) Context: New Insight Upgrade Proposals
|
||||
|
||||
## 1. Existing Insight Upgrades
|
||||
Sourced from `docs/GAME_BRIEFING.md` Prestige/Loop System section. These are permanent upgrades purchased with Insight when prestiging (looping), persisting across all subsequent loops.
|
||||
|
||||
| Upgrade | Max Level | Cost (Insight) | Effect |
|
||||
|---------|-----------|----------------|--------|
|
||||
| Mana Well | 5 | 500 | +500 starting max mana |
|
||||
| Mana Flow | 10 | 750 | +0.5 permanent regen per level |
|
||||
| Deep Memory | 5 | 1000 | +1 memory slot per level |
|
||||
| Insight Amp | 4 | 1500 | +25% insight gain per level |
|
||||
| Spire Key | 5 | 4000 | Start at floor +2 per level |
|
||||
| Temporal Echo | 5 | 3000 | +10% mana generation per level |
|
||||
| Steady Hand | 5 | 1200 | -15% durability loss per level |
|
||||
| Ancient Knowledge | 5 | 2000 | Start with blueprint per level |
|
||||
| Elemental Attune | 10 | 600 | +25 element cap per level |
|
||||
| Spell Memory | 3 | 2500 | Start with random spell per level |
|
||||
| Guardian Pact | 5 | 3500 | +10% pact multiplier per level |
|
||||
| Quick Start | 3 | 400 | +100 starting mana per level |
|
||||
| Elem. Start | 3 | 800 | +5 each unlocked element per level |
|
||||
|
||||
*Note: In-game core skills (e.g., `mana` category skills like Mana Well, Mana Flow) are distinct from these Prestige/Insight upgrades, which are purchased with Insight across loops.*
|
||||
|
||||
## 2. Insight Upgrade Philosophy
|
||||
Insight upgrades are **permanent cross-loop advantages that accelerate early-loop ramp-up**. They are designed to:
|
||||
- Persist indefinitely across all loops (permanent)
|
||||
- Apply universally to every new loop (cross-loop)
|
||||
- Reduce the time/effort required to reach mid/late game in each subsequent loop (accelerate early-loop ramp-up)
|
||||
|
||||
Examples of existing upgrades aligning with this philosophy:
|
||||
- `Quick Start`: Grants starting mana to immediately begin actions
|
||||
- `Elem. Start`: Unlocks elemental mana types earlier, bypassing early-game grinds
|
||||
- `Spire Key`: Skips low-level floors to reach higher content faster
|
||||
|
||||
## 3. Specific Proposal Directions (Task 19 / 5a)
|
||||
New Insight upgrade proposals to expand early-loop acceleration options:
|
||||
|
||||
1. **Unlocked mana type capacity**
|
||||
- Effect: Start with access to unlocked mana types + small capacity bonus
|
||||
- Goal: Reduce early-game mana type unlock grind, provide immediate elemental mana access
|
||||
|
||||
2. **Reduced attunement conversion cost at loop start**
|
||||
- Effect: Lower raw mana cost for converting to elemental mana via attunements early in the loop
|
||||
- Goal: Speed up elemental mana accumulation in first few days of each loop
|
||||
|
||||
3. **Start with N floors of Spire progress cleared**
|
||||
- Effect: Begin each loop with N floors of Spire progress already completed (similar to Spire Key, but more granular)
|
||||
- Goal: Skip repetitive early floor climbs, focus on higher content sooner
|
||||
|
||||
4. **Guardian Pact memory**
|
||||
- Effect: Retain one pact bonus (from a previously signed Guardian) across loops
|
||||
- Goal: Maintain powerful Guardian boons without re-signing pacts every loop
|
||||
|
||||
5. **Skill head-start**
|
||||
- Effect: Start each loop with 1 level in a chosen skill category (e.g., mana, study, enchant)
|
||||
- Goal: Reduce early-game skill grind, immediately access core skill bonuses
|
||||
@@ -0,0 +1,602 @@
|
||||
# Context: Task5 (2a Floor Rendering & Identity)
|
||||
|
||||
## Floor Type Definitions
|
||||
|
||||
### Room Types (from `src/lib/game/constants/rooms.ts` and `src/lib/game/types/game.ts`)
|
||||
```typescript
|
||||
// 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
|
||||
```
|
||||
|
||||
### Swarm Room Configuration
|
||||
```typescript
|
||||
// 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
|
||||
```typescript
|
||||
// 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
|
||||
};
|
||||
```
|
||||
|
||||
### Floor Armor Configuration
|
||||
```typescript
|
||||
// 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
|
||||
};
|
||||
```
|
||||
|
||||
### Puzzle Room Definitions
|
||||
```typescript
|
||||
// Puzzle room definitions - themed around attunements
|
||||
export const PUZZLE_ROOMS: Record<string, {
|
||||
name: string;
|
||||
attunements: string[];
|
||||
baseProgressPerTick: number;
|
||||
attunementBonus: number;
|
||||
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 rooms also defined
|
||||
};
|
||||
```
|
||||
|
||||
### Guardian Definitions (from `src/lib/game/constants/guardians.ts`)
|
||||
```typescript
|
||||
// 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' },
|
||||
],
|
||||
pactCost: 500,
|
||||
pactTime: 2,
|
||||
uniquePerk: "Fire spells cast 10% faster"
|
||||
},
|
||||
20: { name: "Aqua Regia", element: "water", hp: 15000, pact: 1.75, color: "#4ECDC4", armor: 0.15, ... },
|
||||
30: { name: "Ventus Rex", element: "air", hp: 30000, pact: 2.0, color: "#00D4FF", armor: 0.18, ... },
|
||||
40: { name: "Terra Firma", element: "earth", hp: 50000, pact: 2.25, color: "#F4A261", armor: 0.25, ... },
|
||||
50: { name: "Lux Aeterna", element: "light", hp: 80000, pact: 2.5, color: "#FFD700", armor: 0.20, ... },
|
||||
60: { name: "Umbra Mortis", element: "dark", hp: 120000, pact: 2.75, color: "#9B59B6", armor: 0.22, ... },
|
||||
80: { name: "Mors Ultima", element: "death", hp: 250000, pact: 3.25, color: "#778CA3", armor: 0.25, ... },
|
||||
90: { name: "Primordialis", element: "void", hp: 400000, pact: 4.0, color: "#4A235A", armor: 0.30, ... },
|
||||
100: { name: "The Awakened One", element: "stellar", hp: 1000000, pact: 5.0, color: "#F0E68C", armor: 0.35, ... },
|
||||
};
|
||||
```
|
||||
|
||||
### GuardianDef Type (from `src/lib/game/types/attunements.ts`)
|
||||
```typescript
|
||||
export interface GuardianDef {
|
||||
name: string;
|
||||
element: string;
|
||||
hp: number;
|
||||
pact: number; // Pact multiplier when signed
|
||||
color: string;
|
||||
boons: GuardianBoon[]; // Bonuses granted when pact is signed
|
||||
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)
|
||||
}
|
||||
|
||||
export interface GuardianBoon {
|
||||
type: 'maxMana' | 'manaRegen' | 'castingSpeed' | 'elementalDamage' | 'rawDamage' |
|
||||
'critChance' | 'critDamage' | 'spellEfficiency' | 'manaGain' | 'insightGain' |
|
||||
'studySpeed' | 'prestigeInsight';
|
||||
value: number;
|
||||
desc: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Element Definitions (from `src/lib/game/constants/elements.ts`)
|
||||
```typescript
|
||||
export const ELEMENTS: Record<string, ElementDef> = {
|
||||
// Base Elements
|
||||
fire: { name: "Fire", sym: "🔥", color: "#FF6B35", glow: "#FF6B3540", cat: "base" },
|
||||
water: { name: "Water", sym: "💧", color: "#4ECDC4", glow: "#4ECDC440", cat: "base" },
|
||||
air: { name: "Air", sym: "🌬️", color: "#00D4FF", glow: "#00D4FF40", cat: "base" },
|
||||
earth: { name: "Earth", sym: "⛰️", color: "#F4A261", glow: "#F4A26140", cat: "base" },
|
||||
light: { name: "Light", sym: "☀️", color: "#FFD700", glow: "#FFD70040", cat: "base" },
|
||||
dark: { name: "Dark", sym: "🌑", color: "#9B59B6", glow: "#9B59B640", cat: "base" },
|
||||
death: { name: "Death", sym: "💀", color: "#778CA3", glow: "#778CA340", cat: "base" },
|
||||
// ... other elements
|
||||
};
|
||||
|
||||
export const FLOOR_ELEM_CYCLE = ["fire", "water", "air", "earth", "light", "dark", "death"];
|
||||
```
|
||||
|
||||
### Room Type Labels (from `src/lib/game/constants/index.ts`)
|
||||
```typescript
|
||||
export const ROOM_TYPE_LABELS: Record<string, { label: string; icon: string; color: string }> = {
|
||||
combat: { label: 'Combat', icon: '⚔️', color: '#EF4444' },
|
||||
swarm: { label: 'Swarm', icon: '🐝', color: '#F59E0B' },
|
||||
speed: { label: 'Speed', icon: '💨', color: '#3B82F6' },
|
||||
guardian: { label: 'Guardian', icon: '🛡️', color: '#EF4444' },
|
||||
puzzle: { label: 'Puzzle', icon: '🧩', color: '#8B5CF6' },
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Current Floor Rendering Code
|
||||
|
||||
### SpireTab.tsx (from `src/components/game/tabs/SpireTab.tsx`)
|
||||
|
||||
#### Room Type Display Configuration
|
||||
```typescript
|
||||
// Room type configurations for display
|
||||
const ROOM_TYPE_CONFIG: Record<string, { label: string; icon: string; color: string }> = {
|
||||
combat: { label: 'Combat', icon: '⚔️', color: '#EF4444' },
|
||||
swarm: { label: 'Swarm', icon: '🐝', color: '#F59E0B' },
|
||||
speed: { label: 'Speed', icon: '💨', color: '#3B82F6' },
|
||||
guardian: { label: 'Guardian', icon: '🛡️', color: '#EF4444' },
|
||||
puzzle: { label: 'Puzzle', icon: '🧩', color: '#8B5CF6' },
|
||||
};
|
||||
```
|
||||
|
||||
#### Floor Type Badge Rendering
|
||||
```tsx
|
||||
<Badge
|
||||
className="ml-2"
|
||||
style={{
|
||||
backgroundColor: `${roomConfig.color}20`,
|
||||
color: roomConfig.color,
|
||||
borderColor: `${roomConfig.color}60`
|
||||
}}
|
||||
>
|
||||
{roomConfig.icon} {roomConfig.label}
|
||||
</Badge>
|
||||
```
|
||||
|
||||
#### Guardian Name Display
|
||||
```tsx
|
||||
{isGuardianFloor && currentGuardian && (
|
||||
<div className="text-sm font-semibold game-panel-title" style={{ color: floorElemDef?.color }}>
|
||||
⚔️ {currentGuardian.name}
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
#### Single Enemy Display (Combat/Speed/Guardian)
|
||||
```tsx
|
||||
{!isGuardianFloor && primaryEnemy && roomType !== 'swarm' && (
|
||||
<div className="p-3 bg-gray-800/50 rounded border border-gray-700">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skull className="w-4 h-4 text-red-400" />
|
||||
<span className="text-sm font-semibold text-gray-200">
|
||||
{primaryEnemy.name || 'Unknown Enemy'}
|
||||
</span>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{ELEMENTS[primaryEnemy.element]?.sym} {ELEMENTS[primaryEnemy.element]?.name}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Enemy HP Bar */}
|
||||
<div className="space-y-1 mb-2">
|
||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.max(0, (primaryEnemy.hp / primaryEnemy.maxHP) * 100)}%`,
|
||||
background: `linear-gradient(90deg, ${floorElemDef?.color}99, ${floorElemDef?.color})`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-gray-400 game-mono">
|
||||
<span>{fmt(primaryEnemy.hp)} / {fmt(primaryEnemy.maxHP)} HP</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enemy Properties */}
|
||||
<div className="flex flex-wrap gap-2 text-xs">
|
||||
{primaryEnemy.armor > 0 && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Badge variant="outline" className="text-xs py-0">
|
||||
<Shield className="w-3 h-3 mr-1" />
|
||||
{(primaryEnemy.armor * 100).toFixed(0)}% Armor
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Reduces incoming damage by {(primaryEnemy.armor * 100).toFixed(0)}%</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{primaryEnemy.dodgeChance > 0 && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Badge variant="outline" className="text-xs py-0">
|
||||
<Wind className="w-3 h-3 mr-1" />
|
||||
{(primaryEnemy.dodgeChance * 100).toFixed(0)}% Dodge
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Chance to dodge attacks and reduce progress</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
#### Swarm Enemies Display
|
||||
```tsx
|
||||
{roomType === 'swarm' && swarmEnemies.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-gray-400 font-semibold">
|
||||
Swarm Enemies ({swarmEnemies.length})
|
||||
</div>
|
||||
{swarmEnemies.map((enemy, index) => (
|
||||
<div key={enemy.id || `swarm-${index}`} className="p-2 bg-gray-800/50 rounded border border-gray-700">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skull className="w-3 h-3 text-red-400" />
|
||||
<span className="text-xs font-semibold text-gray-300">
|
||||
{enemy.name || `Enemy ${index + 1}`}
|
||||
</span>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs py-0">
|
||||
{ELEMENTS[enemy.element]?.sym} {fmt(enemy.hp)}/{fmt(enemy.maxHP)} HP
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="h-1.5 bg-gray-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.max(0, (enemy.hp / enemy.maxHP) * 100)}%`,
|
||||
background: `linear-gradient(90deg, ${ELEMENTS[enemy.element]?.color}99, ${ELEMENTS[enemy.element]?.color})`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
#### Puzzle Room Display
|
||||
```tsx
|
||||
{roomType === 'puzzle' && (
|
||||
<div className="p-3 bg-purple-900/20 rounded border border-purple-700">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-lg">🧩</span>
|
||||
<span className="text-sm font-semibold text-purple-300">
|
||||
{currentRoom.puzzleId ? currentRoom.puzzleId.replace(/_/g, ' ').toUpperCase() : 'Puzzle Room'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs text-gray-400">
|
||||
<span>Progress</span>
|
||||
<span>{((currentRoom.puzzleProgress || 0) * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={Math.min(100, (currentRoom.puzzleProgress || 0) * 100)}
|
||||
className="h-2 bg-gray-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Enemy Naming Logic
|
||||
|
||||
### Enemy Name Generation (from `src/lib/game/store.ts`)
|
||||
|
||||
```typescript
|
||||
// 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!];
|
||||
}
|
||||
```
|
||||
|
||||
### Enemy State Type (from `src/lib/game/types/game.ts`)
|
||||
```typescript
|
||||
export interface EnemyState {
|
||||
id: string;
|
||||
name: string; // Display name for the enemy
|
||||
hp: number;
|
||||
maxHP: number;
|
||||
armor: number; // Damage reduction (0-1)
|
||||
dodgeChance: number; // For speed rooms (0-1)
|
||||
element: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Floor State Type
|
||||
```typescript
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
### Floor Generation Functions (from `src/lib/game/store.ts`)
|
||||
|
||||
```typescript
|
||||
// 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++) {
|
||||
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,
|
||||
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',
|
||||
name: guardian.name,
|
||||
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': {
|
||||
const speedEnemyName = getEnemyName(element, floor);
|
||||
return {
|
||||
roomType: 'speed',
|
||||
enemies: [{
|
||||
id: 'speed_enemy',
|
||||
name: speedEnemyName,
|
||||
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
|
||||
const combatEnemyName = getEnemyName(element, floor);
|
||||
return {
|
||||
roomType: 'combat',
|
||||
enemies: [{
|
||||
id: 'enemy',
|
||||
name: combatEnemyName,
|
||||
hp: baseHP,
|
||||
maxHP: baseHP,
|
||||
armor: getFloorArmor(floor),
|
||||
dodgeChance: 0,
|
||||
element,
|
||||
}],
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Special Floor Properties
|
||||
|
||||
### Currently Implemented Properties
|
||||
|
||||
#### Armor (Damage Reduction)
|
||||
- **Guardian floors**: Defined in `GUARDIANS[floor].armor` (0.10 to 0.35)
|
||||
- **Non-guardian floors**: Randomly generated via `getFloorArmor(floor)` using `FLOOR_ARMOR_CONFIG`
|
||||
- **Swarm enemies**: `SWARM_CONFIG.armorBase + Math.floor(floor / 10) * SWARM_CONFIG.armorPerFloor`
|
||||
- Displayed in UI with shield icon and percentage
|
||||
|
||||
#### Dodge Chance
|
||||
- **Speed rooms only**: Generated via `getDodgeChance(floor)` using `SPEED_ROOM_CONFIG`
|
||||
- Base: 25%, scales +0.5% per floor, max 50%
|
||||
- Displayed in UI with wind icon and percentage
|
||||
|
||||
#### Health/HP
|
||||
- **Guardian floors**: `GUARDIANS[floor].hp` (5000 to 1000000)
|
||||
- **Normal floors**: `getFloorMaxHP(floor)` - scales with floor number
|
||||
- **Swarm enemies**: `baseHP * SWARM_CONFIG.hpMultiplier` (40% of normal)
|
||||
|
||||
### Properties Mentioned in Task But Not Currently in Floor Config
|
||||
- **healthRegen**: Not currently implemented as a floor/enemy property (only exists in guardian boons as `manaRegen` for player)
|
||||
- **barrier**: Not currently implemented as a floor/enemy property (only exists as attunement mana type)
|
||||
|
||||
Note: The task mentions displaying "Special floor properties (armor%, health regen, barrier, dodge)" but `healthRegen` and `barrier` are not currently implemented in the floor config. These may need to be added as part of this task.
|
||||
|
||||
---
|
||||
|
||||
## File Paths
|
||||
|
||||
### Key Files for Task 5 (2a Floor Rendering & Identity)
|
||||
|
||||
1. **Floor/Room Type Definitions**:
|
||||
- `/home/user/repos/Mana-Loop/src/lib/game/constants/rooms.ts` - Room types, swarm/speed config, armor config
|
||||
- `/home/user/repos/Mana-Loop/src/lib/game/constants/guardians.ts` - Guardian definitions with names, HP, armor
|
||||
- `/home/user/repos/Mana-Loop/src/lib/game/constants/elements.ts` - Element definitions with symbols and colors
|
||||
- `/home/user/repos/Mana-Loop/src/lib/game/constants/index.ts` - ROOM_TYPE_LABELS export
|
||||
|
||||
2. **Type Definitions**:
|
||||
- `/home/user/repos/Mana-Loop/src/lib/game/types/game.ts` - RoomType, EnemyState, FloorState interfaces
|
||||
- `/home/user/repos/Mana-Loop/src/lib/game/types/attunements.ts` - GuardianDef, GuardianBoon interfaces
|
||||
|
||||
3. **Floor Rendering UI**:
|
||||
- `/home/user/repos/Mana-Loop/src/components/game/tabs/SpireTab.tsx` - Main floor rendering component with enemy display, room type badges, armor/dodge tooltips
|
||||
|
||||
4. **Floor Generation Logic**:
|
||||
- `/home/user/repos/Mana-Loop/src/lib/game/store.ts` - `getEnemyName()`, `generateRoomType()`, `generateFloorState()`, `getFloorArmor()`, `getDodgeChance()`, `generateSwarmEnemies()`
|
||||
|
||||
5. **Element Cycle for Floors**:
|
||||
- `/home/user/repos/Mana-Loop/src/lib/game/store.ts` - `getFloorElement()`, `getFloorMaxHP()`
|
||||
- `/home/user/repos/Mana-Loop/src/lib/game/constants/elements.ts` - `FLOOR_ELEM_CYCLE`
|
||||
@@ -0,0 +1,19 @@
|
||||
# Context: Task 6 (Insight Proposal Revision)
|
||||
## Current Proposal File
|
||||
`docs/task5_insight_proposals.md` (95 lines, read earlier)
|
||||
|
||||
## User Feedback (Accepted/Rejected)
|
||||
1. **Proposal 1 (Unlocked Mana Type Capacity)**: Accepted
|
||||
- Revision: Only 1 mana type per purchase (not instant access to all previously unlocked types)
|
||||
2. **Proposal 2 (Attunement Efficiency)**: Accepted as-is
|
||||
3. **Proposal 3 (Spire Progress Retention)**: Accepted as-is
|
||||
4. **Proposal 4 (Guardian Pact Memory)**: Accepted
|
||||
- Revision: Retain the *entire* chosen guardian pact (not reduced strength)
|
||||
5. **Proposal 5 (Skill Head-Start)**: Rejected, remove from proposal
|
||||
|
||||
## Revision Requirements
|
||||
- Update Proposal 1 effect: "Start each loop with +10 base capacity for 1 selected mana type per level, unlocked type selectable during prestige"
|
||||
- Update Proposal 4 effect: "Retain entire chosen Guardian pact bonus across loops, no re-signing required"
|
||||
- Remove Proposal 5 entirely
|
||||
- Keep Proposal 2 & 3 unchanged
|
||||
- Maintain original document structure (themes, tables, implementation notes)
|
||||
@@ -0,0 +1,124 @@
|
||||
# Task 9: Fix Climb/Descend Controls - Context Summary
|
||||
|
||||
**Status:** Partially done (spam prevention and re-entry resume implemented, button rename incomplete)
|
||||
|
||||
## 1. Climbing/Descending State in store.ts
|
||||
|
||||
### State Variables (lines 820, 2260):
|
||||
- `spireMode: boolean` - Whether player is in Spire Mode
|
||||
- `clearedFloors: Record<number, boolean>` - Tracks cleared floors for respawning
|
||||
- `climbDirection: 'up' | 'down' | null` - Current climb direction (persisted for re-entry)
|
||||
- `isDescending: boolean` - True when actively descending (prevents spam clicking)
|
||||
|
||||
### Key Actions:
|
||||
- `enterSpireMode()` (line 2253): Sets `spireMode: true`, `currentAction: 'climb'`, `isDescending: false`
|
||||
- `climbDownFloor()` (line 2267):
|
||||
- Checks `isDescending` to prevent spam
|
||||
- Decrements floor by 1 (min floor 1)
|
||||
- Sets `isDescending: true` during descent
|
||||
- Uses `setTimeout` to reset `isDescending: false` after 500ms
|
||||
- Clears/resets floor state in `clearedFloors`
|
||||
- `exitSpireMode()` (line 2311): Sets `spireMode: false`, `currentAction: 'meditate'`, `isDescending: false`
|
||||
|
||||
### Spam Prevention (COMPLETED):
|
||||
- `isDescending` flag prevents multiple rapid clicks
|
||||
- Button is disabled when `isDescending` is true
|
||||
- 500ms timeout resets the flag after descent completes
|
||||
|
||||
### Re-entry Resume (COMPLETED):
|
||||
- `climbDirection` is persisted in state
|
||||
- `enterSpireMode()` resumes from `climbDirection` state
|
||||
- `exitSpireMode()` allows exit at any floor, re-entry resumes at same floor
|
||||
|
||||
## 2. Buttons in SpireTab Components and page.tsx
|
||||
|
||||
### src/app/page.tsx (Spire Mode UI) - Lines 258-278:
|
||||
|
||||
**Climbing Indicator Badge (line 263-266):**
|
||||
```tsx
|
||||
{store.currentAction === 'climb' && !store.isDescending && (
|
||||
<Badge className="bg-green-900/50 text-green-300 border-green-600">
|
||||
Climbing
|
||||
</Badge>
|
||||
)}
|
||||
```
|
||||
|
||||
**Descend/Climb Button (lines 267-278):**
|
||||
```tsx
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-blue-600/50 text-blue-400 hover:bg-blue-900/20"
|
||||
onClick={() => store.climbDownFloor()}
|
||||
disabled={store.isDescending}
|
||||
>
|
||||
<ChevronDown className="w-4 h-4 mr-2" />
|
||||
{store.isDescending ? 'Descending…' :
|
||||
store.currentAction === 'climb' ? 'Climbing' :
|
||||
'Begin Descent'}
|
||||
</Button>
|
||||
```
|
||||
|
||||
**Current Button Label Logic:**
|
||||
- When `isDescending` is true: Shows "Descending…" (with ellipsis) and button is disabled
|
||||
- When `currentAction === 'climb'`: Shows "Climbing"
|
||||
- Otherwise: Shows "Begin Descent"
|
||||
|
||||
**Enter Spire Mode Button (lines 211-221):**
|
||||
```tsx
|
||||
<Button
|
||||
className="w-full bg-gradient-to-r from-amber-600 to-orange-600..."
|
||||
onClick={() => store.enterSpireMode()}
|
||||
disabled={!canEnterSpireMode(store)}
|
||||
>
|
||||
<Mountain className="w-5 h-5 mr-2" />
|
||||
Climb the Spire
|
||||
</Button>
|
||||
```
|
||||
|
||||
### src/components/game/tabs/SpireTab.tsx:
|
||||
- **No climb/descend buttons** - This component only displays floor info, spells, golems, and activity log
|
||||
- Has "Enter Spire Mode" button (line 76) with label "Enter Spire Mode" (for non-Spire Mode view)
|
||||
- Displays floor information, active spells, golems, and activity log in `simpleMode={true}`
|
||||
|
||||
## 3. What Needs to Change for Button Rename
|
||||
|
||||
**Requirement:** idle: 'Begin Descent', descending: 'Descending' disabled, climbing: 'Climbing'
|
||||
|
||||
**Current Issues:**
|
||||
|
||||
1. **"Descending…" vs "Descending"**: The button shows "Descending…" (with ellipsis) when descending, but requirement says "Descending" (without ellipsis)
|
||||
|
||||
2. **Button label when climbing**: The current logic shows "Climbing" when `currentAction === 'climb'`, but this is confusing because:
|
||||
- The button's action is to descend (calls `climbDownFloor()`)
|
||||
- The "Climbing" Badge already serves as a separate indicator
|
||||
- When climbing, users may want to descend, so the button should probably say "Begin Descent"
|
||||
|
||||
3. **Possible correct implementation:**
|
||||
- Remove the `currentAction === 'climb'` check from button label
|
||||
- Button should always show "Begin Descent" when not descending
|
||||
- Button shows "Descending" (disabled) when descending
|
||||
- Keep the separate "Climbing" Badge as a status indicator
|
||||
|
||||
**Suggested Button Code Fix (in page.tsx, lines 274-277):**
|
||||
```tsx
|
||||
{store.isDescending ? 'Descending' : 'Begin Descent'}
|
||||
```
|
||||
(Remove the `store.currentAction === 'climb' ? 'Climbing' : 'Begin Descent'` part)
|
||||
|
||||
## 4. Summary of Files to Modify
|
||||
|
||||
| File | Change Needed |
|
||||
|------|---------------|
|
||||
| `src/app/page.tsx` | Fix button label logic (lines 274-277) to match requirements |
|
||||
| `src/lib/game/store.ts` | No changes needed (spam prevention and re-entry resume implemented) |
|
||||
| `src/components/game/tabs/SpireTab.tsx` | No changes needed (no climb/descend buttons) |
|
||||
|
||||
## 5. Verification Steps
|
||||
|
||||
After making changes:
|
||||
1. Test spam prevention: Rapidly click descend button - should only descend once per 500ms
|
||||
2. Test re-entry resume: Exit Spire Mode at floor X, re-enter - should resume at floor X
|
||||
3. Test button labels:
|
||||
- Idle (not climbing, not descending): Shows "Begin Descent"
|
||||
- Descending: Shows "Descending" (disabled)
|
||||
- Climbing: The separate Badge shows "Climbing", button shows "Begin Descent"
|
||||
Reference in New Issue
Block a user