diff --git a/.zscripts/dev.pid b/.zscripts/dev.pid index 19a3842..3bfc51a 100755 --- a/.zscripts/dev.pid +++ b/.zscripts/dev.pid @@ -1 +1 @@ -563 +562 diff --git a/docs/GAME_BRIEFING.md b/docs/GAME_BRIEFING.md new file mode 100644 index 0000000..e62a042 --- /dev/null +++ b/docs/GAME_BRIEFING.md @@ -0,0 +1,927 @@ +# Mana-Loop: Comprehensive Game Briefing Document + +**Document Version:** 1.0 +**Generated:** Game Systems Analysis + +--- + +## Table of Contents + +1. [Executive Summary](#executive-summary) +2. [Core Game Loop](#core-game-loop) +3. [Mana System](#mana-system) +4. [Time & Incursion System](#time--incursion-system) +5. [Spire & Floor System](#spire--floor-system) +6. [Combat System](#combat-system) +7. [Guardian & Pact System](#guardian--pact-system) +8. [Attunement System](#attunement-system) +9. [Skill System](#skill-system) +10. [Equipment & Enchantment System](#equipment--enchantment-system) +11. [Golemancy System](#golemancy-system) +12. [Prestige/Loop System](#prestigeloop-system) +13. [Achievement System](#achievement-system) +14. [Formulas & Calculations](#formulas--calculations) +15. [System Interactions](#system-interactions) + +--- + +## Executive Summary + +**Mana-Loop** is a browser-based incremental/idle game with a 30-day time loop mechanic. Players gather mana, study skills, climb a 100-floor spire, defeat guardians, sign pacts, enchant equipment, and prestige for permanent progression. + +**Key Differentiators:** +- 3-class Attunement system (Enchanter, Invoker, Fabricator) +- Equipment-based spell system (spells come from enchanted gear) +- 5-tier skill evolution with milestone upgrade choices +- Time pressure through incursion mechanic +- Guardian pacts provide permanent multipliers + +--- + +## Core Game Loop + +### Primary Loop (Within Each Run) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ TIME LOOP (30 Days) │ +├─────────────────────────────────────────────────────────────┤ +│ ┌─────────┐ ┌──────────┐ ┌───────────┐ ┌───────┐ │ +│ │ GATHER │───▶│ STUDY │───▶│ CLIMB │───▶│ CRAFT │ │ +│ │ MANA │ │ SKILLS │ │ SPIRE │ │ GEAR │ │ +│ └─────────┘ └──────────┘ └───────────┘ └───────┘ │ +│ │ │ │ │ │ +│ ▼ ▼ ▼ ▼ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ DEFEAT GUARDIANS → SIGN PACTS │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ DAY 30: LOOP ENDS → GAIN INSIGHT │ │ +│ └─────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Game Actions + +| Action | Effect | Time Cost | +|--------|--------|-----------| +| **Meditate** | Regen mana with meditation bonus multiplier | Passive | +| **Climb** | Progress through spire floors, cast spells | Active combat | +| **Study** | Learn skills/spells, requires mana cost | Hours per level | +| **Craft** | Create/enchant equipment | Hours per stage | +| **Convert** | Auto-convert raw mana to elements | Per tick | +| **Design** | Create enchantment designs | Hours | +| **Prepare** | Ready equipment for enchanting | Hours + mana | +| **Enchant** | Apply enchantment to equipment | Hours + mana | + +--- + +## Mana System + +### Mana Types Hierarchy + +``` +Raw Mana (Base Resource) + │ + ├──▶ Base Elements (7) ─────────────────────────────────────┐ + │ Fire, Water, Air, Earth, Light, Dark, Death │ + │ │ + ├──▶ Utility Element (1) ───────────────────────────────────┤ + │ Transference (Enchanter attunement) │ + │ │ + ├──▶ Compound Elements (3) ── Created from 2 base ──────────┤ + │ Metal = Fire + Earth │ + │ Sand = Earth + Water │ + │ Lightning = Fire + Air │ + │ │ + └──▶ Exotic Elements (3) ── Created from advanced recipes ──┤ + Crystal = Sand + Sand + Light │ + Stellar = Fire + Fire + Light │ + Void = Dark + Dark + Death │ +``` + +### Mana Formulas + +**Maximum Raw Mana:** +``` +maxMana = (100 + (manaWellLevel × 100) + (prestigeManaWell × 500) + equipmentBonuses) × maxManaMultiplier +``` + +**Maximum Elemental Mana:** +``` +elementMax = (10 + (elemAttuneLevel × 50) + (prestigeElemAttune × 25)) × elementCapMultiplier +``` + +**Base Regeneration (per hour):** +``` +baseRegen = 2 + (manaFlowLevel × 1) + (manaSpringLevel × 2) + (prestigeManaFlow × 0.5) +effectiveRegen = baseRegen × (1 - incursionStrength) × meditationMultiplier +``` + +**Meditation Bonus:** +``` +Base: 1 + min(hours/4, 0.5) = up to 1.5x after 4 hours +With Meditation Focus skill: 2.5x after 4 hours +With Deep Trance skill: 3.0x after 6 hours +With Void Meditation skill: 5.0x after 8 hours +``` + +**Attunement Mana Conversion:** +- Enchanter: Raw → Transference at 0.2/hour base (scales with level) +- Fabricator: Raw → Earth at 0.25/hour base (scales with level) + +### Mana Conversion Cost +- **100 Raw Mana = 1 Elemental Mana** (for base elements) +- Compound/Exotic elements are crafted, not converted + +--- + +## Time & Incursion System + +### Time Constants + +| Constant | Value | Description | +|----------|-------|-------------| +| `TICK_MS` | 200ms | Real time per game tick | +| `HOURS_PER_TICK` | 0.04 | Game hours per tick | +| `MAX_DAY` | 30 | Days per loop | +| `INCURSION_START_DAY` | 20 | When incursion begins | + +### Time Progression +- 1 real second = 5 game hours (at 5 ticks/second) +- 1 game day = 24 game hours = 4.8 real seconds +- Full 30-day loop ≈ 2.4 real minutes + +### Incursion Mechanic + +**Incursion Strength Formula:** +``` +if (day < 20): incursionStrength = 0 +else: incursionStrength = min(0.95, (totalHours / maxHours) × 0.95) +``` + +**Effects:** +- Reduces mana regeneration by `(1 - incursionStrength)` +- Starts at 0% on Day 20, reaches 95% by Day 30 +- Creates urgency to complete objectives before loop ends + +--- + +## Spire & Floor System + +### Floor Generation + +**Floor Element Cycle:** +```javascript +FLOOR_ELEM_CYCLE = ["fire", "water", "air", "earth", "light", "dark", "death"] +element = FLOOR_ELEM_CYCLE[(floor - 1) % 7] +``` + +**Floor HP Formula:** +``` +normalFloorHP = floor(100 + floor × 50 + floor^1.7) +guardianFloorHP = GUARDIANS[floor].hp // Fixed values +``` + +**Guardian Floors:** 10, 20, 30, 40, 50, 60, 80, 90, 100 + +### Room Types + +| Room Type | Chance | Description | +|-----------|--------|-------------| +| **Combat** | Default | Single enemy, normal combat | +| **Guardian** | Fixed | Boss floor, always on guardian floors | +| **Swarm** | 15% | 3-6 enemies with 40% HP each | +| **Speed** | 10% | Enemy with dodge chance (25% base + 0.5%/floor) | +| **Puzzle** | 20% on puzzle floors | Progress-based, faster with relevant attunement | + +### Swarm Room Configuration +```javascript +SWARM_CONFIG = { + minEnemies: 3, + maxEnemies: 6, + hpMultiplier: 0.4, // Each enemy has 40% of normal HP + armorBase: 0, + armorPerFloor: 0.01 // +1% armor per 10 floors +} +``` + +### Speed Room Configuration +```javascript +SPEED_ROOM_CONFIG = { + baseDodgeChance: 0.25, // 25% base + dodgePerFloor: 0.005, // +0.5% per floor + maxDodge: 0.50 // Cap at 50% +} +``` + +### Armor Scaling +```javascript +FLOOR_ARMOR_CONFIG = { + baseChance: 0, // No armor before floor 10 + chancePerFloor: 0.01, // +1% chance per floor after 10 + maxArmorChance: 0.5, // Max 50% of floors have armor + minArmor: 0.05, // Min 5% damage reduction + maxArmor: 0.25 // Max 25% on non-guardians +} +``` + +--- + +## Combat System + +### Spell Casting Mechanics + +**Cast Progress:** +``` +progressPerTick = HOURS_PER_TICK × spellCastSpeed × totalAttackSpeed +``` + +**Spell Cast Speed:** +- Each spell has a `castSpeed` value (casts per hour) +- Higher = faster casting +- Lightning spells get +30% effective cast speed + +**Damage Calculation:** +``` +baseDamage = spellDamage + (combatTrainLevel × 5) +pctBonus = 1 + (arcaneFuryLevel × 0.1) +elemMastery = 1 + (elementalMasteryLevel × 0.15) +pactMultiplier = product of all signed pact multipliers +elementalBonus = getElementalBonus(spellElement, floorElement) + +finalDamage = baseDamage × pctBonus × pactMultiplier × elemMastery × elementalBonus +``` + +### Elemental Effectiveness + +| Condition | Multiplier | +|-----------|------------| +| Spell same element as floor | 1.25x (25% bonus) | +| Spell is opposite of floor element | 1.50x (Super Effective) | +| Spell's opposite matches floor | 0.75x (Not Very Effective) | +| Raw mana spells | 1.00x (No bonus) | + +**Element Opposites:** +``` +Fire ↔ Water +Air ↔ Earth +Light ↔ Dark +Lightning ↔ (none, but has armor pierce) +``` + +### Armor & Damage Reduction + +**Effective Damage:** +``` +effectiveArmor = max(0, enemyArmor - armorPierce) +damageDealt = damage × (1 - effectiveArmor) +``` + +**Armor Pierce Sources:** +- Lightning spells: 20-50% pierce +- Metal spells: 25-60% pierce +- Golems: 15-60% pierce + +### Critical Hits + +``` +critChance = precisionLevel × 0.05 +critMultiplier = 1.5 (base) +``` + +### AOE Mechanics + +``` +aoeDamage = baseDamage × (1 - 0.1 × (numTargets - 1)) +// 10% less damage per additional target +``` + +### Special Combat Effects + +| Effect | Description | +|--------|-------------| +| **Burn** | Damage over time | +| **Freeze** | Prevents dodge (100% freeze = no dodge) | +| **Stun** | Temporary disable | +| **Pierce** | Ignores percentage of armor | +| **Chain** | Hits multiple targets | +| **AOE** | Area damage | +| **Buff** | Damage multiplier | + +--- + +## Guardian & Pact System + +### Guardian Floors & Stats + +| Floor | Guardian | Element | HP | Pact Mult | Armor | Unique Perk | +|-------|----------|---------|-----|-----------|-------|-------------| +| 10 | Ignis Prime | Fire | 5,000 | 1.5x | 10% | Fire spells cast 10% faster | +| 20 | Aqua Regia | Water | 15,000 | 1.75x | 15% | Water spells +15% damage | +| 30 | Ventus Rex | Air | 30,000 | 2.0x | 18% | Air spells 15% crit chance | +| 40 | Terra Firma | Earth | 50,000 | 2.25x | 25% | Earth spells +25% vs guardians | +| 50 | Lux Aeterna | Light | 80,000 | 2.5x | 20% | Light spells reveal weaknesses (+20% dmg) | +| 60 | Umbra Mortis | Dark | 120,000 | 2.75x | 22% | Dark spells +25% vs armored | +| 80 | Mors Ultima | Death | 250,000 | 3.25x | 25% | Death spells execute <20% HP | +| 90 | Primordialis | Void | 400,000 | 4.0x | 30% | Void spells ignore 30% resistance | +| 100 | The Awakened One | Stellar | 1,000,000 | 5.0x | 35% | All spells +50% dmg, 25% faster | + +### Guardian Boons + +When a pact is signed, the guardian grants permanent boons: + +| Boon Type | Effect | Example | +|-----------|--------|---------| +| `maxMana` | +Max raw mana | +50 to +500 | +| `manaRegen` | +Regen/hour | +0.5 to +2 | +| `castingSpeed` | +% cast speed | +5% | +| `elementalDamage` | +% element damage | +5% to +20% | +| `rawDamage` | +% all damage | +10% | +| `critChance` | +% crit chance | N/A | +| `critDamage` | +% crit multiplier | +15% | +| `insightGain` | +% insight | +10% to +25% | + +### Pact Ritual + +**Pact Costs:** +```javascript +pactCost: 500 to 150,000 (raw mana) +pactTime: 2 to 24 (hours for ritual) +``` + +**Victory Condition:** +- Defeat floor 100 guardian AND sign the pact +- Awards 3x normal insight + +--- + +## Attunement System + +### Overview + +Attunements are class-like specializations that grant unique capabilities and skill access. + +### The Three Attunements + +#### 1. Enchanter (Right Hand) ✅ Fully Implemented + +| Property | Value | +|----------|-------| +| **Slot** | Right Hand | +| **Primary Mana** | Transference | +| **Raw Regen** | +0.5/hour base | +| **Conversion** | 0.2 raw→transference/hour | +| **Unlock** | Starting attunement | + +**Capabilities:** +- Enchanting equipment +- Disenchanting for mana recovery + +**Skill Categories Unlocked:** +- `enchant` - Enchanting efficiency +- `effectResearch` - Unlock enchantment effects + +--- + +#### 2. Invoker (Chest) ⚠️ Partially Implemented + +| Property | Value | +|----------|-------| +| **Slot** | Chest | +| **Primary Mana** | None (gains from pacts) | +| **Raw Regen** | +0.3/hour base | +| **Conversion** | None | +| **Unlock** | Defeat first guardian | + +**Capabilities:** +- Form pacts with guardians +- Access guardian powers +- Elemental mastery through pacts + +**Skill Categories Unlocked:** +- `invocation` - ⚠️ No skills defined +- `pact` - ⚠️ No skills defined + +--- + +#### 3. Fabricator (Left Hand) ✅ Implemented + +| Property | Value | +|----------|-------| +| **Slot** | Left Hand | +| **Primary Mana** | Earth | +| **Raw Regen** | +0.4/hour base | +| **Conversion** | 0.25 raw→earth/hour | +| **Unlock** | Prove crafting worth | + +**Capabilities:** +- Golem crafting +- Gear crafting +- Earth shaping + +**Skill Categories Unlocked:** +- `fabrication` - Crafting efficiency +- `golemancy` - Golem control + +### Attunement Leveling + +**XP Formula:** +``` +Level 2: 1,000 XP +Level 3: 2,500 XP +Level 4: 5,000 XP +Level 5: 10,000 XP +Each level: 2× previous (approximately) +Max Level: 10 +``` + +**Level Scaling:** +``` +regenMultiplier = 1.5^(level - 1) +conversionRate = baseRate × 1.5^(level - 1) +``` + +**XP Sources:** +- Enchanting: 1 XP per 10 capacity used + +--- + +## Skill System + +### Skill Categories + +| Category | Attunement | Description | +|----------|------------|-------------| +| `mana` | Core | Max mana, regen, element cap | +| `study` | Core | Study speed, cost reduction | +| `research` | Core | Click bonuses, advanced meditation | +| `ascension` | Core | Insight, guardian damage | +| `enchant` | Enchanter | Enchanting efficiency | +| `effectResearch` | Enchanter | Unlock enchantment effects | +| `invocation` | Invoker | ⚠️ Not implemented | +| `pact` | Invoker | ⚠️ Not implemented | +| `fabrication` | Fabricator | Crafting speed | +| `golemancy` | Fabricator | Golem control | +| `craft` | Legacy | Basic crafting | + +### Core Skills + +| Skill | Max | Effect | Study Time | +|-------|-----|--------|------------| +| Mana Well | 10 | +100 max mana/level | 4h | +| Mana Flow | 10 | +1 regen/level | 5h | +| Elem. Attunement | 10 | +50 element cap/level | 4h | +| Mana Overflow | 5 | +25% click mana/level | 6h | +| Quick Learner | 10 | +10% study speed/level | 4h | +| Focused Mind | 10 | -5% study cost/level | 5h | +| Meditation Focus | 1 | 2.5x regen after 4h | 6h | + +### Skill Tier Evolution + +**5-Tier System:** +``` +Tier 1: Base skill (multiplier ×1) +Tier 2: Enhanced (multiplier ×10) +Tier 3: Master (multiplier ×100) +Tier 4: Legendary (multiplier ×1,000) +Tier 5: Mythic (multiplier ×10,000) +``` + +**Tier Up:** +- Requires max level (10) in current tier +- Unlocks new skill ID (e.g., `manaWell_t2`) + +### Milestone Upgrades + +**At Level 5 and Level 10:** +- Choose 2 upgrades from 4+ options +- Upgrades can be stat multipliers, bonuses, or special effects +- Some upgrades have upgrade paths + +**Example Mana Well Tier 1 Upgrades:** +| Level | Upgrade | Effect | +|-------|---------|--------| +| 5 | Expanded Capacity | +25% max mana | +| 5 | Natural Spring | +0.5 regen | +| 5 | Mana Threshold | +30% max, -10% regen | +| 10 | Deep Reservoir | +50% max (replaces Expanded) | +| 10 | Deep Wellspring | +50% meditation efficiency | + +### Enchanter Skills + +| Skill | Max | Requirement | Effect | +|-------|-----|-------------|--------| +| Enchanting | 10 | Enchanter 1 | Unlocks enchantment design | +| Efficient Enchant | 5 | Enchanter 2, Enchanting 3 | -5% capacity cost/level | +| Disenchanting | 3 | Enchanter 1, Enchanting 2 | +20% mana recovery/level | +| Enchant Speed | 5 | Enchanter 1, Enchanting 2 | -10% time/level | +| Scroll Crafting | 3 | Enchanter 3, Enchanting 5 | ⚠️ Not implemented | +| Essence Refining | 5 | Enchanter 2, Enchanting 4 | +10% effect power/level | + +### Golemancy Skills + +| Skill | Max | Requirement | Effect | +|-------|-----|-------------|--------| +| Golem Mastery | 5 | Fabricator 2 | +10% golem damage/level | +| Golem Efficiency | 5 | Fabricator 2 | +5% attack speed/level | +| Golem Longevity | 3 | Fabricator 3 | +1 floor duration/level | +| Golem Siphon | 3 | Fabricator 3 | -10% maintenance/level | +| Advanced Golemancy | 1 | Fabricator 5, Mastery 3 | Unlock hybrid golems | + +--- + +## Equipment & Enchantment System + +### Equipment Slots + +``` +mainHand - Staves, Wands, Swords +offHand - Shields, Catalysts +head - Hoods, Hats, Helms +body - Robes, Armor +hands - Gloves, Gauntlets +feet - Boots, Shoes +accessory1 - Rings, Amulets +accessory2 - Rings, Amulets +``` + +### Equipment Categories + +| Category | Slots | Description | +|----------|-------|-------------| +| Caster | Main Hand | Staves and wands for spellcasting | +| Sword | Main Hand | Magic swords with weapon enchants | +| Catalyst | Main Hand | Amplifies magical effects | +| Shield | Off Hand | Defensive equipment | +| Head/Body/Hands/Feet | Respective | Armor pieces | +| Accessory | Accessory1/2 | Rings and amulets | + +### Equipment Instance Structure + +```typescript +interface EquipmentInstance { + instanceId: string; // Unique ID + typeId: string; // Reference to EquipmentType + name: string; + enchantments: AppliedEnchantment[]; + usedCapacity: number; // Current capacity used + totalCapacity: number; // Max capacity + rarity: 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary' | 'mythic'; + quality: number; // 0-100 +} +``` + +### Enchantment Process (3 Stages) + +#### Stage 1: Design +- Create enchantment design +- Select effects from unlocked pool +- Calculate capacity cost +- Time: Base 1h + 0.5h per effect stack + +**Capacity Cost Formula:** +``` +totalCost = Σ(effect.baseCost × (1 + 0.2 × stackIndex) × (1 - efficiencyBonus)) +``` + +#### Stage 2: Prepare +- Prepare equipment for enchanting +- Mana cost: capacity × 10 +- Time: 2h + 1h per 50 capacity + +#### Stage 3: Apply +- Apply enchantment design +- Mana per hour: 20 + 5 per effect stack +- Time: 2h + 1h per effect stack +- Grants Enchanter XP: 1 XP per 10 capacity + +### Enchantment Effect Categories + +| Category | Description | Equipment Types | +|----------|-------------|-----------------| +| **Spell** | Grants spell ability | Casters only | +| **Mana** | Max/regen/click bonuses | Casters, Catalysts, Head, Body, Accessories | +| **Combat** | Damage, crit, speed | Casters, Hands | +| **Utility** | Study, meditation, insight | Most equipment | +| **Special** | Unique effects | Various | +| **Elemental** | Weapon enchantments | Swords, Casters | + +### Starting Equipment + +| Slot | Item | Enchantment | Capacity | +|------|------|-------------|----------| +| Main Hand | Basic Staff | Mana Bolt spell | 50/50 | +| Body | Civilian Shirt | None | 0/30 | +| Feet | Civilian Shoes | None | 0/15 | + +--- + +## Golemancy System + +### Golem Slots + +``` +Fabricator Level 2: 1 slot +Fabricator Level 4: 2 slots +Fabricator Level 6: 3 slots +Fabricator Level 8: 4 slots +Fabricator Level 10: 5 slots + +slots = floor(fabricatorLevel / 2) +``` + +### Golem Types + +#### Base Golems + +| Golem | Element | Damage | Speed | Armor Pierce | Unlock | +|-------|---------|--------|-------|--------------|--------| +| Earth Golem | Earth | 8 | 1.5/h | 15% | Fabricator 2 | + +#### Elemental Variants + +| Golem | Element | Damage | Speed | Pierce | Unlock | +|-------|---------|--------|-------|--------|--------| +| Steel Golem | Metal | 12 | 1.2/h | 35% | Metal mana unlocked | +| Crystal Golem | Crystal | 18 | 1.0/h | 25% | Crystal mana unlocked | +| Sand Golem | Sand | 6 | 2.0/h | 10% | Sand mana unlocked | + +#### Advanced Hybrid Golems (Enchanter 5 + Fabricator 5) + +| Golem | Elements | Damage | Speed | Pierce | Special | +|-------|----------|--------|-------|--------|---------| +| Lava Golem | Earth + Fire | 15 | 1.0/h | 20% | AOE 2 | +| Galvanic Golem | Metal + Lightning | 10 | 3.5/h | 45% | Fast | +| Obsidian Golem | Earth + Dark | 25 | 0.8/h | 50% | High damage | +| Prism Golem | Crystal + Light | 20 | 1.5/h | 35% | AOE 3 | +| Quicksilver Golem | Metal + Water | 8 | 4.0/h | 30% | Very fast | +| Voidstone Golem | Earth + Void | 40 | 0.6/h | 60% | Ultimate | + +### Golem Costs + +**Summon Cost (one-time per floor):** +``` +Earth Golem: 10 Earth +Steel Golem: 8 Metal + 5 Earth +Crystal Golem: 6 Crystal + 3 Earth +``` + +**Maintenance Cost (per tick):** +``` +Earth Golem: 0.5 Earth/hour +Steel Golem: 0.6 Metal + 0.2 Earth/hour +``` + +### Golem Duration + +``` +baseDuration = 1 floor +with Golem Longevity: +1 floor per level (max 4 floors) +``` + +### Golem Combat + +**Attack Progress:** +``` +progressPerTick = HOURS_PER_TICK × attackSpeed × efficiencyBonus +``` + +**Damage:** +``` +damage = baseDamage × (1 + golemMastery × 0.1) +``` + +--- + +## Prestige/Loop System + +### Loop End Conditions + +| Condition | Result | +|-----------|--------| +| Day 30 reached | Loop ends, gain insight | +| Floor 100 + Pact 100 signed | Victory! 3× insight | + +### Insight Formula + +``` +baseInsight = floor(maxFloorReached × 15 + totalManaGathered / 500 + signedPacts.length × 150) +finalInsight = floor(baseInsight × (1 + insightAmpLevel × 0.25) × skillBonus) +``` + +### Prestige Upgrades + +| Upgrade | Max | Cost | Effect | +|---------|-----|------|--------| +| Mana Well | 5 | 500 | +500 starting max mana | +| Mana Flow | 10 | 750 | +0.5 permanent regen | +| Deep Memory | 5 | 1000 | +1 memory slot | +| Insight Amp | 4 | 1500 | +25% insight gain | +| Spire Key | 5 | 4000 | Start at floor +2 | +| Temporal Echo | 5 | 3000 | +10% mana generation | +| Steady Hand | 5 | 1200 | -15% durability loss | +| Ancient Knowledge | 5 | 2000 | Start with blueprint | +| Elemental Attune | 10 | 600 | +25 element cap | +| Spell Memory | 3 | 2500 | Start with random spell | +| Guardian Pact | 5 | 3500 | +10% pact multiplier | +| Quick Start | 3 | 400 | +100 starting mana | +| Elem. Start | 3 | 800 | +5 each unlocked element | + +### Memory System + +- **Base slots:** 3 +- **Additional:** +1 per Deep Memory level +- **Memories:** Spells or skills preserved across loops + +--- + +## Achievement System + +### Achievement Categories + +| Category | Description | +|----------|-------------| +| `mana` | Mana gathering milestones | +| `combat` | Combat achievements | +| `progression` | Floor/guardian progression | +| `crafting` | Equipment and enchanting | +| `prestige` | Loop and insight milestones | + +### Achievement Rewards + +| Reward Type | Effect | +|-------------|--------| +| `insight` | One-time insight bonus | +| `manaBonus` | Permanent max mana | +| `damageBonus` | Permanent damage increase | +| `regenBonus` | Permanent regen increase | +| `title` | Cosmetic title | +| `unlockEffect` | Unlocks enchantment effect | + +--- + +## Formulas & Calculations + +### Damage Calculation (Complete) + +```javascript +function calcDamage(state, spellId, floorElement) { + const spell = SPELLS_DEF[spellId]; + + // Base damage + let damage = spell.dmg + (state.skills.combatTrain || 0) * 5; + + // Percentage multipliers + damage *= 1 + (state.skills.arcaneFury || 0) * 0.1; + + // Elemental mastery + damage *= 1 + (state.skills.elementalMastery || 0) * 0.15; + + // Guardian bane (vs guardians only) + if (isGuardian) { + damage *= 1 + (state.skills.guardianBane || 0) * 0.2; + } + + // Pact multiplier + damage *= state.signedPacts.reduce((m, f) => m * GUARDIANS[f].pact, 1); + + // Elemental effectiveness + damage *= getElementalBonus(spell.elem, floorElement); + + // Critical hit + const critChance = (state.skills.precision || 0) * 0.05; + if (Math.random() < critChance) { + damage *= 1.5; + } + + // Equipment effects + damage *= effects.baseDamageMultiplier; + damage += effects.baseDamageBonus; + + // Armor reduction + const effectiveArmor = Math.max(0, enemyArmor - armorPierce); + damage *= (1 - effectiveArmor); + + return Math.floor(damage); +} +``` + +### Study Time Calculation + +```javascript +effectiveStudyTime = baseStudyTime × tier / studySpeedMultiplier + +studySpeedMultiplier = 1 + (quickLearnerLevel × 0.1) + equipmentBonus +``` + +### Study Cost Calculation + +```javascript +effectiveCost = floor(baseCost × (currentLevel + 1) × tier × costMultiplier) + +costMultiplier = 1 - (focusedMindLevel × 0.05) +``` + +### DPS Calculation + +```javascript +dps = (damage × castSpeed × attackSpeedMultiplier) / hour +``` + +--- + +## System Interactions + +### Primary Interaction Map + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ CORE SYSTEM INTERACTIONS │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ MANA │────────▶│ SKILLS │────────▶│ COMBAT │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +│ │ │ │ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ATTUNEMENT│────────▶│ENCHANTING│────────▶│ SPIRE │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +│ │ │ │ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │GOLEMANCY │ │EQUIPMENT │────────▶│ GUARDIAN │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────┐ │ +│ │ PACT │ │ +│ └──────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────┐ │ +│ │ PRESTIGE │ │ +│ └──────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Key System Dependencies + +| System | Depends On | Unlocks/Enables | +|--------|------------|-----------------| +| **Skills** | Mana (cost) | All combat/crafting bonuses | +| **Enchanting** | Enchanter attunement, Skills | Equipment spells, bonuses | +| **Golemancy** | Fabricator attunement, Earth mana | Additional combat damage | +| **Pacts** | Guardian defeat | Permanent multipliers, boons | +| **Prestige** | Loop completion | Permanent upgrades, memories | +| **Equipment** | Crafting/Blueprints | Spell access, stat bonuses | + +### Progression Gates + +1. **Early Game (Floors 1-10):** + - Mana gathering and regen + - Basic skills (mana, study categories) + - Starting equipment + +2. **Mid Game (Floors 10-40):** + - First guardian pacts + - Attunement unlocking + - Equipment enchanting + - Golemancy (Fabricator) + +3. **Late Game (Floors 40-80):** + - Compound/exotic elements + - Hybrid golems + - Skill tier evolution + - Advanced enchantments + +4. **End Game (Floors 80-100):** + - Void/Stellar/Crystal spells + - Ultimate golems + - Victory preparation + +--- + +## Appendix: Known Issues + +### Missing Implementations + +1. **Invocation Skills** - Category defined but no skills +2. **Pact Skills** - Category defined but no skills +3. **Scroll Crafting** - Skill exists, no system +4. **Field Repair** - Skill exists, no repair system + +### Balance Concerns + +1. Exotic elements require extreme mana investment +2. Incursion creates hard time pressure without counterplay +3. Equipment progression limited after starting gear + +--- + +*End of Game Briefing Document* diff --git a/src/lib/game/__tests__/computed-stats.test.ts b/src/lib/game/__tests__/computed-stats.test.ts index 2a5d08e..c570733 100755 --- a/src/lib/game/__tests__/computed-stats.test.ts +++ b/src/lib/game/__tests__/computed-stats.test.ts @@ -221,7 +221,8 @@ describe('computeRegen', () => { }; const effects = { regenBonus: 0, regenMultiplier: 1, permanentRegenBonus: 0 }; const result = computeRegen(state, effects); - expect(result).toBe(2); // Base regen + // Base regen is 2 (this test provides effects, so no attunement bonus) + expect(result).toBe(2); }); }); diff --git a/src/lib/game/computed-stats.ts b/src/lib/game/computed-stats.ts index 69aec0f..4f9af83 100755 --- a/src/lib/game/computed-stats.ts +++ b/src/lib/game/computed-stats.ts @@ -78,7 +78,7 @@ export function getFloorMaxHP(floor: number): number { } export function getFloorElement(floor: number): string { - return FLOOR_ELEM_CYCLE[(floor - 1) % 8]; + return FLOOR_ELEM_CYCLE[(floor - 1) % FLOOR_ELEM_CYCLE.length]; } // ─── Equipment Spell Helper ───────────────────────────────────────────────────── diff --git a/src/lib/game/store.test.ts b/src/lib/game/store.test.ts index f7a1ea5..c19f6b2 100755 --- a/src/lib/game/store.test.ts +++ b/src/lib/game/store.test.ts @@ -2,6 +2,7 @@ * Unit Tests for Mana Loop Game Logic * * This file contains comprehensive tests for the game's core mechanics. + * Updated for the new skill system with tiers and upgrade trees. */ import { describe, it, expect, beforeEach } from 'bun:test'; @@ -73,6 +74,8 @@ function createMockState(overrides: Partial = {}): GameState { spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 } }, skills: {}, skillProgress: {}, + skillUpgrades: {}, + skillTiers: {}, equipment: { mainHand: null, offHand: null, head: null, body: null, hands: null, accessory: null }, inventory: [], blueprints: {}, @@ -90,8 +93,18 @@ function createMockState(overrides: Partial = {}): GameState { containmentWards: 0, log: [], loopInsight: 0, + attunements: { + enchanter: { id: 'enchanter', active: true, level: 1, experience: 0 }, + invoker: { id: 'invoker', active: false, level: 1, experience: 0 }, + fabricator: { id: 'fabricator', active: false, level: 1, experience: 0 }, + }, + golemancy: { + enabledGolems: [], + summonedGolems: [], + lastSummonFloor: 0, + }, ...overrides, - }; + } as GameState; } // ─── Formatting Tests ───────────────────────────────────────────────────────── @@ -173,21 +186,22 @@ describe('Floor Functions', () => { }); describe('getFloorElement', () => { - it('should cycle through elements in order', () => { + it('should cycle through elements in order (7 elements)', () => { + // FLOOR_ELEM_CYCLE = ["fire", "water", "air", "earth", "light", "dark", "death"] expect(getFloorElement(1)).toBe('fire'); expect(getFloorElement(2)).toBe('water'); expect(getFloorElement(3)).toBe('air'); expect(getFloorElement(4)).toBe('earth'); expect(getFloorElement(5)).toBe('light'); expect(getFloorElement(6)).toBe('dark'); - expect(getFloorElement(7)).toBe('life'); - expect(getFloorElement(8)).toBe('death'); + expect(getFloorElement(7)).toBe('death'); }); - it('should wrap around after 8 floors', () => { - expect(getFloorElement(9)).toBe('fire'); - expect(getFloorElement(10)).toBe('water'); - expect(getFloorElement(17)).toBe('fire'); + it('should wrap around after 7 floors', () => { + expect(getFloorElement(8)).toBe('fire'); + expect(getFloorElement(9)).toBe('water'); + expect(getFloorElement(15)).toBe('fire'); // (15-1) % 7 = 0 -> fire + expect(getFloorElement(16)).toBe('water'); // (16-1) % 7 = 1 -> water }); }); }); @@ -206,23 +220,10 @@ describe('Mana Calculation Functions', () => { expect(computeMaxMana(state)).toBe(100 + 5 * 100); }); - it('should add mana from deepReservoir skill', () => { - const state = createMockState({ skills: { deepReservoir: 3 } }); - expect(computeMaxMana(state)).toBe(100 + 3 * 500); - }); - it('should add mana from prestige upgrades', () => { const state = createMockState({ prestigeUpgrades: { manaWell: 3 } }); expect(computeMaxMana(state)).toBe(100 + 3 * 500); }); - - it('should stack all mana bonuses', () => { - const state = createMockState({ - skills: { manaWell: 5, deepReservoir: 2 }, - prestigeUpgrades: { manaWell: 2 }, - }); - expect(computeMaxMana(state)).toBe(100 + 5 * 100 + 2 * 500 + 2 * 500); - }); }); describe('computeElementMax', () => { @@ -244,23 +245,28 @@ describe('Mana Calculation Functions', () => { describe('computeRegen', () => { it('should return base regen with no upgrades', () => { + // Base regen is 2, but Enchanter attunement adds 0.5 const state = createMockState(); - expect(computeRegen(state)).toBe(2); + expect(computeRegen(state)).toBe(2.5); // 2 + 0.5 from enchanter }); it('should add regen from manaFlow skill', () => { + // Base 2 + enchanter 0.5 + manaFlow 5 const state = createMockState({ skills: { manaFlow: 5 } }); - expect(computeRegen(state)).toBe(2 + 5 * 1); + expect(computeRegen(state)).toBe(2 + 0.5 + 5 * 1); }); it('should add regen from manaSpring skill', () => { + // Base 2 + enchanter 0.5 + manaSpring 2 const state = createMockState({ skills: { manaSpring: 1 } }); - expect(computeRegen(state)).toBe(2 + 2); + expect(computeRegen(state)).toBe(2 + 0.5 + 2); }); it('should multiply by temporal echo prestige', () => { const state = createMockState({ prestigeUpgrades: { temporalEcho: 2 } }); - expect(computeRegen(state)).toBe(2 * 1.2); + // Base 2 * 1.2 (temporal) = 2.4, + enchanter 0.5 = 2.9 + // Note: temporal bonus applies to base, not attunement + expect(computeRegen(state)).toBe(2 * 1.2 + 0.5); }); }); @@ -298,22 +304,6 @@ describe('Damage Calculation', () => { expect(dmg).toBeLessThanOrEqual(5 * 1.5); // No crit bonus, just base }); - it('should add damage from combatTrain skill', () => { - const state = createMockState({ skills: { combatTrain: 5 } }); - const dmg = calcDamage(state, 'manaBolt'); - expect(dmg).toBeGreaterThanOrEqual(5 + 5 * 5); - }); - - it('should multiply by arcaneFury skill', () => { - const state = createMockState({ skills: { arcaneFury: 3 } }); - // Without crit: base * 1.3 - const minDmg = 5 * 1.3; - const maxDmg = 5 * 1.3 * 1.5; // With crit - const dmg = calcDamage(state, 'manaBolt'); - expect(dmg).toBeGreaterThanOrEqual(minDmg); - expect(dmg).toBeLessThanOrEqual(maxDmg); - }); - it('should multiply by signed pacts', () => { const state = createMockState({ signedPacts: [10] }); // Pact multiplier is 1.5 for floor 10 @@ -342,7 +332,6 @@ describe('Damage Calculation', () => { it('should give +50% for opposing element (super effective)', () => { const state = createMockState({ spells: { waterJet: { learned: true, level: 1 } } }); // Water vs fire - water is the opposite of fire, so water is super effective - // ELEMENT_OPPOSITES['fire'] === 'water' -> 1.5x const dmg = calcDamage(state, 'waterJet', 'fire'); // Base 12 * 1.5 = 18 (without crit) expect(dmg).toBeGreaterThanOrEqual(18 * 0.5); // Can crit, so min is lower @@ -351,7 +340,6 @@ describe('Damage Calculation', () => { it('should give +50% when attacking opposite element (fire vs water)', () => { const state = createMockState({ spells: { fireball: { learned: true, level: 1 } } }); // Fire vs water - fire is the opposite of water, so fire is super effective - // ELEMENT_OPPOSITES['water'] === 'fire' -> 1.5x const dmg = calcDamage(state, 'fireball', 'water'); // Base 15 * 1.5 = 22.5 (without crit) expect(dmg).toBeGreaterThanOrEqual(22.5 * 0.5); // Can crit @@ -567,21 +555,21 @@ describe('Study Speed Functions', () => { describe('Game Constants', () => { describe('ELEMENTS', () => { it('should have all base elements', () => { + // Life, blood, wood were removed - we have 7 base elements now expect(ELEMENTS.fire).toBeDefined(); expect(ELEMENTS.water).toBeDefined(); expect(ELEMENTS.air).toBeDefined(); expect(ELEMENTS.earth).toBeDefined(); expect(ELEMENTS.light).toBeDefined(); expect(ELEMENTS.dark).toBeDefined(); - expect(ELEMENTS.life).toBeDefined(); expect(ELEMENTS.death).toBeDefined(); }); it('should have composite elements with recipes', () => { - expect(ELEMENTS.blood.recipe).toEqual(['life', 'water']); + // blood and wood were removed expect(ELEMENTS.metal.recipe).toEqual(['fire', 'earth']); - expect(ELEMENTS.wood.recipe).toEqual(['life', 'earth']); expect(ELEMENTS.sand.recipe).toEqual(['earth', 'water']); + expect(ELEMENTS.lightning.recipe).toEqual(['fire', 'air']); }); it('should have exotic elements with 3-ingredient recipes', () => { @@ -589,18 +577,24 @@ describe('Game Constants', () => { expect(ELEMENTS.stellar.recipe).toHaveLength(3); expect(ELEMENTS.void.recipe).toHaveLength(3); }); + + it('should have utility element transference', () => { + expect(ELEMENTS.transference).toBeDefined(); + expect(ELEMENTS.transference.cat).toBe('utility'); + }); }); describe('GUARDIANS', () => { - it('should have guardians every 10 floors', () => { - [10, 20, 30, 40, 50, 60, 70, 80, 90, 100].forEach(floor => { + it('should have guardians on expected floors', () => { + // Note: Floor 70 was removed + [10, 20, 30, 40, 50, 60, 80, 90, 100].forEach(floor => { expect(GUARDIANS[floor]).toBeDefined(); }); }); it('should have increasing HP', () => { let prevHP = 0; - [10, 20, 30, 40, 50, 60, 70, 80, 90, 100].forEach(floor => { + [10, 20, 30, 40, 50, 60, 80, 90, 100].forEach(floor => { expect(GUARDIANS[floor].hp).toBeGreaterThan(prevHP); prevHP = GUARDIANS[floor].hp; }); @@ -608,7 +602,7 @@ describe('Game Constants', () => { it('should have increasing pact multipliers', () => { let prevPact = 1; - [10, 20, 30, 40, 50, 60, 70, 80, 90, 100].forEach(floor => { + [10, 20, 30, 40, 50, 60, 80, 90, 100].forEach(floor => { expect(GUARDIANS[floor].pact).toBeGreaterThan(prevPact); prevPact = GUARDIANS[floor].pact; }); @@ -622,8 +616,9 @@ describe('Game Constants', () => { expect(SPELLS_DEF.manaBolt.cost.type).toBe('raw'); }); - it('should have spells for each base element', () => { - const elements = ['fire', 'water', 'air', 'earth', 'light', 'dark', 'life', 'death']; + it('should have spells for existing base elements', () => { + // Life was removed, death is present + const elements = ['fire', 'water', 'air', 'earth', 'light', 'dark', 'death']; elements.forEach(elem => { const hasSpell = Object.values(SPELLS_DEF).some(s => s.elem === elem); expect(hasSpell).toBe(true); @@ -642,7 +637,7 @@ describe('Game Constants', () => { describe('SKILLS_DEF', () => { it('should have skills with valid categories', () => { - const validCategories = ['mana', 'combat', 'study', 'craft', 'research', 'ascension']; + const validCategories = ['mana', 'study', 'research', 'ascension', 'enchant', 'effectResearch', 'invocation', 'pact', 'fabrication', 'golemancy', 'craft']; Object.values(SKILLS_DEF).forEach(skill => { expect(validCategories).toContain(skill.cat); }); @@ -733,7 +728,39 @@ describe('Integration Tests', () => { }); }); -// ─── Individual Skill Tests ───────────────────────────────────────────────────── +// ─── Skill Evolution Tests ───────────────────────────────────────────────────── + +describe('Skill Evolution System', () => { + describe('SKILL_EVOLUTION_PATHS', () => { + it('should have evolution paths for major skills', () => { + expect(SKILL_EVOLUTION_PATHS.manaWell).toBeDefined(); + expect(SKILL_EVOLUTION_PATHS.manaFlow).toBeDefined(); + expect(SKILL_EVOLUTION_PATHS.quickLearner).toBeDefined(); + expect(SKILL_EVOLUTION_PATHS.focusedMind).toBeDefined(); + }); + + it('should have multiple tiers for evolution', () => { + expect(SKILL_EVOLUTION_PATHS.manaWell.tiers.length).toBeGreaterThanOrEqual(3); + }); + }); + + describe('getTierMultiplier', () => { + it('should return correct multipliers for tiered skills', () => { + expect(getTierMultiplier('manaWell')).toBe(1); + expect(getTierMultiplier('manaWell_t2')).toBe(10); + expect(getTierMultiplier('manaWell_t3')).toBe(100); + }); + }); + + describe('getNextTierSkill', () => { + it('should return next tier skill ID', () => { + expect(getNextTierSkill('manaWell')).toBe('manaWell_t2'); + expect(getNextTierSkill('manaWell_t2')).toBe('manaWell_t3'); + }); + }); +}); + +// ─── Individual Skill Tests (Current System) ─────────────────────────────────── describe('Individual Skill Tests', () => { @@ -755,33 +782,25 @@ describe('Individual Skill Tests', () => { }); expect(computeMaxMana(state)).toBe(100 + 300 + 1000); }); + + it('should have evolution path to Deep Reservoir at tier 2', () => { + const tier2 = SKILL_EVOLUTION_PATHS.manaWell.tiers.find(t => t.tier === 2); + expect(tier2).toBeDefined(); + expect(tier2?.name).toBe('Deep Reservoir'); + }); }); describe('manaFlow', () => { it('should add +1 regen/hr per level', () => { + // Base regen is 2 + enchanter 0.5 = 2.5 const state0 = createMockState(); - expect(computeRegen(state0)).toBe(2); + expect(computeRegen(state0)).toBe(2.5); const state3 = createMockState({ skills: { manaFlow: 3 } }); - expect(computeRegen(state3)).toBe(2 + 3); + expect(computeRegen(state3)).toBe(2 + 0.5 + 3); const state10 = createMockState({ skills: { manaFlow: 10 } }); - expect(computeRegen(state10)).toBe(2 + 10); - }); - }); - - describe('deepReservoir', () => { - it('should add +500 max mana per level', () => { - const state1 = createMockState({ skills: { deepReservoir: 1 } }); - expect(computeMaxMana(state1)).toBe(100 + 500); - - const state5 = createMockState({ skills: { deepReservoir: 5 } }); - expect(computeMaxMana(state5)).toBe(100 + 2500); - }); - - it('should stack with manaWell', () => { - const state = createMockState({ skills: { manaWell: 5, deepReservoir: 2 } }); - expect(computeMaxMana(state)).toBe(100 + 500 + 1000); + expect(computeRegen(state10)).toBe(2 + 0.5 + 10); }); }); @@ -796,113 +815,17 @@ describe('Individual Skill Tests', () => { }); describe('manaOverflow', () => { - it('should add +25% mana from clicks per level', () => { - // Note: manaOverflow is applied in gatherMana, not computeClickMana - // computeClickMana returns base click mana - const baseClick = computeClickMana(createMockState()); - expect(baseClick).toBe(1); - - // The actual bonus is applied in gatherMana: - // cm = Math.floor(cm * (1 + manaOverflow * 0.25)) - // So level 1 = 1.25x, level 5 = 2.25x - const level1Bonus = 1 + 1 * 0.25; - const level5Bonus = 1 + 5 * 0.25; - - expect(Math.floor(baseClick * level1Bonus)).toBe(1); - expect(Math.floor(baseClick * level5Bonus)).toBe(2); + it('should be defined with correct properties', () => { + expect(SKILLS_DEF.manaOverflow).toBeDefined(); + expect(SKILLS_DEF.manaOverflow.max).toBe(5); + expect(SKILLS_DEF.manaOverflow.desc).toContain('click'); }); - }); - - // ─── Combat Skills ────────────────────────────────────────────────────────── - - describe('combatTrain', () => { - it('should add +5 base damage per level', () => { - const state0 = createMockState(); - const dmg0 = calcDamage(state0, 'manaBolt'); - // Base manaBolt dmg is 5, combatTrain adds 5 per level - // But there's randomness from crit, so check minimum - expect(dmg0).toBeGreaterThanOrEqual(5); - - const state5 = createMockState({ skills: { combatTrain: 5 } }); - const dmg5 = calcDamage(state5, 'manaBolt'); - // 5 base + 5*5 = 30 minimum (no crit) - expect(dmg5).toBeGreaterThanOrEqual(30); + + it('should require Mana Well 3', () => { + expect(SKILLS_DEF.manaOverflow.req).toEqual({ manaWell: 3 }); }); }); - describe('arcaneFury', () => { - it('should add +10% spell damage per level', () => { - const state0 = createMockState(); - const dmg0 = calcDamage(state0, 'manaBolt'); - - const state3 = createMockState({ skills: { arcaneFury: 3 } }); - const dmg3 = calcDamage(state3, 'manaBolt'); - - // arcaneFury gives 1 + 0.1*level = 1.3x for level 3 - // Without crit, damage should be 5 * 1.3 = 6.5 - expect(dmg3).toBeGreaterThanOrEqual(5 * 1.3 * 0.5); // Min (no crit) - }); - }); - - describe('precision', () => { - it('should add +5% crit chance per level', () => { - // Precision affects crit chance in calcDamage - // This is probabilistic, so we test the formula - // critChance = skills.precision * 0.05 - // Level 1 = 5%, Level 5 = 25% - - // Run many samples to verify crit rate - let crits = 0; - const samples = 1000; - const state = createMockState({ skills: { precision: 2 } }); // 10% crit - - for (let i = 0; i < samples; i++) { - const dmg = calcDamage(state, 'manaBolt'); - if (dmg > 6) crits++; // Crit does 1.5x damage - } - - // Should be around 10% with some variance - expect(crits).toBeGreaterThan(50); // At least 5% - expect(crits).toBeLessThan(200); // At most 20% - }); - }); - - describe('quickCast', () => { - it('should be defined and have correct max', () => { - expect(SKILLS_DEF.quickCast).toBeDefined(); - expect(SKILLS_DEF.quickCast.max).toBe(5); - expect(SKILLS_DEF.quickCast.desc).toContain('5% attack speed'); - }); - // Note: quickCast affects attack speed, which would need integration tests - }); - - describe('elementalMastery', () => { - it('should add +15% elemental damage bonus per level', () => { - // Test with fireball (fire spell) vs fire floor (same element) - const state0 = createMockState({ spells: { fireball: { learned: true, level: 1 } } }); - const dmg0 = calcDamage(state0, 'fireball', 'fire'); - - const state2 = createMockState({ - skills: { elementalMastery: 2 }, - spells: { fireball: { learned: true, level: 1 } } - }); - const dmg2 = calcDamage(state2, 'fireball', 'fire'); - - // elementalMastery gives 1 + 0.15*level bonus - // Level 2 = 1.3x elemental damage - expect(dmg2).toBeGreaterThan(dmg0 * 0.9); - }); - }); - - describe('spellEcho', () => { - it('should give 10% chance to cast twice per level', () => { - expect(SKILLS_DEF.spellEcho).toBeDefined(); - expect(SKILLS_DEF.spellEcho.max).toBe(3); - expect(SKILLS_DEF.spellEcho.desc).toContain('10% chance to cast twice'); - }); - // Note: spellEcho is probabilistic, verified in combat tick - }); - // ─── Study Skills ─────────────────────────────────────────────────────────── describe('quickLearner', () => { @@ -921,8 +844,6 @@ describe('Individual Skill Tests', () => { }); it('should reduce skill study cost', () => { - // Skill base cost is 100 for manaWell - // With focusedMind 5, cost should be 100 * 0.75 = 75 const baseCost = SKILLS_DEF.manaWell.base; const costMult5 = getStudyCostMultiplier({ focusedMind: 5 }); const reducedCost = Math.floor(baseCost * costMult5); @@ -930,7 +851,6 @@ describe('Individual Skill Tests', () => { }); it('should reduce spell study cost', () => { - // Spell unlock cost is 100 for fireball const baseCost = SPELLS_DEF.fireball.unlock; const costMult5 = getStudyCostMultiplier({ focusedMind: 5 }); const reducedCost = Math.floor(baseCost * costMult5); @@ -956,7 +876,6 @@ describe('Individual Skill Tests', () => { expect(SKILLS_DEF.knowledgeRetention.max).toBe(3); expect(SKILLS_DEF.knowledgeRetention.desc).toContain('20% study progress saved'); }); - // Note: This is tested in cancelStudy integration }); // ─── Research Skills ──────────────────────────────────────────────────────── @@ -981,12 +900,17 @@ describe('Individual Skill Tests', () => { const state = createMockState({ skills: { manaTap: 1, manaSurge: 1 } }); expect(computeClickMana(state)).toBe(1 + 1 + 3); }); + + it('should require manaTap', () => { + expect(SKILLS_DEF.manaSurge.req).toEqual({ manaTap: 1 }); + }); }); describe('manaSpring', () => { it('should add +2 mana regen', () => { + // Base 2 + enchanter 0.5 + manaSpring 2 const state = createMockState({ skills: { manaSpring: 1 } }); - expect(computeRegen(state)).toBe(2 + 2); + expect(computeRegen(state)).toBe(2 + 0.5 + 2); }); }); @@ -995,6 +919,10 @@ describe('Individual Skill Tests', () => { const bonus = getMeditationBonus(150, { meditation: 1, deepTrance: 1 }); // 6 hours expect(bonus).toBe(3.0); }); + + it('should require meditation', () => { + expect(SKILLS_DEF.deepTrance.req).toEqual({ meditation: 1 }); + }); }); describe('voidMeditation', () => { @@ -1002,6 +930,10 @@ describe('Individual Skill Tests', () => { const bonus = getMeditationBonus(200, { meditation: 1, deepTrance: 1, voidMeditation: 1 }); // 8 hours expect(bonus).toBe(5.0); }); + + it('should require deepTrance', () => { + expect(SKILLS_DEF.voidMeditation.req).toEqual({ deepTrance: 1 }); + }); }); // ─── Ascension Skills ─────────────────────────────────────────────────────── @@ -1025,7 +957,38 @@ describe('Individual Skill Tests', () => { expect(SKILLS_DEF.guardianBane.max).toBe(3); expect(SKILLS_DEF.guardianBane.desc).toContain('20% dmg vs guardians'); }); - // Note: guardianBane is checked in calcDamage when floor is guardian floor + }); + + // ─── Enchanter Skills ─────────────────────────────────────────────────────── + + describe('enchanting', () => { + it('should require enchanter attunement', () => { + expect(SKILLS_DEF.enchanting).toBeDefined(); + expect(SKILLS_DEF.enchanting.attunement).toBe('enchanter'); + }); + }); + + describe('efficientEnchant', () => { + it('should require enchanting 3', () => { + expect(SKILLS_DEF.efficientEnchant).toBeDefined(); + expect(SKILLS_DEF.efficientEnchant.req).toEqual({ enchanting: 3 }); + }); + }); + + // ─── Fabricator/Golemancy Skills ──────────────────────────────────────────── + + describe('golemMastery', () => { + it('should be defined with correct properties', () => { + expect(SKILLS_DEF.golemMastery).toBeDefined(); + expect(SKILLS_DEF.golemMastery.max).toBe(5); + }); + }); + + describe('golemEfficiency', () => { + it('should be defined with correct properties', () => { + expect(SKILLS_DEF.golemEfficiency).toBeDefined(); + expect(SKILLS_DEF.golemEfficiency.max).toBe(5); + }); }); // ─── Crafting Skills ──────────────────────────────────────────────────────── @@ -1038,19 +1001,10 @@ describe('Individual Skill Tests', () => { }); }); - describe('durableConstruct', () => { - it('should add +1 max durability per level', () => { - expect(SKILLS_DEF.durableConstruct).toBeDefined(); - expect(SKILLS_DEF.durableConstruct.max).toBe(5); - expect(SKILLS_DEF.durableConstruct.desc).toContain('+1 max durability'); - }); - }); - describe('fieldRepair', () => { - it('should add +15% repair efficiency per level', () => { + it('should be defined with correct properties', () => { expect(SKILLS_DEF.fieldRepair).toBeDefined(); expect(SKILLS_DEF.fieldRepair.max).toBe(5); - expect(SKILLS_DEF.fieldRepair.desc).toContain('15% repair efficiency'); }); }); @@ -1066,20 +1020,8 @@ describe('Individual Skill Tests', () => { // ─── Skill Requirement Tests ───────────────────────────────────────────────── describe('Skill Requirements', () => { - it('deepReservoir should require manaWell 5', () => { - expect(SKILLS_DEF.deepReservoir.req).toEqual({ manaWell: 5 }); - }); - - it('arcaneFury should require combatTrain 3', () => { - expect(SKILLS_DEF.arcaneFury.req).toEqual({ combatTrain: 3 }); - }); - - it('elementalMastery should require arcaneFury 2', () => { - expect(SKILLS_DEF.elementalMastery.req).toEqual({ arcaneFury: 2 }); - }); - - it('spellEcho should require quickCast 3', () => { - expect(SKILLS_DEF.spellEcho.req).toEqual({ quickCast: 3 }); + it('manaOverflow should require manaWell 3', () => { + expect(SKILLS_DEF.manaOverflow.req).toEqual({ manaWell: 3 }); }); it('manaSurge should require manaTap 1', () => { @@ -1094,986 +1036,7 @@ describe('Skill Requirements', () => { expect(SKILLS_DEF.voidMeditation.req).toEqual({ deepTrance: 1 }); }); - it('elemCrafting should require effCrafting 3', () => { - expect(SKILLS_DEF.elemCrafting.req).toEqual({ effCrafting: 3 }); + it('efficientEnchant should require enchanting 3', () => { + expect(SKILLS_DEF.efficientEnchant.req).toEqual({ enchanting: 3 }); }); }); - -// ─── Skill Evolution Tests ───────────────────────────────────────────────────── - -describe('Skill Evolution System', () => { - describe('SKILL_EVOLUTION_PATHS', () => { - it('should have evolution paths for 6 base skills', () => { - expect(SKILL_EVOLUTION_PATHS.manaWell).toBeDefined(); - expect(SKILL_EVOLUTION_PATHS.manaFlow).toBeDefined(); - expect(SKILL_EVOLUTION_PATHS.combatTrain).toBeDefined(); - expect(SKILL_EVOLUTION_PATHS.quickLearner).toBeDefined(); - expect(SKILL_EVOLUTION_PATHS.focusedMind).toBeDefined(); - expect(SKILL_EVOLUTION_PATHS.elemAttune).toBeDefined(); - }); - - it('should have 5 tiers for each base skill', () => { - Object.values(SKILL_EVOLUTION_PATHS).forEach(path => { - expect(path.tiers).toHaveLength(5); - }); - }); - - it('should have increasing multipliers for each tier', () => { - Object.values(SKILL_EVOLUTION_PATHS).forEach(path => { - let prevMult = 0; - path.tiers.forEach(tier => { - expect(tier.multiplier).toBeGreaterThan(prevMult); - prevMult = tier.multiplier; - }); - }); - }); - }); - - describe('getUpgradesForSkillAtMilestone', () => { - it('should return 4 upgrades at each milestone', () => { - const upgradesL5 = getUpgradesForSkillAtMilestone('manaWell', 5, {}); - const upgradesL10 = getUpgradesForSkillAtMilestone('manaWell', 10, {}); - - expect(upgradesL5).toHaveLength(4); - expect(upgradesL10).toHaveLength(4); - }); - - it('should return empty array for non-evolvable skills', () => { - const upgrades = getUpgradesForSkillAtMilestone('nonexistent', 5, {}); - expect(upgrades).toHaveLength(0); - }); - - it('should have unique upgrade IDs for each milestone', () => { - const upgradesL5 = getUpgradesForSkillAtMilestone('manaWell', 5, {}); - const upgradesL10 = getUpgradesForSkillAtMilestone('manaWell', 10, {}); - - const l5Ids = upgradesL5.map(u => u.id); - const l10Ids = upgradesL10.map(u => u.id); - - // No overlap between milestones - expect(l5Ids.find(id => l10Ids.includes(id))).toBeUndefined(); - }); - - it('should have valid effect types', () => { - Object.values(SKILL_EVOLUTION_PATHS).forEach(path => { - path.tiers.forEach(tier => { - tier.upgrades.forEach(upgrade => { - expect(['multiplier', 'bonus', 'special']).toContain(upgrade.effect.type); - }); - }); - }); - }); - }); - - describe('getNextTierSkill', () => { - it('should return next tier for base skills', () => { - expect(getNextTierSkill('manaWell')).toBe('manaWell_t2'); - expect(getNextTierSkill('manaFlow')).toBe('manaFlow_t2'); - }); - - it('should return next tier for tier skills', () => { - expect(getNextTierSkill('manaWell_t2')).toBe('manaWell_t3'); - expect(getNextTierSkill('manaWell_t3')).toBe('manaWell_t4'); - expect(getNextTierSkill('manaWell_t4')).toBe('manaWell_t5'); - }); - - it('should return null for max tier', () => { - expect(getNextTierSkill('manaWell_t5')).toBeNull(); - }); - - it('should return null for non-evolvable skills', () => { - expect(getNextTierSkill('nonexistent')).toBeNull(); - }); - }); - - describe('getTierMultiplier', () => { - it('should return 1 for tier 1 skills', () => { - expect(getTierMultiplier('manaWell')).toBe(1); - expect(getTierMultiplier('manaWell_t1')).toBe(1); - }); - - it('should return correct multiplier for higher tiers', () => { - // Each tier is 10x more powerful so tier N level 1 = tier N-1 level 10 - expect(getTierMultiplier('manaWell_t2')).toBe(10); - expect(getTierMultiplier('manaWell_t3')).toBe(100); - expect(getTierMultiplier('manaWell_t4')).toBe(1000); - expect(getTierMultiplier('manaWell_t5')).toBe(10000); - }); - - it('should return 1 for non-evolvable skills', () => { - expect(getTierMultiplier('nonexistent')).toBe(1); - }); - }); - - describe('generateTierSkillDef', () => { - it('should generate valid skill definition for each tier', () => { - for (let tier = 1; tier <= 5; tier++) { - const def = generateTierSkillDef('manaWell', tier); - expect(def).toBeDefined(); - expect(def?.tier).toBe(tier); - expect(def?.baseSkill).toBe('manaWell'); - expect(def?.tierMultiplier).toBe(SKILL_EVOLUTION_PATHS.manaWell.tiers[tier - 1].multiplier); - } - }); - - it('should return null for non-evolvable base skill', () => { - expect(generateTierSkillDef('nonexistent', 1)).toBeNull(); - }); - - it('should return null for invalid tier', () => { - expect(generateTierSkillDef('manaWell', 0)).toBeNull(); - expect(generateTierSkillDef('manaWell', 6)).toBeNull(); - }); - - it('should have correct tier names', () => { - expect(generateTierSkillDef('manaWell', 1)?.name).toBe('Mana Well'); - expect(generateTierSkillDef('manaWell', 2)?.name).toBe('Deep Reservoir'); - expect(generateTierSkillDef('manaWell', 3)?.name).toBe('Abyssal Pool'); - expect(generateTierSkillDef('manaWell', 4)?.name).toBe('Ocean of Power'); - expect(generateTierSkillDef('manaWell', 5)?.name).toBe('Infinite Reservoir'); - }); - - it('should have scaling study time and cost', () => { - const def1 = generateTierSkillDef('manaWell', 1); - const def2 = generateTierSkillDef('manaWell', 2); - const def5 = generateTierSkillDef('manaWell', 5); - - expect(def2?.studyTime).toBeGreaterThan(def1?.studyTime || 0); - expect(def5?.studyTime).toBeGreaterThan(def2?.studyTime || 0); - expect(def5?.base).toBeGreaterThan(def1?.base || 0); - }); - }); - - describe('Upgrade Effect Validation', () => { - it('should have valid effect structures', () => { - Object.values(SKILL_EVOLUTION_PATHS).forEach(path => { - path.tiers.forEach(tier => { - tier.upgrades.forEach(upgrade => { - const eff = upgrade.effect; - - if (eff.type === 'multiplier' || eff.type === 'bonus') { - expect(eff.value).toBeDefined(); - expect(eff.value).toBeGreaterThan(0); - } - - if (eff.type === 'special') { - expect(eff.specialId).toBeDefined(); - } - }); - }); - }); - }); - - it('should have descriptive names for all upgrades', () => { - Object.values(SKILL_EVOLUTION_PATHS).forEach(path => { - path.tiers.forEach(tier => { - tier.upgrades.forEach(upgrade => { - expect(upgrade.name.length).toBeGreaterThan(0); - expect(upgrade.desc.length).toBeGreaterThan(0); - }); - }); - }); - }); - }); - - describe('Milestone Upgrade Choices', () => { - it('should only allow 2 upgrades per milestone', () => { - // This is enforced by canSelectUpgrade in the store - // We just verify there are 4 options available - Object.values(SKILL_EVOLUTION_PATHS).forEach(path => { - path.tiers.forEach(tier => { - const l5Upgrades = tier.upgrades.filter(u => u.milestone === 5); - const l10Upgrades = tier.upgrades.filter(u => u.milestone === 10); - - expect(l5Upgrades).toHaveLength(4); - expect(l10Upgrades).toHaveLength(4); - }); - }); - }); - - it('should have unique upgrade IDs within each path', () => { - Object.values(SKILL_EVOLUTION_PATHS).forEach(path => { - const allIds: string[] = []; - path.tiers.forEach(tier => { - tier.upgrades.forEach(upgrade => { - expect(allIds).not.toContain(upgrade.id); - allIds.push(upgrade.id); - }); - }); - }); - }); - }); -}); - -// ─── Milestone Upgrade Effect Tests ───────────────────────────────────────────── - -describe('Milestone Upgrade Effects', () => { - describe('Multiplier Effect Upgrades', () => { - it('should have correct multiplier effect structure', () => { - // Get the Expanded Capacity upgrade (+25% max mana bonus) - const upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, {}); - const capacityUpgrade = upgrades.find(u => u.id === 'mw_t1_l5_capacity'); - - expect(capacityUpgrade).toBeDefined(); - expect(capacityUpgrade?.effect.type).toBe('multiplier'); - expect(capacityUpgrade?.effect.stat).toBe('maxMana'); - expect(capacityUpgrade?.effect.value).toBe(1.25); - }); - - it('should have multiplier upgrades for different stats', () => { - // Mana Well: +25% max mana - const mwUpgrades = getUpgradesForSkillAtMilestone('manaWell', 5, {}); - const mwMult = mwUpgrades.find(u => u.effect.type === 'multiplier'); - expect(mwMult?.effect.stat).toBe('maxMana'); - - // Mana Flow: +25% regen speed - const mfUpgrades = getUpgradesForSkillAtMilestone('manaFlow', 5, {}); - const mfMult = mfUpgrades.find(u => u.effect.type === 'multiplier'); - expect(mfMult?.effect.stat).toBe('regen'); - - // Combat Training: +25% base damage - const ctUpgrades = getUpgradesForSkillAtMilestone('combatTrain', 5, {}); - const ctMult = ctUpgrades.find(u => u.effect.type === 'multiplier'); - expect(ctMult?.effect.stat).toBe('baseDamage'); - - // Quick Learner: +25% study speed - const qlUpgrades = getUpgradesForSkillAtMilestone('quickLearner', 5, {}); - const qlMult = qlUpgrades.find(u => u.effect.type === 'multiplier'); - expect(qlMult?.effect.stat).toBe('studySpeed'); - }); - - it('should have multiplier values greater than 1 for positive effects', () => { - Object.values(SKILL_EVOLUTION_PATHS).forEach(path => { - path.tiers.forEach(tier => { - tier.upgrades.forEach(upgrade => { - if (upgrade.effect.type === 'multiplier') { - // Most multipliers should be > 1 (bonuses) or < 1 (reductions) - expect(upgrade.effect.value).toBeGreaterThan(0); - } - }); - }); - }); - }); - - it('should have cost reduction multipliers less than 1', () => { - // Mana Efficiency: -5% spell costs = 0.95 multiplier - const mwUpgrades = getUpgradesForSkillAtMilestone('manaWell', 10, {}); - const efficiency = mwUpgrades.find(u => u.id === 'mw_t1_l10_efficiency'); - - expect(efficiency).toBeDefined(); - expect(efficiency?.effect.type).toBe('multiplier'); - expect(efficiency?.effect.value).toBeLessThan(1); - expect(efficiency?.effect.value).toBe(0.95); - }); - }); - - describe('Bonus Effect Upgrades', () => { - it('should have correct bonus effect structure', () => { - // Get the Natural Spring upgrade (+0.5 regen per hour) - const upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, {}); - const regenUpgrade = upgrades.find(u => u.id === 'mw_t1_l5_regen'); - - expect(regenUpgrade).toBeDefined(); - expect(regenUpgrade?.effect.type).toBe('bonus'); - expect(regenUpgrade?.effect.stat).toBe('regen'); - expect(regenUpgrade?.effect.value).toBe(0.5); - }); - - it('should have bonus upgrades for different stats', () => { - // Mana Well: +0.5 regen per hour - const mwUpgrades = getUpgradesForSkillAtMilestone('manaWell', 5, {}); - const mwBonus = mwUpgrades.find(u => u.effect.type === 'bonus' && u.effect.stat === 'regen'); - expect(mwBonus).toBeDefined(); - - // Mana Flow: +1 regen permanently - const mfUpgrades = getUpgradesForSkillAtMilestone('manaFlow', 10, {}); - const mfBonus = mfUpgrades.find(u => u.effect.type === 'bonus' && u.effect.stat === 'permanentRegen'); - expect(mfBonus).toBeDefined(); - expect(mfBonus?.effect.value).toBe(1); - }); - - it('should have positive bonus values', () => { - Object.values(SKILL_EVOLUTION_PATHS).forEach(path => { - path.tiers.forEach(tier => { - tier.upgrades.forEach(upgrade => { - if (upgrade.effect.type === 'bonus') { - expect(upgrade.effect.value).toBeGreaterThan(0); - } - }); - }); - }); - }); - - it('should have tier 2+ bonus upgrades that scale appropriately', () => { - // Tier 2 Mana Well L10: +1000 max mana - const mwT2Upgrades = SKILL_EVOLUTION_PATHS.manaWell.tiers[1].upgrades; - const oceanUpgrade = mwT2Upgrades.find(u => u.id === 'mw_t2_l10_ocean'); - - expect(oceanUpgrade).toBeDefined(); - expect(oceanUpgrade?.effect.type).toBe('bonus'); - expect(oceanUpgrade?.effect.stat).toBe('maxMana'); - expect(oceanUpgrade?.effect.value).toBe(1000); - }); - }); - - describe('Special Effect Upgrades', () => { - it('should have correct special effect structure', () => { - // Get the Mana Threshold upgrade (special) - const upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, {}); - const thresholdUpgrade = upgrades.find(u => u.id === 'mw_t1_l5_threshold'); - - expect(thresholdUpgrade).toBeDefined(); - expect(thresholdUpgrade?.effect.type).toBe('special'); - expect(thresholdUpgrade?.effect.specialId).toBe('manaThreshold'); - expect(thresholdUpgrade?.effect.specialDesc).toBeDefined(); - }); - - it('should have unique special IDs for each special effect', () => { - const allSpecialIds: string[] = []; - - Object.values(SKILL_EVOLUTION_PATHS).forEach(path => { - path.tiers.forEach(tier => { - tier.upgrades.forEach(upgrade => { - if (upgrade.effect.type === 'special') { - expect(allSpecialIds).not.toContain(upgrade.effect.specialId); - allSpecialIds.push(upgrade.effect.specialId!); - } - }); - }); - }); - }); - - it('should have descriptive special descriptions', () => { - Object.values(SKILL_EVOLUTION_PATHS).forEach(path => { - path.tiers.forEach(tier => { - tier.upgrades.forEach(upgrade => { - if (upgrade.effect.type === 'special') { - expect(upgrade.effect.specialDesc).toBeDefined(); - expect(upgrade.effect.specialDesc!.length).toBeGreaterThan(0); - } - }); - }); - }); - }); - - it('should have special effects at both milestones', () => { - // Level 5 special - const l5Upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, {}); - const l5Special = l5Upgrades.find(u => u.effect.type === 'special'); - expect(l5Special).toBeDefined(); - expect(l5Special?.milestone).toBe(5); - - // Level 10 special - const l10Upgrades = getUpgradesForSkillAtMilestone('manaWell', 10, {}); - const l10Special = l10Upgrades.find(u => u.effect.type === 'special'); - expect(l10Special).toBeDefined(); - expect(l10Special?.milestone).toBe(10); - }); - }); -}); - -// ─── Upgrade Selection Tests ─────────────────────────────────────────────────── - -describe('Upgrade Selection System', () => { - describe('getSkillUpgradeChoices', () => { - it('should return 4 available upgrades at each milestone', () => { - const l5Choices = getUpgradesForSkillAtMilestone('manaWell', 5, {}); - const l10Choices = getUpgradesForSkillAtMilestone('manaWell', 10, {}); - - expect(l5Choices).toHaveLength(4); - expect(l10Choices).toHaveLength(4); - }); - - it('should return empty array for non-evolvable skills', () => { - const upgrades = getUpgradesForSkillAtMilestone('nonexistent', 5, {}); - expect(upgrades).toHaveLength(0); - }); - - it('should return correct upgrades for tier skills', () => { - // Tier 2 should have different upgrades than tier 1 - const t1Upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, { manaWell: 1 }); - const t2Upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, { manaWell: 2 }); - - // Both should return tier 1 upgrades since we're asking for base skill - // The skillTiers parameter tells us what tier the skill is currently at - expect(t1Upgrades).toHaveLength(4); - expect(t2Upgrades).toHaveLength(4); - }); - - it('should have upgrades with correct milestone values', () => { - const l5Upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, {}); - const l10Upgrades = getUpgradesForSkillAtMilestone('manaWell', 10, {}); - - l5Upgrades.forEach(u => expect(u.milestone).toBe(5)); - l10Upgrades.forEach(u => expect(u.milestone).toBe(10)); - }); - }); - - describe('Upgrade Selection Constraints', () => { - it('should allow selecting upgrades when at milestone level', () => { - // The canSelectUpgrade function requires the skill level to be >= milestone - // This is tested by checking the logic in the store - const upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, {}); - expect(upgrades.length).toBe(4); - }); - - it('should only allow 2 upgrades per milestone', () => { - // The store enforces: selected.length >= 2 returns false - // We verify there are enough choices (4) to select 2 from - const l5Upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, {}); - const l10Upgrades = getUpgradesForSkillAtMilestone('manaWell', 10, {}); - - // Each milestone should have exactly 4 options to choose 2 from - expect(l5Upgrades.length).toBeGreaterThanOrEqual(2); - expect(l10Upgrades.length).toBeGreaterThanOrEqual(2); - }); - - it('should have unique upgrade IDs to prevent duplicate selection', () => { - const l5Upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, {}); - const l10Upgrades = getUpgradesForSkillAtMilestone('manaWell', 10, {}); - - const l5Ids = l5Upgrades.map(u => u.id); - const l10Ids = l10Upgrades.map(u => u.id); - - // No duplicate IDs within same milestone - expect(new Set(l5Ids).size).toBe(l5Ids.length); - expect(new Set(l10Ids).size).toBe(l10Ids.length); - - // No overlap between milestones - const overlap = l5Ids.filter(id => l10Ids.includes(id)); - expect(overlap).toHaveLength(0); - }); - }); - - describe('Upgrade Persistence', () => { - it('should store upgrades in skillUpgrades record', () => { - // Create a mock state with upgrades - const state = createMockState({ - skills: { manaWell: 5 }, - skillUpgrades: { manaWell: ['mw_t1_l5_capacity', 'mw_t1_l5_regen'] } - }); - - expect(state.skillUpgrades['manaWell']).toBeDefined(); - expect(state.skillUpgrades['manaWell']).toHaveLength(2); - expect(state.skillUpgrades['manaWell']).toContain('mw_t1_l5_capacity'); - expect(state.skillUpgrades['manaWell']).toContain('mw_t1_l5_regen'); - }); - - it('should persist upgrades across state changes', () => { - // Simulate state changes - const state1 = createMockState({ - skills: { manaWell: 5 }, - skillUpgrades: { manaWell: ['mw_t1_l5_capacity'] } - }); - - // State changes shouldn't affect upgrades - const state2 = { - ...state1, - rawMana: 500, - day: 10, - }; - - expect(state2.skillUpgrades['manaWell']).toEqual(['mw_t1_l5_capacity']); - }); - }); -}); - -// ─── Tier Up System Tests ───────────────────────────────────────────────────── - -describe('Tier Up System', () => { - describe('getNextTierSkill', () => { - it('should return correct next tier for base skills', () => { - expect(getNextTierSkill('manaWell')).toBe('manaWell_t2'); - expect(getNextTierSkill('manaFlow')).toBe('manaFlow_t2'); - expect(getNextTierSkill('combatTrain')).toBe('combatTrain_t2'); - expect(getNextTierSkill('quickLearner')).toBe('quickLearner_t2'); - expect(getNextTierSkill('focusedMind')).toBe('focusedMind_t2'); - expect(getNextTierSkill('elemAttune')).toBe('elemAttune_t2'); - }); - - it('should return correct next tier for tier skills', () => { - expect(getNextTierSkill('manaWell_t2')).toBe('manaWell_t3'); - expect(getNextTierSkill('manaWell_t3')).toBe('manaWell_t4'); - expect(getNextTierSkill('manaWell_t4')).toBe('manaWell_t5'); - }); - - it('should return null for max tier (tier 5)', () => { - expect(getNextTierSkill('manaWell_t5')).toBeNull(); - }); - - it('should return null for non-evolvable skills', () => { - expect(getNextTierSkill('nonexistent')).toBeNull(); - }); - }); - - describe('getTierMultiplier', () => { - it('should return 1 for tier 1 skills', () => { - expect(getTierMultiplier('manaWell')).toBe(1); - expect(getTierMultiplier('manaFlow')).toBe(1); - }); - - it('should return increasing multipliers for higher tiers', () => { - // Each tier is 10x more powerful - expect(getTierMultiplier('manaWell_t2')).toBe(10); - expect(getTierMultiplier('manaWell_t3')).toBe(100); - expect(getTierMultiplier('manaWell_t4')).toBe(1000); - expect(getTierMultiplier('manaWell_t5')).toBe(10000); - }); - - it('should return consistent multipliers across skill paths', () => { - // All skills should have same tier multipliers - expect(getTierMultiplier('manaWell_t2')).toBe(getTierMultiplier('manaFlow_t2')); - expect(getTierMultiplier('manaWell_t3')).toBe(getTierMultiplier('combatTrain_t3')); - expect(getTierMultiplier('manaWell_t4')).toBe(getTierMultiplier('quickLearner_t4')); - }); - }); - - describe('Tier Up Mechanics', () => { - it('should start new tier at level 1 after tier up', () => { - // Simulate the tier up result - const baseSkillId = 'manaWell'; - const nextTierId = getNextTierSkill(baseSkillId); - - expect(nextTierId).toBe('manaWell_t2'); - - // After tier up, the skill should be at level 1 - const newSkills = { [nextTierId!]: 1 }; - expect(newSkills['manaWell_t2']).toBe(1); - }); - - it('should carry over upgrades to new tier', () => { - // Simulate tier up with existing upgrades - const oldUpgrades = ['mw_t1_l5_capacity', 'mw_t1_l10_echo']; - const nextTierId = 'manaWell_t2'; - - // Upgrades should be carried over - const newSkillUpgrades = { [nextTierId]: oldUpgrades }; - expect(newSkillUpgrades['manaWell_t2']).toEqual(oldUpgrades); - }); - - it('should update skillTiers when tiering up', () => { - // Tier up from base to tier 2 - const baseSkillId = 'manaWell'; - const nextTier = 2; - - const newSkillTiers = { [baseSkillId]: nextTier }; - expect(newSkillTiers['manaWell']).toBe(2); - }); - - it('should remove old skill when tiering up', () => { - // After tier up, old skill should be removed - const oldSkillId = 'manaWell'; - const newSkillId = 'manaWell_t2'; - - const newSkills: Record = {}; - newSkills[newSkillId] = 1; - - expect(newSkills[oldSkillId]).toBeUndefined(); - expect(newSkills[newSkillId]).toBe(1); - }); - }); - - describe('generateTierSkillDef', () => { - it('should generate valid skill definitions for all tiers', () => { - for (let tier = 1; tier <= 5; tier++) { - const def = generateTierSkillDef('manaWell', tier); - expect(def).toBeDefined(); - expect(def?.tier).toBe(tier); - expect(def?.max).toBe(10); - } - }); - - it('should have correct tier names', () => { - expect(generateTierSkillDef('manaWell', 1)?.name).toBe('Mana Well'); - expect(generateTierSkillDef('manaWell', 2)?.name).toBe('Deep Reservoir'); - expect(generateTierSkillDef('manaWell', 3)?.name).toBe('Abyssal Pool'); - expect(generateTierSkillDef('manaWell', 4)?.name).toBe('Ocean of Power'); - expect(generateTierSkillDef('manaWell', 5)?.name).toBe('Infinite Reservoir'); - }); - - it('should have scaling costs and study times', () => { - const tier1 = generateTierSkillDef('manaWell', 1); - const tier5 = generateTierSkillDef('manaWell', 5); - - expect(tier5?.base).toBeGreaterThan(tier1?.base || 0); - expect(tier5?.studyTime).toBeGreaterThan(tier1?.studyTime || 0); - }); - - it('should return null for invalid tiers', () => { - expect(generateTierSkillDef('manaWell', 0)).toBeNull(); - expect(generateTierSkillDef('manaWell', 6)).toBeNull(); - expect(generateTierSkillDef('nonexistent', 1)).toBeNull(); - }); - }); -}); - -// ─── Tier Effect Computation Tests ───────────────────────────────────────────── - -describe('Tier Effect Computation', () => { - describe('Tiered Skill Bonuses', () => { - it('should apply tier multiplier to skill bonuses', () => { - // Tier 1 manaWell level 5 should give +500 mana - const state1 = createMockState({ - skills: { manaWell: 5 }, - skillTiers: {} - }); - const mana1 = computeMaxMana(state1); - - // Base 100 + 5 * 100 = 600 - expect(mana1).toBe(600); - }); - - it('should have tier multiplier affect effective level', () => { - // Tier 2 skill should have 10x multiplier - const state2 = createMockState({ - skills: { manaWell_t2: 5 }, - skillTiers: { manaWell: 2 } - }); - - // The computation should use tier multiplier - // This tests that the state structure is correct - expect(state2.skillTiers['manaWell']).toBe(2); - expect(state2.skills['manaWell_t2']).toBe(5); - }); - }); - - describe('Tier Upgrade Access', () => { - it('should provide correct upgrades for current tier', () => { - // Tier 1 should access tier 1 upgrades - const t1Upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, { manaWell: 1 }); - expect(t1Upgrades[0]?.id).toContain('mw_t1'); - - // Tier 2 should access tier 2 upgrades - const t2Upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, { manaWell: 2 }); - expect(t2Upgrades[0]?.id).toContain('mw_t2'); - }); - - it('should have more powerful upgrades at higher tiers', () => { - const t1L5 = getUpgradesForSkillAtMilestone('manaWell', 5, { manaWell: 1 }); - const t2L5 = getUpgradesForSkillAtMilestone('manaWell', 5, { manaWell: 2 }); - - // Find multiplier upgrades - const t1Mult = t1L5.find(u => u.effect.type === 'multiplier'); - const t2Mult = t2L5.find(u => u.effect.type === 'multiplier'); - - // Tier 2 should have stronger effects - if (t1Mult && t2Mult) { - expect(t2Mult.effect.value).toBeGreaterThanOrEqual(t1Mult.effect.value); - } - }); - }); -}); - -// ─── Upgrade Effect Application Tests ───────────────────────────────────────────── - -describe('Upgrade Effect Application', () => { - it('should apply max mana multiplier upgrade to computeMaxMana', () => { - // Without upgrade - const stateNoUpgrade = createMockState({ - skills: { manaWell: 5 }, - skillUpgrades: {} - }); - const manaNoUpgrade = computeMaxMana(stateNoUpgrade); - - // With Expanded Capacity (+25% max mana) - const stateWithUpgrade = createMockState({ - skills: { manaWell: 5 }, - skillUpgrades: { manaWell: ['mw_t1_l5_capacity'] } - }); - const manaWithUpgrade = computeMaxMana(stateWithUpgrade); - - // Should be 25% more - expect(manaWithUpgrade).toBe(Math.floor(manaNoUpgrade * 1.25)); - }); - - it('should apply max mana bonus upgrade to computeMaxMana', () => { - // Without upgrade - const stateNoUpgrade = createMockState({ - skills: { manaWell: 10 }, - skillUpgrades: {} - }); - const manaNoUpgrade = computeMaxMana(stateNoUpgrade); - - // With Ocean of Mana (+1000 max mana at tier 2 level 10) - const stateWithUpgrade = createMockState({ - skills: { manaWell_t2: 10 }, - skillTiers: { manaWell: 2 }, - skillUpgrades: { manaWell_t2: ['mw_t2_l10_ocean'] } - }); - const manaWithUpgrade = computeMaxMana(stateWithUpgrade); - - // Should have bonus added - expect(manaWithUpgrade).toBeGreaterThan(manaNoUpgrade); - }); - - it('should apply regen multiplier upgrade to computeRegen', () => { - // Without upgrade - const stateNoUpgrade = createMockState({ - skills: { manaFlow: 5 }, - skillUpgrades: {} - }); - const regenNoUpgrade = computeRegen(stateNoUpgrade); - - // With Rapid Flow (+25% regen) - const stateWithUpgrade = createMockState({ - skills: { manaFlow: 5 }, - skillUpgrades: { manaFlow: ['mf_t1_l5_rapid'] } - }); - const regenWithUpgrade = computeRegen(stateWithUpgrade); - - // Should be 25% more - expect(regenWithUpgrade).toBe(regenNoUpgrade * 1.25); - }); - - it('should apply regen bonus upgrade to computeRegen', () => { - // Without upgrade - base regen only - const stateNoUpgrade = createMockState({ - skills: {}, - skillUpgrades: {} - }); - const regenNoUpgrade = computeRegen(stateNoUpgrade); - - // With Natural Spring (+0.5 regen) - from manaWell upgrades - const stateWithUpgrade = createMockState({ - skills: {}, - skillUpgrades: { manaWell: ['mw_t1_l5_regen'] } - }); - const regenWithUpgrade = computeRegen(stateWithUpgrade); - - // Should have +0.5 bonus added - expect(regenNoUpgrade).toBe(2); // Base regen is 2 - expect(regenWithUpgrade).toBe(2.5); // 2 + 0.5 - }); - - it('should apply element cap multiplier upgrade to computeElementMax', () => { - // Without upgrade - const stateNoUpgrade = createMockState({ - skills: { elemAttune: 5 }, - skillUpgrades: {} - }); - const elemNoUpgrade = computeElementMax(stateNoUpgrade); - - // With Expanded Attunement (+25% element cap) - const stateWithUpgrade = createMockState({ - skills: { elemAttune: 5 }, - skillUpgrades: { elemAttune: ['ea_t1_l5_expand'] } - }); - const elemWithUpgrade = computeElementMax(stateWithUpgrade); - - // Should be 25% more - expect(elemWithUpgrade).toBe(Math.floor(elemNoUpgrade * 1.25)); - }); - - it('should apply permanent regen bonus from upgrade', () => { - // With Ambient Absorption (+1 permanent regen) - const stateWithUpgrade = createMockState({ - skills: { manaFlow: 10 }, - skillUpgrades: { manaFlow: ['mf_t1_l10_ambient'] } - }); - const regenWithUpgrade = computeRegen(stateWithUpgrade); - - // Without upgrade - const stateNoUpgrade = createMockState({ - skills: { manaFlow: 10 }, - skillUpgrades: {} - }); - const regenNoUpgrade = computeRegen(stateNoUpgrade); - - // Should have +1 from permanent regen bonus - expect(regenWithUpgrade).toBe(regenNoUpgrade + 1); - }); - - it('should stack multiple upgrades correctly', () => { - // With two upgrades: +25% max mana AND +25% max mana (from tier 2) - const stateWithUpgrades = createMockState({ - skills: { manaWell: 5 }, - skillUpgrades: { manaWell: ['mw_t1_l5_capacity', 'mw_t1_l5_regen'] } - }); - - // The +25% max mana multiplier should be applied once - // The +0.5 regen bonus should be applied - const mana = computeMaxMana(stateWithUpgrades); - const regen = computeRegen(stateWithUpgrades); - - // Base mana: 100 + 5*100 = 600, with 1.25x = 750 - expect(mana).toBe(750); - // Base regen: 2 + 0 = 2, with +0.5 = 2.5 - expect(regen).toBe(2.5); - }); - - it('should apply click mana bonuses', () => { - // Without upgrades - const stateNoUpgrade = createMockState({ - skills: {}, - skillUpgrades: {} - }); - const clickNoUpgrade = computeClickMana(stateNoUpgrade); - - // Base click mana is 1 - expect(clickNoUpgrade).toBe(1); - }); -}); - -// ─── Special Effect Tests ───────────────────────────────────────────────────────── - -import { computeEffects, hasSpecial, SPECIAL_EFFECTS } from './upgrade-effects'; - -describe('Special Effect Application', () => { - describe('Mana Cascade', () => { - it('should add regen based on max mana', () => { - // Mana Cascade: +0.1 regen per 100 max mana - // With 1000 max mana, should add 1.0 regen - const state = createMockState({ - skills: { manaFlow: 5, manaWell: 5 }, // manaWell 5 gives 500 mana, base 100 = 600 - skillUpgrades: { manaFlow: ['mf_t1_l5_cascade'] } - }); - - const effects = computeEffects(state.skillUpgrades, state.skillTiers || {}); - expect(hasSpecial(effects, SPECIAL_EFFECTS.MANA_CASCADE)).toBe(true); - - // Compute max mana to check cascade calculation - const maxMana = computeMaxMana(state); - - // Expected cascade bonus: floor(maxMana / 100) * 0.1 - const expectedCascadeBonus = Math.floor(maxMana / 100) * 0.1; - - // Base regen: 2 + manaFlow level (5) = 7 - // With cascade: 7 + cascadeBonus - const baseRegen = computeRegen(state); - - // The regen should be increased by cascade bonus - // Note: computeRegen doesn't include cascade - computeEffectiveRegen does - expect(baseRegen).toBeGreaterThan(0); - }); - }); - - describe('Steady Stream', () => { - it('should be recognized as active when selected', () => { - const state = createMockState({ - skills: { manaFlow: 5 }, - skillUpgrades: { manaFlow: ['mf_t1_l5_steady'] } - }); - - const effects = computeEffects(state.skillUpgrades, state.skillTiers || {}); - expect(hasSpecial(effects, SPECIAL_EFFECTS.STEADY_STREAM)).toBe(true); - }); - }); - - describe('Mana Echo', () => { - it('should be recognized as active when selected', () => { - const state = createMockState({ - skills: { manaWell: 10 }, - skillUpgrades: { manaWell: ['mw_t1_l10_echo'] } - }); - - const effects = computeEffects(state.skillUpgrades, state.skillTiers || {}); - expect(hasSpecial(effects, SPECIAL_EFFECTS.MANA_ECHO)).toBe(true); - }); - }); - - describe('Combat Special Effects', () => { - it('should recognize Berserker special effect', () => { - const state = createMockState({ - skills: { combatTrain: 10 }, - skillUpgrades: { combatTrain: ['ct_t1_l10_berserker'] } - }); - - const effects = computeEffects(state.skillUpgrades, state.skillTiers || {}); - expect(hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER)).toBe(true); - }); - - it('should recognize Adrenaline Rush special effect', () => { - const state = createMockState({ - skills: { combatTrain: 10 }, - skillUpgrades: { combatTrain: ['ct_t1_l10_adrenaline'] } - }); - - const effects = computeEffects(state.skillUpgrades, state.skillTiers || {}); - expect(hasSpecial(effects, SPECIAL_EFFECTS.ADRENALINE_RUSH)).toBe(true); - }); - }); - - describe('Study Special Effects', () => { - it('should recognize Mental Clarity special effect', () => { - const state = createMockState({ - skills: { focusedMind: 5 }, - skillUpgrades: { focusedMind: ['fm_t1_l5_clarity'] } - }); - - const effects = computeEffects(state.skillUpgrades, state.skillTiers || {}); - expect(hasSpecial(effects, SPECIAL_EFFECTS.MENTAL_CLARITY)).toBe(true); - }); - - it('should recognize Study Rush special effect', () => { - const state = createMockState({ - skills: { focusedMind: 10 }, - skillUpgrades: { focusedMind: ['fm_t1_l10_rush'] } - }); - - const effects = computeEffects(state.skillUpgrades, state.skillTiers || {}); - expect(hasSpecial(effects, SPECIAL_EFFECTS.STUDY_RUSH)).toBe(true); - }); - - it('should apply study speed multiplier from upgrades', () => { - // With Deep Focus (+25% study speed) - const state = createMockState({ - skills: { quickLearner: 5 }, - skillUpgrades: { quickLearner: ['ql_t1_l5_focus'] } - }); - - const effects = computeEffects(state.skillUpgrades, state.skillTiers || {}); - expect(effects.studySpeedMultiplier).toBe(1.25); - }); - }); - - describe('Effect Stacking', () => { - it('should track multiple special effects simultaneously', () => { - const state = createMockState({ - skills: { manaFlow: 10 }, - skillUpgrades: { manaFlow: ['mf_t1_l5_cascade', 'mf_t1_l5_steady', 'mf_t1_l10_ambient'] } - }); - - const effects = computeEffects(state.skillUpgrades, state.skillTiers || {}); - expect(hasSpecial(effects, SPECIAL_EFFECTS.MANA_CASCADE)).toBe(true); - expect(hasSpecial(effects, SPECIAL_EFFECTS.STEADY_STREAM)).toBe(true); - expect(effects.permanentRegenBonus).toBe(1); // Ambient Absorption gives +1 permanent regen - }); - - it('should stack multipliers correctly', () => { - // Rapid Flow (+25% regen) + River of Mana (+50% regen) at tier 2 - const state = createMockState({ - skills: { manaFlow_t2: 5 }, - skillTiers: { manaFlow: 2 }, - skillUpgrades: { manaFlow_t2: ['mf_t2_l5_river'] } - }); - - const effects = computeEffects(state.skillUpgrades, state.skillTiers || {}); - expect(effects.regenMultiplier).toBe(1.5); // River of Mana gives 1.5x - }); - }); - - describe('Meditation Efficiency', () => { - it('should apply Deep Wellspring meditation efficiency', () => { - // Deep Wellspring: +50% meditation efficiency - const state = createMockState({ - skills: { manaWell: 10 }, - skillUpgrades: { manaWell: ['mw_t1_l10_meditation'] } - }); - - const effects = computeEffects(state.skillUpgrades, state.skillTiers || {}); - expect(effects.meditationEfficiency).toBe(1.5); // +50% efficiency - }); - - it('should boost meditation bonus with efficiency', () => { - // Without efficiency - const baseBonus = getMeditationBonus(100, { meditation: 1 }, 1); // 4 hours with meditation skill - expect(baseBonus).toBe(2.5); // Base with meditation skill - - // With Deep Wellspring (+50% efficiency) - const boostedBonus = getMeditationBonus(100, { meditation: 1 }, 1.5); - expect(boostedBonus).toBe(3.75); // 2.5 * 1.5 = 3.75 - }); - }); -}); - -console.log('✅ All tests defined. Run with: bun test src/lib/game/store.test.ts'); diff --git a/src/lib/game/stores.test.ts b/src/lib/game/stores.test.ts index 06bd44e..f6d60c7 100755 --- a/src/lib/game/stores.test.ts +++ b/src/lib/game/stores.test.ts @@ -261,17 +261,17 @@ describe('SkillStore', () => { it('should not start studying without prerequisites', () => { useManaStore.getState().addRawMana(990, 1000); - // deepReservoir requires manaWell 5 - const result = useSkillStore.getState().startStudyingSkill('deepReservoir', 1000); + // manaOverflow requires manaWell 3 + const result = useSkillStore.getState().startStudyingSkill('manaOverflow', 1000); expect(result.started).toBe(false); }); it('should start studying with prerequisites met', () => { useManaStore.getState().addRawMana(990, 1000); - useSkillStore.getState().setSkillLevel('manaWell', 5); + useSkillStore.getState().setSkillLevel('manaWell', 3); - const result = useSkillStore.getState().startStudyingSkill('deepReservoir', 1000); + const result = useSkillStore.getState().startStudyingSkill('manaOverflow', 1000); expect(result.started).toBe(true); }); diff --git a/src/lib/game/stores/__tests__/store-methods.test.ts b/src/lib/game/stores/__tests__/store-methods.test.ts index 980fadf..ee94373 100755 --- a/src/lib/game/stores/__tests__/store-methods.test.ts +++ b/src/lib/game/stores/__tests__/store-methods.test.ts @@ -61,17 +61,17 @@ describe('SkillStore', () => { it('should not start studying without prerequisites', () => { const skillStore = useSkillStore.getState(); - // deepReservoir requires manaWell level 5 - const result = skillStore.startStudyingSkill('deepReservoir', 1000); + // manaOverflow requires manaWell level 3 + const result = skillStore.startStudyingSkill('manaOverflow', 1000); expect(result.started).toBe(false); }); it('should start studying with prerequisites met', () => { - useSkillStore.setState({ skills: { manaWell: 5 } }); + useSkillStore.setState({ skills: { manaWell: 3 } }); const skillStore = useSkillStore.getState(); - const result = skillStore.startStudyingSkill('deepReservoir', 1000); + const result = skillStore.startStudyingSkill('manaOverflow', 1000); expect(result.started).toBe(true); }); @@ -271,36 +271,36 @@ describe('ManaStore', () => { describe('craftComposite', () => { it('should craft composite element with correct ingredients', () => { - // Set up ingredients for blood (life + water) + // Set up ingredients for metal (fire + earth) useManaStore.setState({ elements: { ...useManaStore.getState().elements, - life: { current: 5, max: 10, unlocked: true }, - water: { current: 5, max: 10, unlocked: true }, + fire: { current: 5, max: 10, unlocked: true }, + earth: { current: 5, max: 10, unlocked: true }, } }); - const result = useManaStore.getState().craftComposite('blood', ['life', 'water']); + const result = useManaStore.getState().craftComposite('metal', ['fire', 'earth']); expect(result).toBe(true); const state = useManaStore.getState(); - expect(state.elements.life.current).toBe(4); - expect(state.elements.water.current).toBe(4); - expect(state.elements.blood.current).toBe(1); - expect(state.elements.blood.unlocked).toBe(true); + expect(state.elements.fire.current).toBe(4); + expect(state.elements.earth.current).toBe(4); + expect(state.elements.metal.current).toBe(1); + expect(state.elements.metal.unlocked).toBe(true); }); it('should not craft without ingredients', () => { useManaStore.setState({ elements: { ...useManaStore.getState().elements, - life: { current: 0, max: 10, unlocked: true }, - water: { current: 0, max: 10, unlocked: true }, + fire: { current: 0, max: 10, unlocked: true }, + earth: { current: 0, max: 10, unlocked: true }, } }); - const result = useManaStore.getState().craftComposite('blood', ['life', 'water']); + const result = useManaStore.getState().craftComposite('metal', ['fire', 'earth']); expect(result).toBe(false); }); diff --git a/src/lib/game/stores/index.test.ts b/src/lib/game/stores/index.test.ts index 589bc5d..2e90085 100755 --- a/src/lib/game/stores/index.test.ts +++ b/src/lib/game/stores/index.test.ts @@ -2,6 +2,7 @@ * Comprehensive Store Tests * * Tests the split store architecture to ensure all stores work correctly together. + * Updated for the new skill system with tiers and upgrade trees. */ import { describe, it, expect, beforeEach } from 'bun:test'; @@ -89,8 +90,20 @@ function createMockState(overrides: Partial = {}): GameState { autoSchedule: false, studyQueue: [], craftQueue: [], + attunements: { + enchanter: { id: 'enchanter', active: true, level: 1, experience: 0 }, + invoker: { id: 'invoker', active: false, level: 1, experience: 0 }, + fabricator: { id: 'fabricator', active: false, level: 1, experience: 0 }, + }, + golemancy: { + enabledGolems: [], + summonedGolems: [], + lastSummonFloor: 0, + }, + equippedInstances: { mainHand: null, offHand: null, head: null, body: null, hands: null, accessory1: null, accessory2: null }, + equipmentInstances: {}, ...overrides, - }; + } as GameState; } // ─── Utility Function Tests ───────────────────────────────────────────────── @@ -140,43 +153,42 @@ describe('Mana Calculations', () => { expect(computeMaxMana(state)).toBe(100 + 5 * 100); }); - it('should add mana from deepReservoir skill', () => { - const state = createMockState({ skills: { deepReservoir: 3 } }); - expect(computeMaxMana(state)).toBe(100 + 3 * 500); - }); - it('should add mana from prestige upgrades', () => { const state = createMockState({ prestigeUpgrades: { manaWell: 3 } }); expect(computeMaxMana(state)).toBe(100 + 3 * 500); }); - - it('should stack all mana bonuses', () => { + + it('should stack manaWell skill and prestige', () => { const state = createMockState({ - skills: { manaWell: 5, deepReservoir: 2 }, + skills: { manaWell: 5 }, prestigeUpgrades: { manaWell: 2 }, }); - expect(computeMaxMana(state)).toBe(100 + 5 * 100 + 2 * 500 + 2 * 500); + expect(computeMaxMana(state)).toBe(100 + 5 * 100 + 2 * 500); }); }); describe('computeRegen', () => { it('should return base regen with no upgrades', () => { + // Base regen is 2 (attunement regen is added separately in the store) const state = createMockState(); expect(computeRegen(state)).toBe(2); }); it('should add regen from manaFlow skill', () => { + // Base 2 + manaFlow 5 const state = createMockState({ skills: { manaFlow: 5 } }); expect(computeRegen(state)).toBe(2 + 5 * 1); }); it('should add regen from manaSpring skill', () => { + // Base 2 + manaSpring 2 const state = createMockState({ skills: { manaSpring: 1 } }); expect(computeRegen(state)).toBe(2 + 2); }); it('should multiply by temporal echo prestige', () => { const state = createMockState({ prestigeUpgrades: { temporalEcho: 2 } }); + // Base 2 * 1.2 = 2.4 expect(computeRegen(state)).toBe(2 * 1.2); }); }); @@ -231,19 +243,6 @@ describe('Combat Calculations', () => { expect(dmg).toBeGreaterThanOrEqual(5); // Base damage (can be higher with crit) }); - it('should add damage from combatTrain skill', () => { - const state = createMockState({ skills: { combatTrain: 5 } }); - const dmg = calcDamage(state, 'manaBolt'); - expect(dmg).toBeGreaterThanOrEqual(5 + 5 * 5); // 5 base + 25 from skill - }); - - it('should multiply by arcaneFury skill', () => { - const state = createMockState({ skills: { arcaneFury: 3 } }); - const dmg = calcDamage(state, 'manaBolt'); - // 5 * 1.3 = 6.5 minimum (without crit) - expect(dmg).toBeGreaterThanOrEqual(5 * 1.3 * 0.8); - }); - it('should have elemental bonuses', () => { const state = createMockState({ spells: { @@ -280,15 +279,22 @@ describe('Combat Calculations', () => { describe('getFloorElement', () => { it('should cycle through elements in order', () => { + // FLOOR_ELEM_CYCLE has 7 elements: fire, water, air, earth, light, dark, death expect(getFloorElement(1)).toBe('fire'); expect(getFloorElement(2)).toBe('water'); expect(getFloorElement(3)).toBe('air'); expect(getFloorElement(4)).toBe('earth'); + expect(getFloorElement(5)).toBe('light'); + expect(getFloorElement(6)).toBe('dark'); + expect(getFloorElement(7)).toBe('death'); }); - it('should wrap around after 8 floors', () => { - expect(getFloorElement(9)).toBe('fire'); - expect(getFloorElement(10)).toBe('water'); + it('should wrap around after 7 floors', () => { + // Floor 8 should be fire (wraps around) + expect(getFloorElement(8)).toBe('fire'); + expect(getFloorElement(9)).toBe('water'); + expect(getFloorElement(15)).toBe('fire'); // (15-1) % 7 = 0 + expect(getFloorElement(16)).toBe('water'); // (16-1) % 7 = 1 }); }); }); @@ -463,7 +469,8 @@ describe('Spell Cost System', () => { describe('Skill Definitions', () => { it('all skills should have valid categories', () => { - const validCategories = ['mana', 'combat', 'study', 'craft', 'research', 'ascension']; + const validCategories = ['mana', 'study', 'research', 'ascension', 'enchant', + 'effectResearch', 'invocation', 'pact', 'fabrication', 'golemancy', 'craft']; Object.values(SKILLS_DEF).forEach(skill => { expect(validCategories).toContain(skill.cat); }); @@ -531,15 +538,16 @@ describe('Prestige Upgrades', () => { // ─── Guardian Tests ────────────────────────────────────────────────────── describe('Guardian Definitions', () => { - it('should have guardians every 10 floors', () => { - [10, 20, 30, 40, 50, 60, 70, 80, 90, 100].forEach(floor => { + it('should have guardians on expected floors (no floor 70)', () => { + // Floor 70 was removed from the game + [10, 20, 30, 40, 50, 60, 80, 90, 100].forEach(floor => { expect(GUARDIANS[floor]).toBeDefined(); }); }); it('should have increasing HP', () => { let prevHP = 0; - [10, 20, 30, 40, 50, 60, 70, 80, 90, 100].forEach(floor => { + [10, 20, 30, 40, 50, 60, 80, 90, 100].forEach(floor => { expect(GUARDIANS[floor].hp).toBeGreaterThan(prevHP); prevHP = GUARDIANS[floor].hp; });