Files
Mana-Loop/docs/task5/subtask_5_context.md
T
Refactoring Agent 03815f27ee
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 5m57s
feat: add prestige system and skill upgrades with comprehensive documentation
2026-05-01 15:18:09 +02:00

21 KiB

Context: Task5 (2a Floor Rendering & Identity)

Floor Type Definitions

Room Types (from src/lib/game/constants/rooms.ts and src/lib/game/types/game.ts)

// Room types for spire floors
export type RoomType = 'combat' | 'puzzle' | 'swarm' | 'speed' | 'guardian';

// Room generation rules:
// - Guardian floors (10, 20, 30, etc.) are ALWAYS guardian type
// - Every 5th floor (5, 15, 25, etc.) has a chance for special rooms
// - Other floors are combat with chance for swarm/speed
export const PUZZLE_ROOM_INTERVAL = 7; // Every 7 floors, chance for puzzle
export const SWARM_ROOM_CHANCE = 0.15; // 15% chance for swarm room
export const SPEED_ROOM_CHANCE = 0.10; // 10% chance for speed room
export const PUZZLE_ROOM_CHANCE = 0.20; // 20% chance for puzzle room on puzzle floors

Swarm Room Configuration

// Swarm room configuration
export const SWARM_CONFIG = {
  minEnemies: 3,
  maxEnemies: 6,
  hpMultiplier: 0.4,  // Each enemy has 40% of normal floor HP
  armorBase: 0,       // Swarm enemies start with no armor
  armorPerFloor: 0.01, // Gain 1% armor per 10 floors
};

Speed Room Configuration

// Speed room configuration (dodging enemies)
export const SPEED_ROOM_CONFIG = {
  baseDodgeChance: 0.25,  // 25% base dodge chance
  dodgePerFloor: 0.005,   // +0.5% dodge per floor
  maxDodge: 0.50,         // Max 50% dodge
  speedBonus: 0.5,        // 50% less time to complete if dodged
};

Floor Armor Configuration

// Armor scaling for normal floors
export const FLOOR_ARMOR_CONFIG = {
  baseChance: 0,          // No armor on floor 1-9
  chancePerFloor: 0.01,   // +1% chance per floor after 10
  maxArmorChance: 0.5,    // Max 50% of floors have armor
  minArmor: 0.05,         // Min 5% armor
  maxArmor: 0.25,         // Max 25% armor on non-guardians
};

Puzzle Room Definitions

// Puzzle room definitions - themed around attunements
export const PUZZLE_ROOMS: Record<string, {
  name: string;
  attunements: string[];
  baseProgressPerTick: number;
  attunementBonus: number;
  description: string;
}> = {
  enchanter_trial: {
    name: "Enchanter's Trial",
    attunements: ['enchanter'],
    baseProgressPerTick: 0.02,
    attunementBonus: 0.03,
    description: "Decipher ancient enchantment runes."
  },
  fabricator_trial: {
    name: "Fabricator's Trial",
    attunements: ['fabricator'],
    baseProgressPerTick: 0.02,
    attunementBonus: 0.03,
    description: "Construct a mana-powered mechanism."
  },
  invoker_trial: {
    name: "Invoker's Trial",
    attunements: ['invoker'],
    baseProgressPerTick: 0.02,
    attunementBonus: 0.03,
    description: "Commune with guardian spirits."
  },
  // ... hybrid rooms also defined
};

Guardian Definitions (from src/lib/game/constants/guardians.ts)

// All guardians have armor - damage reduction percentage
export const GUARDIANS: Record<number, GuardianDef> = {
  10:  { 
    name: "Ignis Prime", element: "fire", hp: 5000, pact: 1.5, color: "#FF6B35",
    armor: 0.10, // 10% damage reduction
    boons: [
      { type: 'elementalDamage', value: 5, desc: '+5% Fire damage' },
      { type: 'maxMana', value: 50, desc: '+50 max mana' },
    ],
    pactCost: 500,
    pactTime: 2,
    uniquePerk: "Fire spells cast 10% faster"
  },
  20:  { name: "Aqua Regia", element: "water", hp: 15000, pact: 1.75, color: "#4ECDC4", armor: 0.15, ... },
  30:  { name: "Ventus Rex", element: "air", hp: 30000, pact: 2.0, color: "#00D4FF", armor: 0.18, ... },
  40:  { name: "Terra Firma", element: "earth", hp: 50000, pact: 2.25, color: "#F4A261", armor: 0.25, ... },
  50:  { name: "Lux Aeterna", element: "light", hp: 80000, pact: 2.5, color: "#FFD700", armor: 0.20, ... },
  60:  { name: "Umbra Mortis", element: "dark", hp: 120000, pact: 2.75, color: "#9B59B6", armor: 0.22, ... },
  80:  { name: "Mors Ultima", element: "death", hp: 250000, pact: 3.25, color: "#778CA3", armor: 0.25, ... },
  90:  { name: "Primordialis", element: "void", hp: 400000, pact: 4.0, color: "#4A235A", armor: 0.30, ... },
  100: { name: "The Awakened One", element: "stellar", hp: 1000000, pact: 5.0, color: "#F0E68C", armor: 0.35, ... },
};

GuardianDef Type (from src/lib/game/types/attunements.ts)

export interface GuardianDef {
  name: string;
  element: string;
  hp: number;
  pact: number;           // Pact multiplier when signed
  color: string;
  boons: GuardianBoon[];  // Bonuses granted when pact is signed
  pactCost: number;       // Mana cost to perform pact ritual
  pactTime: number;       // Hours required for pact ritual
  uniquePerk: string;     // Description of unique perk
  armor?: number;         // Damage reduction (0-1, e.g., 0.2 = 20% reduction)
}

export interface GuardianBoon {
  type: 'maxMana' | 'manaRegen' | 'castingSpeed' | 'elementalDamage' | 'rawDamage' | 
        'critChance' | 'critDamage' | 'spellEfficiency' | 'manaGain' | 'insightGain' | 
        'studySpeed' | 'prestigeInsight';
  value: number;
  desc: string;
}

Element Definitions (from src/lib/game/constants/elements.ts)

export const ELEMENTS: Record<string, ElementDef> = {
  // Base Elements
  fire:   { name: "Fire",   sym: "🔥", color: "#FF6B35", glow: "#FF6B3540", cat: "base" },
  water:  { name: "Water",  sym: "💧", color: "#4ECDC4", glow: "#4ECDC440", cat: "base" },
  air:    { name: "Air",    sym: "🌬️", color: "#00D4FF", glow: "#00D4FF40", cat: "base" },
  earth:  { name: "Earth",  sym: "⛰️", color: "#F4A261", glow: "#F4A26140", cat: "base" },
  light:  { name: "Light",  sym: "☀️", color: "#FFD700", glow: "#FFD70040", cat: "base" },
  dark:   { name: "Dark",   sym: "🌑", color: "#9B59B6", glow: "#9B59B640", cat: "base" },
  death:  { name: "Death",  sym: "💀", color: "#778CA3", glow: "#778CA340", cat: "base" },
  // ... other elements
};

export const FLOOR_ELEM_CYCLE = ["fire", "water", "air", "earth", "light", "dark", "death"];

Room Type Labels (from src/lib/game/constants/index.ts)

export const ROOM_TYPE_LABELS: Record<string, { label: string; icon: string; color: string }> = {
  combat: { label: 'Combat', icon: '⚔️', color: '#EF4444' },
  swarm: { label: 'Swarm', icon: '🐝', color: '#F59E0B' },
  speed: { label: 'Speed', icon: '💨', color: '#3B82F6' },
  guardian: { label: 'Guardian', icon: '🛡️', color: '#EF4444' },
  puzzle: { label: 'Puzzle', icon: '🧩', color: '#8B5CF6' },
};

Current Floor Rendering Code

SpireTab.tsx (from src/components/game/tabs/SpireTab.tsx)

Room Type Display Configuration

// Room type configurations for display
const ROOM_TYPE_CONFIG: Record<string, { label: string; icon: string; color: string }> = {
  combat: { label: 'Combat', icon: '⚔️', color: '#EF4444' },
  swarm: { label: 'Swarm', icon: '🐝', color: '#F59E0B' },
  speed: { label: 'Speed', icon: '💨', color: '#3B82F6' },
  guardian: { label: 'Guardian', icon: '🛡️', color: '#EF4444' },
  puzzle: { label: 'Puzzle', icon: '🧩', color: '#8B5CF6' },
};

Floor Type Badge Rendering

<Badge 
  className="ml-2" 
  style={{ 
    backgroundColor: `${roomConfig.color}20`, 
    color: roomConfig.color,
    borderColor: `${roomConfig.color}60`
  }}
>
  {roomConfig.icon} {roomConfig.label}
</Badge>

Guardian Name Display

{isGuardianFloor && currentGuardian && (
  <div className="text-sm font-semibold game-panel-title" style={{ color: floorElemDef?.color }}>
    ⚔️ {currentGuardian.name}
  </div>
)}

Single Enemy Display (Combat/Speed/Guardian)

{!isGuardianFloor && primaryEnemy && roomType !== 'swarm' && (
  <div className="p-3 bg-gray-800/50 rounded border border-gray-700">
    <div className="flex items-center justify-between mb-2">
      <div className="flex items-center gap-2">
        <Skull className="w-4 h-4 text-red-400" />
        <span className="text-sm font-semibold text-gray-200">
          {primaryEnemy.name || 'Unknown Enemy'}
        </span>
      </div>
      <Badge variant="outline" className="text-xs">
        {ELEMENTS[primaryEnemy.element]?.sym} {ELEMENTS[primaryEnemy.element]?.name}
      </Badge>
    </div>
    
    {/* Enemy HP Bar */}
    <div className="space-y-1 mb-2">
      <div className="h-2 bg-gray-800 rounded-full overflow-hidden">
        <div
          className="h-full rounded-full transition-all duration-300"
          style={{
            width: `${Math.max(0, (primaryEnemy.hp / primaryEnemy.maxHP) * 100)}%`,
            background: `linear-gradient(90deg, ${floorElemDef?.color}99, ${floorElemDef?.color})`,
          }}
        />
      </div>
      <div className="flex justify-between text-xs text-gray-400 game-mono">
        <span>{fmt(primaryEnemy.hp)} / {fmt(primaryEnemy.maxHP)} HP</span>
      </div>
    </div>
    
    {/* Enemy Properties */}
    <div className="flex flex-wrap gap-2 text-xs">
      {primaryEnemy.armor > 0 && (
        <Tooltip>
          <TooltipTrigger>
            <Badge variant="outline" className="text-xs py-0">
              <Shield className="w-3 h-3 mr-1" />
              {(primaryEnemy.armor * 100).toFixed(0)}% Armor
            </Badge>
          </TooltipTrigger>
          <TooltipContent>
            <p>Reduces incoming damage by {(primaryEnemy.armor * 100).toFixed(0)}%</p>
          </TooltipContent>
        </Tooltip>
      )}
      {primaryEnemy.dodgeChance > 0 && (
        <Tooltip>
          <TooltipTrigger>
            <Badge variant="outline" className="text-xs py-0">
              <Wind className="w-3 h-3 mr-1" />
              {(primaryEnemy.dodgeChance * 100).toFixed(0)}% Dodge
            </Badge>
          </TooltipTrigger>
          <TooltipContent>
            <p>Chance to dodge attacks and reduce progress</p>
          </TooltipContent>
        </Tooltip>
      )}
    </div>
  </div>
)}

Swarm Enemies Display

{roomType === 'swarm' && swarmEnemies.length > 0 && (
  <div className="space-y-2">
    <div className="text-xs text-gray-400 font-semibold">
      Swarm Enemies ({swarmEnemies.length})
    </div>
    {swarmEnemies.map((enemy, index) => (
      <div key={enemy.id || `swarm-${index}`} className="p-2 bg-gray-800/50 rounded border border-gray-700">
        <div className="flex items-center justify-between mb-1">
          <div className="flex items-center gap-2">
            <Skull className="w-3 h-3 text-red-400" />
            <span className="text-xs font-semibold text-gray-300">
              {enemy.name || `Enemy ${index + 1}`}
            </span>
          </div>
          <Badge variant="outline" className="text-xs py-0">
            {ELEMENTS[enemy.element]?.sym} {fmt(enemy.hp)}/{fmt(enemy.maxHP)} HP
          </Badge>
        </div>
        <div className="h-1.5 bg-gray-800 rounded-full overflow-hidden">
          <div
            className="h-full rounded-full transition-all duration-300"
            style={{
              width: `${Math.max(0, (enemy.hp / enemy.maxHP) * 100)}%`,
              background: `linear-gradient(90deg, ${ELEMENTS[enemy.element]?.color}99, ${ELEMENTS[enemy.element]?.color})`,
            }}
          />
        </div>
      </div>
    ))}
  </div>
)}

Puzzle Room Display

{roomType === 'puzzle' && (
  <div className="p-3 bg-purple-900/20 rounded border border-purple-700">
    <div className="flex items-center gap-2 mb-2">
      <span className="text-lg">🧩</span>
      <span className="text-sm font-semibold text-purple-300">
        {currentRoom.puzzleId ? currentRoom.puzzleId.replace(/_/g, ' ').toUpperCase() : 'Puzzle Room'}
      </span>
    </div>
    <div className="space-y-1">
      <div className="flex justify-between text-xs text-gray-400">
        <span>Progress</span>
        <span>{((currentRoom.puzzleProgress || 0) * 100).toFixed(0)}%</span>
      </div>
      <Progress 
        value={Math.min(100, (currentRoom.puzzleProgress || 0) * 100)} 
        className="h-2 bg-gray-800" 
      />
    </div>
  </div>
)}

Enemy Naming Logic

Enemy Name Generation (from src/lib/game/store.ts)

// Generate enemy names based on element and floor tier
const ENEMY_NAMES_BY_ELEMENT: Record<string, string[]> = {
  fire: ['Fire Imp', 'Flame Sprite', 'Emberling', 'Scorchling', 'Inferno Whelp'],
  water: ['Water Elemental', 'Tidal Wraith', 'Aqua Sprite', 'Drowned One', 'Tsunami Spawn'],
  air: ['Wind Sylph', 'Gale Rider', 'Storm Spirit', 'Zephyr Darter', 'Cyclone Wisp'],
  earth: ['Stone Golem', 'Earth Elemental', 'Graveling', 'Mountain Giant', 'Terra Brute'],
  light: ['Light Saint', 'Radiant Angel', 'Luminous Spirit', 'Divine Warden', 'Holy Sentinel'],
  dark: ['Shadow Assassin', 'Dark Cultist', 'Umbral Fiend', 'Void Walker', 'Night Stalker'],
  death: ['Skeleton Warrior', 'Zombie Lord', 'Lichling', 'Bone Reaper', 'Necrotic Wraith'],
  // Special element names
  lightning: ['Storm Elemental', 'Thunder Hawk', 'Lightning Eel', 'Shock Sprite', 'Voltaic Wisp'],
  metal: ['Iron Golem', 'Steel Guardian', 'Rust Monster', 'Chrome Beetle', 'Mercury Spirit'],
  sand: ['Sand Wraith', 'Dune Stalker', 'Desert Spirit', 'Cactus Thrasher', 'Mirage Runner'],
  crystal: ['Crystal Guardian', 'Prism Sprite', 'Gem Hound', 'Diamond Golem', 'Shardling'],
  stellar: ['Star Spawn', 'Cosmic Entity', 'Nova Spirit', 'Astral Watcher', 'Supernova Seed'],
  void: ['Void Lord', 'Abyssal Horror', 'Entropy Spawn', 'Chaos Elemental', 'Nether Beast'],
};

// Get enemy name based on element and floor tier (1-100)
export function getEnemyName(element: string, floor: number): string {
  const names = ENEMY_NAMES_BY_ELEMENT[element] || ['Unknown Entity'];
  // Higher floors get "stronger" sounding names (pick from later in the list)
  const tierIndex = Math.min(names.length - 1, Math.floor(floor / 20));
  const randomIndex = (tierIndex + Math.floor(Math.random() * (names.length - tierIndex))) % names.length;
  return names[randomIndex!];
}

Enemy State Type (from src/lib/game/types/game.ts)

export interface EnemyState {
  id: string;
  name: string;             // Display name for the enemy
  hp: number;
  maxHP: number;
  armor: number;            // Damage reduction (0-1)
  dodgeChance: number;      // For speed rooms (0-1)
  element: string;
}

Floor State Type

export interface FloorState {
  roomType: RoomType;
  enemies: EnemyState[];      // For swarm rooms, multiple enemies
  puzzleProgress?: number;    // For puzzle rooms (0-1)
  puzzleRequired?: number;    // Total progress needed
  puzzleId?: string;          // Which puzzle type
  puzzleAttunements?: string[]; // Which attunements speed up this puzzle
}

Floor Generation Functions (from src/lib/game/store.ts)

// Generate room type for a floor
export function generateRoomType(floor: number): RoomType {
  // Guardian floors are always guardian type
  if (GUARDIANS[floor]) {
    return 'guardian';
  }
  
  // Check for puzzle room (every PUZZLE_ROOM_INTERVAL floors)
  if (floor % PUZZLE_ROOM_INTERVAL === 0 && Math.random() < PUZZLE_ROOM_CHANCE) {
    return 'puzzle';
  }
  
  // Check for swarm room
  if (Math.random() < SWARM_ROOM_CHANCE) {
    return 'swarm';
  }
  
  // Check for speed room
  if (Math.random() < SPEED_ROOM_CHANCE) {
    return 'speed';
  }
  
  // Default to combat
  return 'combat';
}

// Get armor for a non-guardian floor
export function getFloorArmor(floor: number): number {
  if (GUARDIANS[floor]) {
    return GUARDIANS[floor].armor || 0;
  }
  
  // Armor becomes more common on higher floors
  if (floor < 10) return 0;
  
  const armorChance = Math.min(FLOOR_ARMOR_CONFIG.maxArmorChance, 
    FLOOR_ARMOR_CONFIG.baseChance + (floor - 10) * FLOOR_ARMOR_CONFIG.chancePerFloor);
  
  if (Math.random() > armorChance) return 0;
  
  // Scale armor with floor
  const armorRange = FLOOR_ARMOR_CONFIG.maxArmor - FLOOR_ARMOR_CONFIG.minArmor;
  const floorProgress = Math.min(1, (floor - 10) / 90);
  return FLOOR_ARMOR_CONFIG.minArmor + armorRange * floorProgress * Math.random();
}

// Get dodge chance for a speed room
export function getDodgeChance(floor: number): number {
  return Math.min(
    SPEED_ROOM_CONFIG.maxDodge,
    SPEED_ROOM_CONFIG.baseDodgeChance + floor * SPEED_ROOM_CONFIG.dodgePerFloor
  );
}

// Generate enemies for a swarm room
export function generateSwarmEnemies(floor: number): EnemyState[] {
  const baseHP = getFloorMaxHP(floor);
  const element = getFloorElement(floor);
  const numEnemies = SWARM_CONFIG.minEnemies + 
    Math.floor(Math.random() * (SWARM_CONFIG.maxEnemies - SWARM_CONFIG.minEnemies + 1));
  
  const enemies: EnemyState[] = [];
  for (let i = 0; i < numEnemies; i++) {
    const enemyName = getEnemyName(element, floor);
    enemies.push({
      id: `enemy_${i}`,
      name: enemyName,
      hp: Math.floor(baseHP * SWARM_CONFIG.hpMultiplier),
      maxHP: Math.floor(baseHP * SWARM_CONFIG.hpMultiplier),
      armor: SWARM_CONFIG.armorBase + Math.floor(floor / 10) * SWARM_CONFIG.armorPerFloor,
      dodgeChance: 0,
      element,
    });
  }
  return enemies;
}

// Generate initial floor state
export function generateFloorState(floor: number): FloorState {
  const roomType = generateRoomType(floor);
  const element = getFloorElement(floor);
  const baseHP = getFloorMaxHP(floor);
  const guardian = GUARDIANS[floor];
  
  switch (roomType) {
    case 'guardian':
      return {
        roomType: 'guardian',
        enemies: [{
          id: 'guardian',
          name: guardian.name,
          hp: guardian.hp,
          maxHP: guardian.hp,
          armor: guardian.armor || 0,
          dodgeChance: 0,
          element: guardian.element,
        }],
      };
    
    case 'swarm':
      return {
        roomType: 'swarm',
        enemies: generateSwarmEnemies(floor),
      };
    
    case 'speed': {
      const speedEnemyName = getEnemyName(element, floor);
      return {
        roomType: 'speed',
        enemies: [{
          id: 'speed_enemy',
          name: speedEnemyName,
          hp: baseHP,
          maxHP: baseHP,
          armor: getFloorArmor(floor),
          dodgeChance: getDodgeChance(floor),
          element,
        }],
      };
    }
    
    case 'puzzle': {
      // Select a puzzle type based on player's attunements
      const puzzleKeys = Object.keys(PUZZLE_ROOMS);
      const selectedPuzzle = puzzleKeys[Math.floor(Math.random() * puzzleKeys.length)];
      const puzzle = PUZZLE_ROOMS[selectedPuzzle];
      return {
        roomType: 'puzzle',
        enemies: [],
        puzzleProgress: 0,
        puzzleRequired: 1,
        puzzleId: selectedPuzzle,
        puzzleAttunements: puzzle.attunements,
      };
    }
    
    default: // combat
      const combatEnemyName = getEnemyName(element, floor);
      return {
        roomType: 'combat',
        enemies: [{
          id: 'enemy',
          name: combatEnemyName,
          hp: baseHP,
          maxHP: baseHP,
          armor: getFloorArmor(floor),
          dodgeChance: 0,
          element,
        }],
      };
  }
}

Special Floor Properties

Currently Implemented Properties

Armor (Damage Reduction)

  • Guardian floors: Defined in GUARDIANS[floor].armor (0.10 to 0.35)
  • Non-guardian floors: Randomly generated via getFloorArmor(floor) using FLOOR_ARMOR_CONFIG
  • Swarm enemies: SWARM_CONFIG.armorBase + Math.floor(floor / 10) * SWARM_CONFIG.armorPerFloor
  • Displayed in UI with shield icon and percentage

Dodge Chance

  • Speed rooms only: Generated via getDodgeChance(floor) using SPEED_ROOM_CONFIG
  • Base: 25%, scales +0.5% per floor, max 50%
  • Displayed in UI with wind icon and percentage

Health/HP

  • Guardian floors: GUARDIANS[floor].hp (5000 to 1000000)
  • Normal floors: getFloorMaxHP(floor) - scales with floor number
  • Swarm enemies: baseHP * SWARM_CONFIG.hpMultiplier (40% of normal)

Properties Mentioned in Task But Not Currently in Floor Config

  • healthRegen: Not currently implemented as a floor/enemy property (only exists in guardian boons as manaRegen for player)
  • barrier: Not currently implemented as a floor/enemy property (only exists as attunement mana type)

Note: The task mentions displaying "Special floor properties (armor%, health regen, barrier, dodge)" but healthRegen and barrier are not currently implemented in the floor config. These may need to be added as part of this task.


File Paths

Key Files for Task 5 (2a Floor Rendering & Identity)

  1. Floor/Room Type Definitions:

    • /home/user/repos/Mana-Loop/src/lib/game/constants/rooms.ts - Room types, swarm/speed config, armor config
    • /home/user/repos/Mana-Loop/src/lib/game/constants/guardians.ts - Guardian definitions with names, HP, armor
    • /home/user/repos/Mana-Loop/src/lib/game/constants/elements.ts - Element definitions with symbols and colors
    • /home/user/repos/Mana-Loop/src/lib/game/constants/index.ts - ROOM_TYPE_LABELS export
  2. Type Definitions:

    • /home/user/repos/Mana-Loop/src/lib/game/types/game.ts - RoomType, EnemyState, FloorState interfaces
    • /home/user/repos/Mana-Loop/src/lib/game/types/attunements.ts - GuardianDef, GuardianBoon interfaces
  3. Floor Rendering UI:

    • /home/user/repos/Mana-Loop/src/components/game/tabs/SpireTab.tsx - Main floor rendering component with enemy display, room type badges, armor/dodge tooltips
  4. Floor Generation Logic:

    • /home/user/repos/Mana-Loop/src/lib/game/store.ts - getEnemyName(), generateRoomType(), generateFloorState(), getFloorArmor(), getDodgeChance(), generateSwarmEnemies()
  5. Element Cycle for Floors:

    • /home/user/repos/Mana-Loop/src/lib/game/store.ts - getFloorElement(), getFloorMaxHP()
    • /home/user/repos/Mana-Loop/src/lib/game/constants/elements.ts - FLOOR_ELEM_CYCLE