import { useEffect, useMemo, useState } from 'react' import { loadActionMechanicConfig, resetActionMechanicConfig, saveActionMechanicConfig, type ActionAttackConfig, type ActionMechanicConfig, } from '../actionBoss/actionEncounterConfig' import type { EnemyKind } from '../actionBoss/bulldromeSimulation' import { ACTION_DIFFICULTY_TIERS, ACTION_GEAR_SLOTS, ACTION_GEAR_UPGRADE_COST, completeActionDungeonHunt, getActionCoinCount, getActionDifficultyTier, getActionGearStats, getActionLevelProgress, getUpgradeCoinCount, getUpgradeCoinName, loadActionCharacter, saveActionCharacter, upgradeBulldromeGear, type ActionCharacter, type ActionDifficulty, type ActionDungeonId, type ActionGearPiece, type ActionGearSlot, type ActionGearSource, type ActionRunMode, type ActionRunReward, } from '../actionMode' import { BulldromeBossSlice } from './BulldromeBossSlice' type ActionModeScreenProps = { onBack?: () => void } type ActionHubTab = 'dungeons' | 'raids' | 'pvp' | 'roguelike' | 'customize' | 'settings' type ActionHubScreen = 'menu' | ActionHubTab type PlayableActionDungeonId = Exclude const ACTION_HUB_ITEMS: Array<{ id: ActionHubTab label: string glyph: string description: string }> = [ { id: 'dungeons', label: 'Dungeons', glyph: 'D', description: 'Run action dungeons and earn gear.' }, { id: 'raids', label: 'Raids', glyph: 'R', description: 'Large action encounters.' }, { id: 'pvp', label: 'PVP', glyph: 'P', description: 'Action healer competitions.' }, { id: 'roguelike', label: 'Roguelike', glyph: 'L', description: 'Draft upgrades through action fights.' }, { id: 'customize', label: 'Customize Character', glyph: 'C', description: 'Manage Bulldrome gear and upgrades.' }, { id: 'settings', label: 'Settings', glyph: 'S', description: 'Tune action mode controls.' }, ] export function ActionModeScreen({ onBack }: ActionModeScreenProps) { const [character, setCharacter] = useState(() => loadActionCharacter()) const [activeDifficulty, setActiveDifficulty] = useState(null) const [activeDungeonId, setActiveDungeonId] = useState('bulldrome') const [activeRunMode, setActiveRunMode] = useState('hunt') const [activeScreen, setActiveScreen] = useState('menu') const [lastReward, setLastReward] = useState(null) const [runKey, setRunKey] = useState(0) const [message, setMessage] = useState('') const progress = useMemo(() => getActionLevelProgress(character), [character]) useEffect(() => { saveActionCharacter(character) }, [character]) if (activeDifficulty) { return (
{ setLastReward(null) setActiveDifficulty(null) setActiveScreen('dungeons') }} onRunComplete={() => { if (lastReward && activeRunMode === 'hunt') return const { character: nextCharacter, reward } = completeActionDungeonHunt(character, activeDungeonId, activeDifficulty) setCharacter(nextCharacter) if (activeRunMode === 'hunt') setLastReward(reward) setMessage('') }} /> {lastReward && ( { setLastReward(null) setRunKey((current) => current + 1) }} onMainMenu={() => { setLastReward(null) setActiveDifficulty(null) setActiveScreen('menu') }} /> )}
) } return (

Action Mode

{getHubTitle(activeScreen)}

{character.name} Healer Level {character.level}
{(activeScreen !== 'menu' || onBack) && ( )}
{activeScreen === 'menu' && ( )} {message &&

{message}

} {activeScreen === 'dungeons' && ( { setActiveRunMode('hunt') setLastReward(null) setActiveDungeonId(dungeonId) setRunKey((current) => current + 1) setActiveDifficulty(difficulty) }} onStartMarathon={(dungeonId, difficulty) => { setActiveRunMode('marathon') setLastReward(null) setActiveDungeonId(dungeonId) setRunKey((current) => current + 1) setActiveDifficulty(difficulty) }} /> )} {activeScreen === 'customize' && ( { const before = character.inventory.find((item) => item.id === itemId) const nextCharacter = upgradeBulldromeGear(character, itemId) const coinCount = before ? getUpgradeCoinCount(character, before) : 0 setCharacter(nextCharacter) setMessage( before && before.itemLevel < 5 && coinCount >= ACTION_GEAR_UPGRADE_COST ? `${before.name} upgraded to item level ${before.itemLevel + 1}.` : 'Upgrade unavailable.', ) }} /> )} {activeScreen === 'settings' && ( )} {activeScreen !== 'menu' && activeScreen !== 'dungeons' && activeScreen !== 'customize' && activeScreen !== 'settings' && (

{getHubTitle(activeScreen)}

Coming Soon

This section has its own Action Mode button now. Content hooks can land here without touching Normal Mode.

)}
) } function ActionMechanicsAdmin() { const [config, setConfig] = useState(() => loadActionMechanicConfig()) const [enemyPage, setEnemyPage] = useState(0) const [attackPageByEnemy, setAttackPageByEnemy] = useState>>({}) function updateAttack(enemyKind: EnemyKind, attackId: string, patch: Partial) { const next = { ...config, [enemyKind]: { ...config[enemyKind], attacks: config[enemyKind].attacks.map((attack) => ( attack.id === attackId ? { ...attack, ...patch } : attack )), }, } setConfig(next) saveActionMechanicConfig(next) } function resetConfig() { setConfig(resetActionMechanicConfig()) setEnemyPage(0) setAttackPageByEnemy({}) } const enemyKinds = Object.keys(config) as EnemyKind[] const enemyKind = enemyKinds[Math.min(enemyPage, enemyKinds.length - 1)] ?? enemyKinds[0] const enemy = config[enemyKind] const enabledAttacks = enemy.attacks.filter((attack) => attack.enabled) const disabledAttacks = enemy.attacks.filter((attack) => !attack.enabled) const attackPage = Math.min(attackPageByEnemy[enemyKind] ?? 0, Math.max(0, enabledAttacks.length - 1)) const activeAttack = enabledAttacks[attackPage] ?? null function setAttackPage(enemyKind: EnemyKind, page: number) { setAttackPageByEnemy((current) => ({ ...current, [enemyKind]: page })) } return (

Admin

Mob And Boss Mechanics

{enemy.role}

{enemy.label}

{enemyPage + 1} / {enemyKinds.length}
{activeAttack ? (
{attackPage + 1} / {enabledAttacks.length}
updateAttack(enemyKind, activeAttack.id, patch)} />
) : (

No active attacks.

)} {disabledAttacks.length > 0 && (
Add Attack {disabledAttacks.map((attack) => ( ))}
)}
) } function AttackEditor({ attack, onChange, }: { attack: ActionAttackConfig onChange: (patch: Partial) => void }) { return (
{attack.label} {attack.kind}
{attack.windupSeconds !== undefined && ( )} {attack.recoverSeconds !== undefined && ( )} {attack.speed !== undefined && ( )} {attack.radius !== undefined && ( )} {attack.everyNthCharge !== undefined && ( )}
) } function DungeonsPanel({ character, onStart, onStartMarathon, }: { character: ActionCharacter onStart: (dungeonId: PlayableActionDungeonId, difficulty: ActionDifficulty) => void onStartMarathon: (dungeonId: PlayableActionDungeonId, difficulty: ActionDifficulty) => void }) { const [selectedDungeonId, setSelectedDungeonId] = useState('bulldrome') const [selectedDifficulty, setSelectedDifficulty] = useState('ilvl-1') const selectedDungeon = ACTION_DUNGEONS.find((dungeon) => dungeon.id === selectedDungeonId) ?? ACTION_DUNGEONS[0] const selectedTier = getActionDifficultyTier(selectedDifficulty) const selectedCoinCount = getActionCoinCount(character, selectedDungeon.source, selectedTier.coinColor) return (
{ACTION_DUNGEONS.map((dungeon) => { const selected = dungeon.id === selectedDungeonId const locked = dungeon.locked return ( ) })}
) } const ACTION_DUNGEONS: Array<{ id: ActionDungeonId eyebrow: string glyph: string name: string summary: string description: string source: ActionGearSource coinLabel: string locked?: boolean }> = [ { id: 'bulldrome', eyebrow: 'Dungeon 1', glyph: 'B', name: 'Bulldrome Hunting Grounds', summary: 'Charges, slams, and Bullfango pressure.', description: 'Bulldrome drops White Bulldrome Coins and item level 1 Bulldrome gear.', source: 'bulldrome', coinLabel: 'White Bulldrome Coins', }, { id: 'yian-kut-ku', eyebrow: 'Dungeon 2', glyph: 'Y', name: 'Yian Kut-Ku Roost', summary: 'Bouncing fireballs and dive birds.', description: 'Yian Kut-Ku fireballs bounce until the next cast and leave fire on walls or players.', source: 'yian-kut-ku', coinLabel: 'White Yian Kut-Ku Coins', }, { id: 'rathian', eyebrow: 'Dungeon 3', glyph: 'R', name: 'Rathian Nest', summary: 'Coming next.', description: 'Rathian mechanics will be built after Yian Kut-Ku.', source: 'bulldrome', coinLabel: 'Rathian Coins', locked: true, }, ] function RunRewardModal({ dungeonId, difficulty, onGoAgain, onMainMenu, reward, }: { dungeonId: PlayableActionDungeonId difficulty: ActionDifficulty onGoAgain: () => void onMainMenu: () => void reward: ActionRunReward }) { return (

Dungeon Complete

{getRunRewardTitle(dungeonId, difficulty)}

XP
{reward.experience}
{reward.coinName}
{reward.coins}
Level
{reward.leveledUp ? 'Up' : 'No Change'}
Loot {reward.gear.length === 0 ? (

No gear dropped.

) : ( reward.gear.map((item) => ( {item.name} · ilvl {item.itemLevel} )) )}
) } function CustomizePanel({ character, onUpgrade, }: { character: ActionCharacter onUpgrade: (itemId: string) => void }) { const [selectedSlot, setSelectedSlot] = useState('weapon') const slotMeta = ACTION_GEAR_SLOTS.find((slot) => slot.slot === selectedSlot) ?? ACTION_GEAR_SLOTS[0] const filteredItems = useMemo(() => ( character.inventory .filter((item) => item.slot === selectedSlot) .sort((a, b) => b.itemLevel - a.itemLevel || a.name.localeCompare(b.name)) ), [character.inventory, selectedSlot]) const [selectedItemId, setSelectedItemId] = useState(null) const selectedItem = filteredItems.find((item) => item.id === selectedItemId) ?? filteredItems[0] ?? null return (

Craft Table

Bulldrome Gear

Pick a gear slot, inspect that slot inventory, then preview the upgrade cost and stat gain before spending coins.

{ACTION_GEAR_SLOTS.map((slot) => { const count = character.inventory.filter((item) => item.slot === slot.slot).length return ( ) })}
) } function GearDetail({ coins, item, onUpgrade, }: { coins: number item: ActionGearPiece | null onUpgrade: () => void }) { if (!item) { return (

Select a slot with gear to see stats and upgrade costs.

) } const currentStats = getActionGearStats(item) const nextItem = { ...item, itemLevel: Math.min(5, item.itemLevel + 1) } const nextStats = getActionGearStats(nextItem) const coinName = getUpgradeCoinName(item) const canUpgrade = coins >= ACTION_GEAR_UPGRADE_COST && item.itemLevel < 5 return (

Selected

{item.name}

ilvl {item.itemLevel}
Healing
{currentStats.healingPower} → {nextStats.healingPower}
Stamina
{currentStats.stamina} → {nextStats.stamina}
Upgrade Cost
{item.itemLevel >= 5 ? 'Max' : `${ACTION_GEAR_UPGRADE_COST} ${coinName}`}
You Have
{coins} {coinName}
) } function getHubTitle(screen: ActionHubScreen) { if (screen === 'menu') return 'Action Mode' return ACTION_HUB_ITEMS.find((item) => item.id === screen)?.label ?? 'Action Mode' } function getRunRewardTitle(dungeonId: PlayableActionDungeonId, difficulty: ActionDifficulty) { const dungeonName = dungeonId === 'yian-kut-ku' ? 'Yian Kut-Ku Hunt' : 'Bulldrome Hunt' return `${getActionDifficultyTier(difficulty).label} ${dungeonName}` }