From a100885552ac4fd12be9e692ee1b029dcec17572 Mon Sep 17 00:00:00 2001 From: troytt <47798984@qq.com> Date: Mon, 28 Sep 2026 08:09:37 +0000 Subject: [PATCH] chore: remove standalone simulator pages (arena, loot, boss, skills) and related entry code --- README.md | 10 +- arena.html | 780 ----------- boss.html | 924 ------------- loot.html | 1331 ------------------- scripts/audit-boss-kill-10-zh.js | 183 --- scripts/audit-parity.ts | 83 +- scripts/audit-skills-browser.ts | 473 ------- scripts/verify-maxroll-loot.ts | 140 -- skills.html | 652 --------- src/arena.ts | 1842 ------------------------- src/boss.ts | 658 --------- src/loot.ts | 2137 ------------------------------ src/skills.ts | 787 ----------- tests/boss.test.ts | 160 --- tests/loot.test.ts | 412 ------ tests/parity-1.13c.test.ts | 3 +- vite.config.ts | 8 +- 17 files changed, 89 insertions(+), 10494 deletions(-) delete mode 100644 arena.html delete mode 100644 boss.html delete mode 100644 loot.html delete mode 100644 scripts/audit-boss-kill-10-zh.js delete mode 100644 scripts/audit-skills-browser.ts delete mode 100644 scripts/verify-maxroll-loot.ts delete mode 100644 skills.html delete mode 100644 src/arena.ts delete mode 100644 src/boss.ts delete mode 100644 src/loot.ts delete mode 100644 src/skills.ts delete mode 100644 tests/boss.test.ts delete mode 100644 tests/loot.test.ts diff --git a/README.md b/README.md index 384a4bc..630d13f 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ src/render/ WebGL2 渲染器(多图集、动态光照着色、加法/常规 src/scene/ 页面级装配(act-scene 主游戏循环、A* 鼠标控制器、跨图过渡;资源包优先,失败回退到直读归档) src/data/ 1.13c 权威内嵌数据表(canonical-drop-data / canonical-tbl-strings / canonical-vendor-data / canonical-quest-data / skills-catalog / gitea-skill-issues.json) scripts/ 资源包多进程烘焙(地图/UI/动画/实体/投射物/叠加特效)、格式/数据/部署校验、无头浏览器全量走查、夹具生成 -*.html 入口页面:acts.html(游戏主页面)、arena.html(战斗数值与技能竞技场)、skills.html(技能加点模拟器)、loot.html(掉落模拟器)、boss.html(幕首领战利品模拟器) +*.html 入口页面:index.html(双语选择门户)、acts.html / acts-zh.html / acts-en.html(中英战役主页面)、bnet.html(原版片头与战网完整流程) ``` ## 美术资源与图集资产(Assets) @@ -210,11 +210,9 @@ npm run dev # 启动 Vite:http://127. | 页面 | 用途 | | --- | --- | -| `/acts.html`(或访问根路径 `/`) | **主游戏页面**:全屏 WebGL2 画布 + Glide 动态光影 + 等距精灵自动地图/小地图 + HUD 控制底栏 + 左右停靠全套功能面板 + 7 职业漫步战斗与打宝交易 | -| `/arena.html` | **战斗数值与技能测试竞技场**:攻防数值数学模型、技能加成、伤害计算器(1.13c 原版公式验证) | -| `/skills.html` | **全职业技能加点模拟器**:7 大职业 210 个技能树加点、协同加成与 1.13c 技能数值实时预览 | -| `/loot.html` | **掉落模拟器与精确概率计算器**:Maxroll 风格宝藏等级(TC)掉落模拟、MF 收益曲线与物品概率查询 | -| `/boss.html` | **幕首领击杀与战利品属性模拟器**:五幕首领击杀掉落模拟与暗金/套装/词缀属性检视 | +| `/index.html`(或访问根路径 `/`) | **双语版本选择门户页**:选择简体中文版(`CHI.tbl`)或英文原版(`ENG.tbl`)进入庇护之地 | +| `/acts.html` / `/acts-zh.html` / `/acts-en.html` | **战役主场景(中/英双语)**:全屏 WebGL2 画布 + Glide 动态光影 + 等距精灵自动地图/小地图 + HUD 控制底栏 + 左右停靠全套功能面板 + 7 职业漫步战斗与打宝交易 | +| `/bnet.html` | **原版前端与战网(Battle.net)7 阶段完整流程页**:800×600 原版片头、主菜单、战网登录、篝火 7 职业角色创建/选择、大厅建房并无缝进入第一幕罗格营地 | #### 页面查询参数(`/acts.html`) diff --git a/arena.html b/arena.html deleted file mode 100644 index dd81213..0000000 --- a/arena.html +++ /dev/null @@ -1,780 +0,0 @@ - - - - - - d2web · 战斗数学与攻防测试竞技场(Issue #24 / M9) - - - -
-

- ⚔️ - 暗黑破坏神 2 · 真实战斗数学与攻防测试竞技场 - (Issue #24 / M9) -

-
- 🎮 返回战役漫步 (acts.html) -
-
- -
- - - - -
- -
- -
- - -
- - - - - - - - -
- - -
-
- 📐 实时战斗数学公式推导与日志 (Combat Math Provenance) -
- DPS: 0.0 - -
-
-
-
-
- - - -
- - - - diff --git a/boss.html b/boss.html deleted file mode 100644 index 7f6e06b..0000000 --- a/boss.html +++ /dev/null @@ -1,924 +0,0 @@ - - - - - - 暗黑破坏神 2 · 幕首领击杀与战利品属性模拟器 (Act Boss Loot Simulator) - - - - -
-

- DIABLO II - BOSS KILL SIMULATOR - 1.13c 幕首领战利品与词缀生成器 -

- -
- - -
- - - - -
- -
- -
- - -
-
- 💎 - 战利品展示面板 -
-
- 装备语言: - - - -
-
- - -
- -
-
-
- - - - - diff --git a/loot.html b/loot.html deleted file mode 100644 index fe39228..0000000 --- a/loot.html +++ /dev/null @@ -1,1331 +0,0 @@ - - - - - - 暗黑破坏神 2 · 掉落模拟器与精确概率计算器 (Maxroll 风格) - - - - -
-

- 💎 - 暗黑破坏神 2 · 掉落计算器与精确概率表 - Maxroll 风格 -

- -
- -
- - - - -
- -
- - - -
- - -
-
-
- 目标: - 巴尔 (Baal) -
-
- TC: - Baal (H) -
-
- 有效人数: - 1 P -
-
- -
-
- 可掉落叶子 - 0 种 -
-
- NoDrop 概率 - 0.0% -
-
- 期望件数/击杀 - 0.00 -
-
-
- - -
- - - - - - - -
- - -
- - -
- - - -
-
- - -
- -
- -
-
- - -
- -
-
- 就绪:点击左侧「开始模拟抽样」运行 Monte Carlo 模拟 - Non-blocking chunked loop -
-
-
-
-
- - -
- - - - - - - - - - - - -
品质类别 (Quality Tier)实测掉落件数 (Count)掉落占比 (%)分布直方图 (Histogram)
暂无模拟数据
-
- - -
-
暂无模拟结果,请点击左侧「开始模拟抽样」运行抽样
-
-
- - -
-
- 🔍 - -
- -
- 反查匹配的 TreasureClass 掉落源 - 0 项匹配 -
- -
- - - - - - - - - - - - - - -
所属掉落表 (Treasure Class)匹配物品 (Matched Item)挑选次数单次概率 (Single)综合概率 (Compound)掉落几率比 (1 in X Odds)
在上方搜索框输入物品代码或名称(例如 Shako、uap、r33、cap)反查可掉落的 TC 列表
-
-
-
-
- - - - - diff --git a/scripts/audit-boss-kill-10-zh.js b/scripts/audit-boss-kill-10-zh.js deleted file mode 100644 index 21e305a..0000000 --- a/scripts/audit-boss-kill-10-zh.js +++ /dev/null @@ -1,183 +0,0 @@ -import { spawn } from 'child_process' - -async function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)) -} - -async function runBrowserAudit() { - console.log('1. Starting Google Chrome headless...') - const chromeProc = spawn('/usr/bin/google-chrome', [ - '--headless=new', - '--remote-debugging-port=9222', - '--no-sandbox', - '--disable-gpu', - '--disable-extensions', - 'http://localhost:8080/boss.html', - ]) - - // Wait for Chrome to be ready - let wsUrl = null - for (let i = 0; i < 30; i++) { - await sleep(300) - try { - const res = await fetch('http://127.0.0.1:9222/json/list') - const pages = await res.json() - const page = pages.find(p => p.type === 'page' && p.url.includes('boss.html')) - if (page && page.webSocketDebuggerUrl) { - wsUrl = page.webSocketDebuggerUrl - break - } - } catch (e) { - // keep trying - } - } - - if (!wsUrl) { - chromeProc.kill() - throw new Error('Failed to connect to Chrome headless CDP within 10s') - } - - console.log('2. Connected to Chrome CDP on page:', wsUrl) - - const ws = new WebSocket(wsUrl) - await new Promise(resolve => ws.onopen = resolve) - - let idCounter = 1 - function sendCommand(method, params = {}) { - return new Promise((resolve, reject) => { - const id = idCounter++ - const handler = event => { - const msg = JSON.parse(event.data) - if (msg.id === id) { - ws.removeEventListener('message', handler) - if (msg.error) reject(msg.error) - else resolve(msg.result) - } - } - ws.addEventListener('message', handler) - ws.send(JSON.stringify({ id, method, params })) - }) - } - - // Enable Runtime - await sendCommand('Runtime.enable') - - // Wait 1.5s for initial drop tables to load - await sleep(1500) - - console.log('3. Triggering Chinese language switch and 10x Boss Kill in browser DOM...') - - const evalResult = await sendCommand('Runtime.evaluate', { - expression: `(async () => { - // 1. Click Chinese language pill - const zhPill = document.querySelector('.lang-pill[data-lang="zh"]'); - if (zhPill) zhPill.click(); - - // 2. Click 10x kill button - const kill10Btn = document.getElementById('btn-kill-10'); - if (kill10Btn) kill10Btn.click(); - - // Wait a short moment for DOM rendering - await new Promise(r => setTimeout(r, 800)); - - // 3. Extract items - const cards = Array.from(document.querySelectorAll('#loot-items-grid .d2-item-card')); - return cards.map(c => { - const title = c.querySelector('.item-title')?.textContent?.trim() || ''; - const subTitle = c.querySelector('.item-subtitle')?.textContent?.trim() || ''; - const quality = c.querySelector('.item-quality-pill')?.textContent?.trim() || ''; - const ilvl = c.querySelector('.item-ilvl-pill')?.textContent?.trim() || ''; - const stats = Array.from(c.querySelectorAll('.item-base-stats .base-stat-item')).map(s => s.textContent.trim()); - const affixes = Array.from(c.querySelectorAll('.item-affix-lines .tooltip-stat-line')).map(a => a.textContent.trim()); - return { - title, - subTitle, - quality, - ilvl, - stats, - affixes - }; - }); - })()`, - awaitPromise: true, - returnByValue: true, - }) - - console.log('4. Evaluation completed. Closing Chrome...') - ws.close() - chromeProc.kill() - - const items = evalResult.result.value - console.log(`\nSuccessfully captured ${items.length} items from http://localhost:8080/boss.html!\n`) - return items -} - -runBrowserAudit() - .then(items => { - console.log('================================================================'); - console.log(`=== AUDITING ALL ${items.length} ITEMS GENERATED FROM 10x BOSS KILL ===`); - console.log('================================================================\n'); - - let errorsFound = 0; - const traditionalChars = ['屬', '無', '紅', '綠', '變', '爛', '誕', '誠', '雲', '犧', '嚇', '層', '靜', '換', '亂', '節', '甦']; - - items.forEach((item, index) => { - console.log(`[#${index + 1}] [${item.quality}] ${item.title} (底材: ${item.subTitle || '无'}, ${item.ilvl})`); - if (item.stats.length > 0) { - console.log(` 基础属性: ${item.stats.join(' | ')}`); - } - if (item.affixes.length > 0) { - console.log(` 词条属性:`); - item.affixes.forEach(aff => console.log(` * ${aff}`)); - } - - // Check 1: Negative numbers in charged spells - item.affixes.forEach(aff => { - if (aff.includes('次充能') && (aff.includes('等级 -') || aff.includes('(-'))) { - console.error(` ❌ ERROR: Negative value in charged spell: "${aff}"`); - errorsFound++; - } - }); - - // Check 2: Missing English affix translations (e.g. English letters followed by 之 or preceded by 之) - const englishAffixMatch = item.title.match(/[a-zA-Z]+之|之[a-zA-Z]+/); - if (englishAffixMatch) { - console.error(` ❌ ERROR: Untranslated English affix in title: "${item.title}"`); - errorsFound++; - } - - // Check 3: Word order anomaly: base name followed by '之' at the end of title - if (item.subTitle && item.title.endsWith(item.subTitle + '之')) { - console.error(` ❌ ERROR: Word order inversion (base followed by suffix particle): "${item.title}"`); - errorsFound++; - } - - // Check 4: Traditional Chinese characters - const allText = item.title + ' ' + item.subTitle + ' ' + item.affixes.join(' '); - traditionalChars.forEach(ch => { - if (allText.includes(ch)) { - console.error(` ❌ ERROR: Traditional Chinese character '${ch}' found in: "${allText}"`); - errorsFound++; - } - }); - - console.log(''); - }); - - console.log('================================================================'); - if (errorsFound === 0) { - console.log(`🎉 AUDIT PASSED! All ${items.length} items checked with ZERO errors!`); - console.log(`✅ All names in correct Chinese grammar word order`); - console.log(`✅ 100% Affix translations matched without fallback artifacts`); - console.log(`✅ Charged spell positive charges and positive skill levels verified`); - console.log(`✅ Normalized Simplified Chinese characters verified`); - } else { - console.error(`⚠️ AUDIT FOUND ${errorsFound} ERRORS!`); - process.exit(1); - } - console.log('================================================================\n'); - }) - .catch(err => { - console.error('Audit execution failed:', err); - process.exit(1); - }); diff --git a/scripts/audit-parity.ts b/scripts/audit-parity.ts index b4b84de..c3ff345 100644 --- a/scripts/audit-parity.ts +++ b/scripts/audit-parity.ts @@ -7,12 +7,93 @@ * npx tsx scripts/audit-parity.ts --kills=1000 --boss=mephisto --difficulty=hell --mf=250 --players=1 */ -import { BOSS_LIST, type Difficulty } from '../src/boss.ts' import { getEmbeddedDropTables } from '../src/game/embedded-drop-tables.ts' import { executeDropPipeline } from '../src/game/drop-pipeline.ts' import type { UpgradableItemBase } from '../src/game/item-upgrade.ts' import type { Item } from '../src/game/items.ts' +export type Difficulty = 'normal' | 'nightmare' | 'hell' + +export interface BossPreset { + readonly id: string + readonly act: 1 | 2 | 3 | 4 | 5 + readonly nameCn: string + readonly nameEn: string + readonly mlvl: { normal: number; nightmare: number; hell: number } + readonly tc: { normal: string; nightmare: string; hell: string } + readonly questTc: { normal: string; nightmare: string; hell: string } + readonly baseNoDrop: number + readonly picks: number + readonly monsterType?: number +} + +export type BossDefinition = BossPreset + +export const ACT_BOSSES: readonly BossPreset[] = [ + { + id: 'andariel', + act: 1, + nameCn: '安达利尔 (Andariel)', + nameEn: 'Maiden of Anguish', + mlvl: { normal: 12, nightmare: 49, hell: 75 }, + tc: { normal: 'Andariel', nightmare: 'Andariel (N)', hell: 'Andariel (H)' }, + questTc: { normal: 'Andarielq', nightmare: 'Andarielq (N)', hell: 'Andarielq (H)' }, + baseNoDrop: 19, + picks: 7, + monsterType: 4, + }, + { + id: 'duriel', + act: 2, + nameCn: '督瑞尔 (Duriel)', + nameEn: 'Lord of Pain', + mlvl: { normal: 22, nightmare: 55, hell: 88 }, + tc: { normal: 'Duriel', nightmare: 'Duriel (N)', hell: 'Duriel (H)' }, + questTc: { normal: 'Durielq', nightmare: 'Durielq (N)', hell: 'Durielq (H)' }, + baseNoDrop: 19, + picks: 7, + monsterType: 4, + }, + { + id: 'mephisto', + act: 3, + nameCn: '墨菲斯托 (Mephisto)', + nameEn: 'Lord of Hatred', + mlvl: { normal: 26, nightmare: 59, hell: 87 }, + tc: { normal: 'Mephisto', nightmare: 'Mephisto (N)', hell: 'Mephisto (H)' }, + questTc: { normal: 'Mephistoq', nightmare: 'Mephistoq (N)', hell: 'Mephistoq (H)' }, + baseNoDrop: 15, + picks: 7, + monsterType: 4, + }, + { + id: 'diablo', + act: 4, + nameCn: '暗黑破坏神 (Diablo)', + nameEn: 'Lord of Terror', + mlvl: { normal: 40, nightmare: 62, hell: 94 }, + tc: { normal: 'Diablo', nightmare: 'Diablo (N)', hell: 'Diablo (H)' }, + questTc: { normal: 'Diabloq', nightmare: 'Diabloq (N)', hell: 'Diabloq (H)' }, + baseNoDrop: 15, + picks: 7, + monsterType: 4, + }, + { + id: 'baal', + act: 5, + nameCn: '巴尔 (Baal)', + nameEn: 'Lord of Destruction', + mlvl: { normal: 60, nightmare: 75, hell: 99 }, + tc: { normal: 'Baal', nightmare: 'Baal (N)', hell: 'Baal (H)' }, + questTc: { normal: 'Baalq', nightmare: 'Baalq (N)', hell: 'Baalq (H)' }, + baseNoDrop: 15, + picks: 7, + monsterType: 4, + }, +] + +export const BOSS_LIST: readonly BossDefinition[] = ACT_BOSSES + export interface ParityAuditSummary { readonly config: { readonly boss: string diff --git a/scripts/audit-skills-browser.ts b/scripts/audit-skills-browser.ts deleted file mode 100644 index bd6e17d..0000000 --- a/scripts/audit-skills-browser.ts +++ /dev/null @@ -1,473 +0,0 @@ -/** - * Comprehensive Browser-based E2E Verification & Screenshot Audit for Diablo II Skill Calculator. - */ - -import { spawn, type ChildProcess } from 'child_process' -import { createServer } from 'http' -import { readFileSync, existsSync, mkdirSync, writeFileSync, copyFileSync } from 'fs' -import { join, extname } from 'path' - -async function sleep(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)) -} - -const MIME_TYPES: Record = { - '.html': 'text/html', - '.js': 'application/javascript', - '.css': 'text/css', - '.png': 'image/png', - '.jpg': 'image/jpeg', - '.json': 'application/json', -} - -async function runAudit() { - console.log('=== Starting Diablo II Skill Calculator Browser Audit ===') - - // 1. Start simple static server on port 8899 serving dist/ - const PORT = 8899 - const baseDir = join(process.cwd(), 'dist') - if (!existsSync(baseDir)) { - throw new Error('dist/ directory does not exist! Run npm run build first.') - } - - const server = createServer((req, res) => { - let reqPath = req.url?.split('?')[0]?.split('#')[0] || '/' - if (reqPath === '/') reqPath = '/skills.html' - const filePath = join(baseDir, reqPath) - - if (existsSync(filePath)) { - const ext = extname(filePath) - res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' }) - res.end(readFileSync(filePath)) - } else { - // Check public/ folder for skill images - const publicPath = join(process.cwd(), 'public', reqPath) - if (existsSync(publicPath)) { - const ext = extname(publicPath) - res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' }) - res.end(readFileSync(publicPath)) - } else { - res.writeHead(404) - res.end('Not found: ' + reqPath) - } - } - }) - - await new Promise(resolve => server.listen(PORT, '127.0.0.1', () => resolve())) - console.log(`1. Local HTTP server listening at http://127.0.0.1:${PORT}/`) - - // 2. Launch headless Chrome with CDP - console.log('2. Starting Chrome headless on port 9223...') - const chromeProc: ChildProcess = spawn('/usr/bin/google-chrome', [ - '--headless=new', - '--remote-debugging-port=9223', - '--no-sandbox', - '--disable-gpu', - '--disable-extensions', - '--window-size=1280,900', - `http://127.0.0.1:${PORT}/skills.html`, - ]) - - let wsUrl: string | null = null - for (let i = 0; i < 40; i++) { - await sleep(300) - try { - const res = await fetch('http://127.0.0.1:9223/json/list') - const pages = await res.json() - const page = pages.find((p: any) => p.type === 'page' && p.url.includes('skills.html')) - if (page && page.webSocketDebuggerUrl) { - wsUrl = page.webSocketDebuggerUrl - break - } - } catch {} - } - - if (!wsUrl) { - chromeProc.kill() - server.close() - throw new Error('Failed to connect to Chrome headless on port 9223') - } - - console.log('3. Connected to Chrome WebSocket:', wsUrl) - const ws = new WebSocket(wsUrl) - await new Promise(resolve => ws.onopen = resolve) - - let idCounter = 1 - function sendCommand(method: string, params: any = {}): Promise { - return new Promise((resolve, reject) => { - const id = idCounter++ - const handler = (event: any) => { - const msg = JSON.parse(event.data) - if (msg.id === id) { - ws.removeEventListener('message', handler) - if (msg.error) reject(msg.error) - else resolve(msg.result) - } - } - ws.addEventListener('message', handler) - ws.send(JSON.stringify({ id, method, params })) - }) - } - - await sendCommand('Runtime.enable') - await sendCommand('Page.enable') - await sleep(1500) - - // Test 1: Check initial Amazon render - console.log('4. Auditing initial page render (Amazon)...') - const initialData = await sendCommand('Runtime.evaluate', { - expression: `(() => { - const titles = Array.from(document.querySelectorAll('.tree-title')).map(el => el.textContent.trim()); - const sockets = document.querySelectorAll('.skill-socket'); - const reqLvl = document.getElementById('val-req-level')?.textContent; - const spent = document.getElementById('val-points-spent')?.textContent; - return { titles, socketCount: sockets.length, reqLvl, spent }; - })()`, - returnByValue: true, - }) - console.log(' Initial render state:', initialData.result.value) - if (initialData.result.value.socketCount !== 30) { - throw new Error(`Expected 30 skill sockets for Amazon, found: ${initialData.result.value.socketCount}`) - } - if (initialData.result.value.titles.length !== 3) { - throw new Error(`Expected 3 tree titles, found: ${initialData.result.value.titles.length}`) - } - - // Test 2: Left-click point allocation on Jab (id 10) - console.log('5. Auditing Left-click allocation on Jab (id 10)...') - const allocData = await sendCommand('Runtime.evaluate', { - expression: `(() => { - const jabSocket = document.querySelector('.skill-socket[data-skill-id="10"]'); - if (!jabSocket) return { error: 'Jab socket not found' }; - // Left click - jabSocket.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); - const newJab = document.querySelector('.skill-socket[data-skill-id="10"]'); - const badge = newJab?.querySelector('.skill-pts-badge')?.textContent; - const reqLvl = document.getElementById('val-req-level')?.textContent; - const spent = document.getElementById('val-points-spent')?.textContent; - return { badge, reqLvl, spent }; - })()`, - returnByValue: true, - }) - console.log(' After 1 click on Jab:', allocData.result.value) - if (allocData.result.value.badge !== '1') { - throw new Error(`Expected Jab badge to be 1, got ${allocData.result.value.badge}`) - } - - // Shift+Click allocation (+5 points) - console.log('6. Auditing Shift+Left-click allocation on Jab (+5 points)...') - const shiftAllocData = await sendCommand('Runtime.evaluate', { - expression: `(() => { - const jabSocket = document.querySelector('.skill-socket[data-skill-id="10"]'); - jabSocket.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: true })); - const newJab = document.querySelector('.skill-socket[data-skill-id="10"]'); - const badge = newJab?.querySelector('.skill-pts-badge')?.textContent; - const spent = document.getElementById('val-points-spent')?.textContent; - return { badge, spent }; - })()`, - returnByValue: true, - }) - console.log(' After Shift+Click on Jab:', shiftAllocData.result.value) - if (shiftAllocData.result.value.badge !== '6') { - throw new Error(`Expected Jab badge to be 6, got ${shiftAllocData.result.value.badge}`) - } - - // Max out Jab to 20 points - await sendCommand('Runtime.evaluate', { - expression: `(() => { - const jabSocket = document.querySelector('.skill-socket[data-skill-id="10"]'); - for (let i = 0; i < 3; i++) { - jabSocket.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: true })); - } - })()`, - }) - - // Test 3: Prerequisite chain unlocking - console.log('7. Auditing Prerequisite chain: Power Strike (id 14) and Impale (id 19)...') - const prereqCheck = await sendCommand('Runtime.evaluate', { - expression: `(() => { - const psSocket = document.querySelector('.skill-socket[data-skill-id="14"]'); - const impSocket = document.querySelector('.skill-socket[data-skill-id="19"]'); - const psLocked = psSocket?.classList.contains('locked'); - const impLocked = impSocket?.classList.contains('locked'); - // Allocate 5 points to Power Strike - psSocket.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: true })); - const newPs = document.querySelector('.skill-socket[data-skill-id="14"]'); - const psBadge = newPs?.querySelector('.skill-pts-badge')?.textContent; - return { psLocked, impLocked, psBadge }; - })()`, - returnByValue: true, - }) - console.log(' Prerequisite unlocking result:', prereqCheck.result.value) - if (prereqCheck.result.value.psLocked || prereqCheck.result.value.impLocked) { - throw new Error('Power Strike or Impale remained locked after allocating prerequisite Jab!') - } - if (prereqCheck.result.value.psBadge !== '5') { - throw new Error(`Expected Power Strike badge to be 5, got ${prereqCheck.result.value.psBadge}`) - } - - // Test 4: Tooltip verification on Charged Strike (id 24) - console.log('8. Auditing Hover Tooltip on Charged Strike (id 24)...') - const tooltipCheck = await sendCommand('Runtime.evaluate', { - expression: `(() => { - const csSocket = document.querySelector('.skill-socket[data-skill-id="24"]'); - const rect = csSocket.getBoundingClientRect(); - csSocket.dispatchEvent(new MouseEvent('mouseenter', { - bubbles: true, - clientX: rect.left + 20, - clientY: rect.top + 20 - })); - const tt = document.getElementById('skill-tooltip'); - const display = tt?.style.display; - const title = tt?.querySelector('.tt-title')?.textContent; - const damage = tt?.querySelector('.tt-damage-main')?.textContent?.trim(); - const synergies = Array.from(tt?.querySelectorAll('.tt-syn-item') || []).map(el => el.textContent.trim()); - return { display, title, damage, synergies }; - })()`, - returnByValue: true, - }) - console.log(' Hover Tooltip data:', tooltipCheck.result.value) - if (tooltipCheck.result.value.display !== 'block') { - throw new Error('Tooltip was not displayed on mouseenter!') - } - if (!tooltipCheck.result.value.title?.includes('充能') && !tooltipCheck.result.value.title?.includes('Charged Strike')) { - throw new Error(`Unexpected tooltip title: ${tooltipCheck.result.value.title}`) - } - - // Test 5: Right-click deallocation and Orphan Protection - console.log('9. Auditing Orphan Protection on Jab and Power Strike...') - const orphanCheck = await sendCommand('Runtime.evaluate', { - expression: `(() => { - // Allocate prerequisites for Charged Strike (24): Poison Javelin (15) -> Lightning Bolt (20) -> Charged Strike (24) - document.querySelector('.skill-socket[data-skill-id="15"]')?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); - document.querySelector('.skill-socket[data-skill-id="20"]')?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); - document.querySelector('.skill-socket[data-skill-id="24"]')?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); - - // Now attempt to right-click Power Strike (14) down to 0 points with shiftKey - document.querySelector('.skill-socket[data-skill-id="14"]')?.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true, shiftKey: true })); - - const newPs = document.querySelector('.skill-socket[data-skill-id="14"]'); - const psBadge = newPs?.querySelector('.skill-pts-badge')?.textContent; - const toastText = document.getElementById('toast')?.textContent; - return { psBadge, toastText }; - })()`, - returnByValue: true, - }) - console.log(' Orphan protection result:', orphanCheck.result.value) - if (orphanCheck.result.value.psBadge === '0') { - throw new Error('Orphan protection failed! Power Strike was reduced to 0 while Charged Strike still depends on it!') - } - - // Test 6: Class switcher to Sorceress, URL Hash, and bilingual toggle - console.log('10. Auditing Class Switcher to Sorceress (sor)...') - const sorCheck = await sendCommand('Runtime.evaluate', { - expression: `(() => { - const sorBtn = document.querySelector('.class-btn[data-class-code="sor"]'); - sorBtn.click(); - const titles = Array.from(document.querySelectorAll('.tree-title')).map(el => el.textContent.trim()); - const hash = window.location.hash; - return { titles, hash }; - })()`, - returnByValue: true, - }) - console.log(' Sorceress tabs:', sorCheck.result.value) - if (!sorCheck.result.value.titles[0]?.includes('火焰法术') && !sorCheck.result.value.titles[0]?.includes('Fire Spells')) { - throw new Error('Failed to switch to Sorceress trees!') - } - - // Test 7: Bilingual toggle - console.log('11. Auditing Language Toggle...') - const langCheck = await sendCommand('Runtime.evaluate', { - expression: `(() => { - const btnLang = document.getElementById('btn-lang'); - btnLang.click(); // Switch to EN - const titlesEn = Array.from(document.querySelectorAll('.tree-title')).map(el => el.textContent.trim()); - const respecText = document.getElementById('btn-respec')?.textContent; - return { titlesEn, respecText }; - })()`, - returnByValue: true, - }) - console.log(' English switch result:', langCheck.result.value) - if (!langCheck.result.value.titlesEn[0]?.includes('Fire Spells')) { - throw new Error(`Expected English tab 'Fire Spells', got: ${langCheck.result.value.titlesEn[0]}`) - } - - // Test 8: Non-Damage Skills Tooltip Display Audit (Warmth, Raise Skeleton, Battle Orders, Fanaticism) - console.log('12. Auditing Non-Damage Skills (Warmth, Raise Skeleton, Battle Orders, Fanaticism)...') - const nonDamageAudit = await sendCommand('Runtime.evaluate', { - expression: `(() => { - document.getElementById('btn-lang').click(); // Back to Chinese - - // 1. Warmth (37) - const warmthSocket = document.querySelector('.skill-socket[data-skill-id="37"]'); - warmthSocket.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); - const tt = document.getElementById('skill-tooltip'); - const warmthHasDmg = Boolean(tt.querySelector('.tt-damage-box')); - const warmthEffects = Array.from(tt.querySelectorAll('.tt-effect-line')).map(e => e.textContent); - - // 2. Switch to Necromancer (nec) for Raise Skeleton (70) - document.querySelector('.class-btn[data-class-code="nec"]').click(); - const skelSocket = document.querySelector('.skill-socket[data-skill-id="70"]'); - const smSocket = document.querySelector('.skill-socket[data-skill-id="69"]'); - // Allocate 20 Raise Skeleton & 20 Skeleton Mastery - for (let i = 0; i < 4; i++) skelSocket.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: true })); - for (let i = 0; i < 4; i++) smSocket.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: true })); - - const skelRect = skelSocket.getBoundingClientRect(); - skelSocket.dispatchEvent(new MouseEvent('mouseenter', { - bubbles: true, - clientX: skelRect.left + 24, - clientY: skelRect.top + 24 - })); - const skelHasDmg = Boolean(tt.querySelector('.tt-damage-box')); - const skelEffects = Array.from(tt.querySelectorAll('.tt-effect-line')).map(e => e.textContent); - - // 3. Switch to Barbarian (bar) for Battle Orders (149) - document.querySelector('.class-btn[data-class-code="bar"]').click(); - const boSocket = document.querySelector('.skill-socket[data-skill-id="149"]'); - boSocket.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); - const boHasDmg = Boolean(tt.querySelector('.tt-damage-box')); - const boEffects = Array.from(tt.querySelectorAll('.tt-effect-line')).map(e => e.textContent); - - // 4. Switch to Paladin (pal) for Fanaticism (122) - document.querySelector('.class-btn[data-class-code="pal"]').click(); - const fanSocket = document.querySelector('.skill-socket[data-skill-id="122"]'); - fanSocket.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); - const fanHasDmg = Boolean(tt.querySelector('.tt-damage-box')); - const fanEffects = Array.from(tt.querySelectorAll('.tt-effect-line')).map(e => e.textContent); - - return { - warmth: { hasDmg: warmthHasDmg, effects: warmthEffects }, - raiseSkeleton: { hasDmg: skelHasDmg, effects: skelEffects }, - battleOrders: { hasDmg: boHasDmg, effects: boEffects }, - fanaticism: { hasDmg: fanHasDmg, effects: fanEffects }, - }; - })()`, - returnByValue: true, - }) - - console.log(' Non-Damage Audit Results:', JSON.stringify(nonDamageAudit.result.value, null, 2)) - const nd = nonDamageAudit.result.value - if (nd.warmth.hasDmg) throw new Error('Warmth MUST NOT display direct damage!') - if (!nd.warmth.effects.some((t: string) => t.includes('法力回復速度'))) { - throw new Error('Warmth missing mana recovery rate effect!') - } - if (nd.raiseSkeleton.hasDmg) throw new Error('Raise Skeleton MUST NOT display direct damage!') - if (!nd.raiseSkeleton.effects.some((t: string) => t.includes('骷髏總數'))) { - throw new Error('Raise Skeleton missing skeleton count effect!') - } - if (nd.battleOrders.hasDmg) throw new Error('Battle Orders MUST NOT display direct damage!') - if (!nd.battleOrders.effects.some((t: string) => t.includes('生命最大值'))) { - throw new Error('Battle Orders missing max life effect!') - } - if (nd.fanaticism.hasDmg) throw new Error('Fanaticism MUST NOT display direct damage!') - if (!nd.fanaticism.effects.some((t: string) => t.includes('攻擊速度'))) { - throw new Error('Fanaticism missing attack speed effect!') - } - console.log('✓ Non-Damage Skills Tooltip Audit Passed: No fake damage, all authentic mechanics displayed!') - - // Capture Necromancer Raise Skeleton Screenshot - console.log('12.1 Capturing Necromancer Raise Skeleton Tooltip Screenshot...') - await sendCommand('Runtime.evaluate', { - expression: `(() => { - document.querySelector('.class-btn[data-class-code="nec"]').click(); - const skelSocket = document.querySelector('.skill-socket[data-skill-id="70"]'); - const smSocket = document.querySelector('.skill-socket[data-skill-id="69"]'); - for (let i = 0; i < 4; i++) skelSocket.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: true })); - for (let i = 0; i < 4; i++) smSocket.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: true })); - - const skelRect = skelSocket.getBoundingClientRect(); - skelSocket.dispatchEvent(new MouseEvent('mouseenter', { - bubbles: true, - clientX: skelRect.left + 24, - clientY: skelRect.top + 24 - })); - })()`, - }) - await sleep(500) - const necShot = await sendCommand('Page.captureScreenshot', { format: 'png', quality: 100, fromSurface: true }) - const necBuf = Buffer.from(necShot.data, 'base64') - const necPathLocal = join(process.cwd(), 'tests', 'skills_calculator_nec_audit.png') - const necPathArtifact = '/usr/local/google/home/taodao/.gemini/jetski/brain/d8366262-b464-4b0a-bff5-16918852fb1a/skills_calculator_nec_audit.png' - const necPathX20 = '/google/data/rw/users/ta/taodao/skills_calculator_nec_audit.png' - writeFileSync(necPathLocal, necBuf) - writeFileSync(necPathArtifact, necBuf) - try { copyFileSync(necPathLocal, necPathX20) } catch {} - console.log(`✓ Saved Necromancer screenshot to: ${necPathArtifact} and x20`) - - // Switch back to Sorceress and allocate Fire Ball build for final screenshot - console.log('13. Configuring Fire Sorceress build for visual audit screenshot...') - await sendCommand('Runtime.evaluate', { - expression: `(() => { - document.querySelector('.class-btn[data-class-code="sor"]').click(); - - // Allocate 20 Fire Bolt (36) - const bolt = document.querySelector('.skill-socket[data-skill-id="36"]'); - for (let i = 0; i < 4; i++) bolt.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: true })); - - // Allocate 20 Fire Ball (47) - const ball = document.querySelector('.skill-socket[data-skill-id="47"]'); - for (let i = 0; i < 4; i++) ball.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: true })); - - // Allocate 20 Fire Mastery (61) - const fm = document.querySelector('.skill-socket[data-skill-id="61"]'); - for (let i = 0; i < 4; i++) fm.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: true })); - - // Add +3 All Skills - const incBtn = document.getElementById('btn-all-skills-inc'); - incBtn.click(); incBtn.click(); incBtn.click(); - - // Trigger hover tooltip on Fire Ball (47) - const ballRect = ball.getBoundingClientRect(); - ball.dispatchEvent(new MouseEvent('mouseenter', { - bubbles: true, - clientX: ballRect.left + 24, - clientY: ballRect.top + 24 - })); - })()`, - }) - - await sleep(600) - - // Capture screenshot of the full viewport - console.log('13. Capturing full-page screenshot via CDP...') - const screenshotResult = await sendCommand('Page.captureScreenshot', { - format: 'png', - quality: 100, - fromSurface: true, - }) - - const screenshotBuf = Buffer.from(screenshotResult.data, 'base64') - const localScreenshotPath = join(process.cwd(), 'tests', 'skills_calculator_audit.png') - const publicScreenshotPath = join(process.cwd(), 'public', 'skills_calculator_audit.png') - const x20Dir = '/google/data/rw/users/ta/taodao' - const x20Path = join(x20Dir, 'skills_calculator_audit.png') - const artifactPath = '/usr/local/google/home/taodao/.gemini/jetski/brain/d8366262-b464-4b0a-bff5-16918852fb1a/skills_calculator_audit.png' - - writeFileSync(localScreenshotPath, screenshotBuf) - writeFileSync(publicScreenshotPath, screenshotBuf) - writeFileSync(artifactPath, screenshotBuf) - if (existsSync(x20Dir)) { - try { - copyFileSync(localScreenshotPath, x20Path) - console.log(`✓ Copied audit screenshot to x20: ${x20Path}`) - } catch (e) { - console.warn('Could not copy to x20:', e) - } - } - - console.log(`✓ Saved screenshot to: ${localScreenshotPath}`) - console.log(`✓ Saved screenshot to: ${artifactPath}`) - - // Cleanup - ws.close() - chromeProc.kill() - server.close() - - console.log('=== All Browser Audit Tests Passed Successfully! ===') -} - -runAudit().catch(err => { - console.error('Audit failed:', err) - process.exit(1) -}) diff --git a/scripts/verify-maxroll-loot.ts b/scripts/verify-maxroll-loot.ts deleted file mode 100644 index a98b2dc..0000000 --- a/scripts/verify-maxroll-loot.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { - MONSTER_PRESETS, - FAMOUS_ITEMS_META, - classifyItemCategory, - loadCanonicalTreasureClasses, - calculateAnalyticProbabilities, - runMonteCarloSimulation, - buildReverseLookupIndex, - reverseLookup, -} from '../src/loot'; - -async function main() { - console.log('=== [1] 验证 MONSTER_PRESETS 与 TC 完整性 ==='); - const { tcTable, autoTcs } = loadCanonicalTreasureClasses(); - console.log(`载入系统 TC 节点数: ${tcTable.all.length}, 自动生成 TC 节点数: ${autoTcs.size}`); - - let missingTcCount = 0; - for (const mon of MONSTER_PRESETS) { - for (const diff of ['normal', 'nightmare', 'hell'] as const) { - const tcName = mon.tc[diff]; - if (!tcTable.get(tcName) && !autoTcs.has(tcName)) { - console.error(`❌ [缺失] 怪物 ${mon.nameCn} 在 ${diff} 难度的 TC 不存在: ${tcName}`); - missingTcCount++; - } - if (mon.questTc) { - const questTcName = mon.questTc[diff]; - if (!tcTable.get(questTcName) && !autoTcs.has(questTcName)) { - console.error(`❌ [缺失] 怪物 ${mon.nameCn} 在 ${diff} 难度的 首杀TC 不存在: ${questTcName}`); - missingTcCount++; - } - } - } - } - - if (missingTcCount === 0) { - console.log(`✅ 所有 ${MONSTER_PRESETS.length} 个怪物预设在全难度的基础 TC 与首杀 TC 校验 100% 存在!`); - } else { - throw new Error(`存在 ${missingTcCount} 个无效的 TC 引用`); - } - - console.log('\n=== [2] 验证 FAMOUS_ITEMS_META 与物品分类器 ==='); - const famousCount = Object.keys(FAMOUS_ITEMS_META).length; - console.log(`已收录核心暗金与极品底模元数据条目: ${famousCount} 条`); - - // Test classifyItemCategory - const r30Cat = classifyItemCategory('r30'); - const shakoCat = classifyItemCategory('uap', 'unique'); - const phaseBladeCat = classifyItemCategory('7cr'); - const sojCat = classifyItemCategory('rin', 'unique'); - const tokenCat = classifyItemCategory('tes'); - - console.log(`- 30# 贝 (r30) 分类: ${r30Cat} (期望: rune)`); - console.log(`- 军帽 (uap, unique) 分类: ${shakoCat} (期望: unique)`); - console.log(`- 幻化之刃 (7cr) 分类: ${phaseBladeCat} (期望: base)`); - console.log(`- 戒指/护身符 (rin) 分类: ${sojCat} (期望: charm_gem)`); - console.log(`- 洗点徽章 (tes) 分类: ${tokenCat} (期望: misc)`); - - if (r30Cat !== 'rune' || shakoCat !== 'unique' || phaseBladeCat !== 'base' || sojCat !== 'charm_gem' || tokenCat !== 'misc') { - throw new Error('分类器逻辑测试失败'); - } - console.log('✅ 物品多维分类器运行正确!'); - - console.log('\n=== [3] 验证精确概率表解析 (Baal H 与 Andariel H) ==='); - const baalOdds = calculateAnalyticProbabilities('Baal (H)', { tcTable, autoTcs, gamePlayers: 1, partyPlayers: 1 }); - console.log(`Baal (H) - 叶子节点数: ${baalOdds.leaves.length}, Picks: ${baalOdds.picks}, NoDrop: ${(baalOdds.noDropCompoundProb * 100).toFixed(2)}%`); - - // Verify Shako / uap odds for Baal - const uapLeaf = baalOdds.leaves.find(l => l.item === 'uap'); - if (uapLeaf) { - console.log(`- 军帽底模 (uap) 单次概率: ${(uapLeaf.singleProbability * 100).toFixed(4)}%, 综合概率: ${(uapLeaf.compoundProbability * 100).toFixed(4)}%, 几率比: 1 : ${uapLeaf.oneInOdds.toFixed(1)}`); - } - - // Verify Quest Drop for Andariel - const andyNormalOdds = calculateAnalyticProbabilities('Andariel (H)', { tcTable, autoTcs, gamePlayers: 1, partyPlayers: 1 }); - const andyQuestOdds = calculateAnalyticProbabilities('Andarielq (H)', { tcTable, autoTcs, gamePlayers: 1, partyPlayers: 1 }); - console.log(`Andariel (H) 普通杀 NoDrop: ${(andyNormalOdds.noDropCompoundProb * 100).toFixed(2)}%`); - console.log(`Andarielq (H) Bug首杀 NoDrop: ${(andyQuestOdds.noDropCompoundProb * 100).toFixed(2)}%`); - - const andyNormalUap = andyNormalOdds.leaves.find(l => l.item === 'uap')?.compoundProbability || 0; - const andyQuestUap = andyQuestOdds.leaves.find(l => l.item === 'uap')?.compoundProbability || 0; - console.log(`- 安姐普通杀 军帽(uap) 几率: ${(andyNormalUap * 100).toFixed(4)}%`); - console.log(`- 安姐Bug首杀 军帽(uap) 几率: ${(andyQuestUap * 100).toFixed(4)}% (提升 +${(((andyQuestUap - andyNormalUap) / andyNormalUap) * 100).toFixed(1)}%)`); - - const andyQuestHasJunk = andyQuestOdds.leaves.some(l => l.item === 'tsc' || l.item === 'isc' || l.item === 'gld'); - if (andyQuestHasJunk) { - throw new Error('安姐首杀 TC 不应包含卷轴或金币等垃圾杂项!'); - } - if (andyQuestUap <= andyNormalUap) { - throw new Error('安姐首杀装备出货率应当显著高于普通击杀!'); - } - console.log('✅ 安姐首杀剔除杂质与装备出货率暴增验证通过!'); - - console.log('\n=== [4] 验证 1,000 次蒙特卡洛掉落模拟 (实测出货与品质分布) ==='); - const simRes = await runMonteCarloSimulation({ - tcName: 'Baal (H)', - runs: 1000, - gamePlayers: 3, - partyPlayers: 1, - magicFind: 250, - difficulty: 'hell', - tcTable, - autoTcs, - }); - - console.log(`模拟总轮数: ${simRes.totalRuns}`); - console.log(`掉落总件数: ${simRes.itemsDropped} 件 (平均每轮 ${(simRes.itemsDropped / simRes.totalRuns).toFixed(2)} 件)`); - console.log(`品质分布:`); - console.log(` - Unique (暗金): ${simRes.qualityCounts.unique} 件`); - console.log(` - Set (套装): ${simRes.qualityCounts.set} 件`); - console.log(` - Rare (稀有): ${simRes.qualityCounts.rare} 件`); - console.log(` - Magic (魔法): ${simRes.qualityCounts.magic} 件`); - console.log(` - Rune (符文): ${simRes.qualityCounts.rune} 件`); - console.log(` - Normal/Sup (底模): ${simRes.qualityCounts.normal + simRes.qualityCounts.superior} 件`); - console.log(` - Gold (金币): ${simRes.qualityCounts.gold} 堆`); - - if (simRes.itemsDropped === 0) { - throw new Error('1000次模拟依然为0件掉落!'); - } - console.log(`✅ 1,000 次模拟正常出货,共 ${simRes.itemSummaries.length} 种不同物品!`); - - console.log('\n=== [5] 验证反查索引 (Reverse Lookup) ==='); - const revIndex = buildReverseLookupIndex(tcTable, autoTcs); - const shakoRev = reverseLookup(revIndex, 'shako'); - console.log(`搜索 "shako" 匹配到的掉落源数量: ${shakoRev[0]?.entries.length || 0} 个 TC`); - if (shakoRev.length > 0 && shakoRev[0]!.entries.length > 0) { - console.log(`- 最佳掉落源 TOP 1: ${shakoRev[0]!.entries[0]!.tcName}, 综合几率: ${(shakoRev[0]!.entries[0]!.compoundProbability * 100).toFixed(4)}%`); - } else { - throw new Error('反查 shako 失败'); - } - console.log('✅ 反查索引测试通过!'); - - console.log('\n========================================'); - console.log('🎉 全部 Maxroll 怪物选择器与掉落列表核心功能实测 100% 成功!'); - console.log('========================================'); -} - -main().catch(err => { - console.error(err); - process.exit(1); -}); diff --git a/skills.html b/skills.html deleted file mode 100644 index f37124e..0000000 --- a/skills.html +++ /dev/null @@ -1,652 +0,0 @@ - - - - - - - 暗黑破坏神 2 · 全职业技能加点模拟器 (Diablo II Skill Calculator v1.13c) - - - -
-
- 暗黑破坏神 2 · 技能加点模拟器 - v1.13c -
- - - - -
- -
-
-
- 需求角色等级: - 1 -
-
- 已用技能点: - 0 / 110 -
-
- 剩余技能点: - 110 -
-
- + 全部技能: -
- - 0 - -
-
-
- -
- - -
-
- -
-
- -
-
- - -
- - -
已复制配点链接到剪贴板!
- - - - diff --git a/src/arena.ts b/src/arena.ts deleted file mode 100644 index 73e157d..0000000 --- a/src/arena.ts +++ /dev/null @@ -1,1842 +0,0 @@ -/** - * Standalone Combat Mathematics & Attack/Magic Testing Arena (Issue #24 / M9). - * - * Renders 1 Player and 1 Monster in an isometric testing duel ring. - * Allows interactive configuration of character attributes, monster types, and levels. - * Provides real-time execution and step-by-step mathematical breakdown for: - * - Melee physical attacks (AR vs DR to-hit, Shield Block, Critical/Deadly Strike, Crushing Blow, DR%) - * - Fire, Cold, Lightning, Poison, and Magic spells (resistances, immunities, MDR, chill/stun effects) - * - Monster counter-attacks, Hit Recovery (FHR), and continuous DPS benchmarks. - */ - -import { - calculateAttack, - calculateHitChance, - calculatePlayerBlockChance, - calculateCriticalChance, - evaluateCriticalAndDeadlyStrike, - calculateCrushingBlowDamage, - calculateOpenWoundsDamage, - calculateLeech, - calculateFreezeTarget, - calculateResistanceFactor, - calculateResistanceReduction, - calculateMonsterRegeneration, - evaluatePreventMonsterHeal, - calculateKnockbackSuccess, - calculateSlowTarget, - calculateHitBlindsTarget, - calculateHitCausesFlee, - calculatePhysicalWeaponDamage, - calculatePoisonStack, - checkHitRecovery, - D2_EXPERIENCE_TABLE_99, -} from './game/formulas.ts' -import type { CombatCalculationResult, TargetTier, GameDifficulty, MonsterSize } from './game/formulas.ts' - -// --- Monster Database & Scaling --------------------------------------------- - -interface MonsterBaseDefinition { - readonly id: string - readonly name: string - readonly category: string - readonly baseMinHp: number - readonly baseMaxHp: number - readonly baseAc: number - readonly baseTh: number - readonly baseMinD: number - readonly baseMaxD: number - readonly baseExp: number - readonly blockPercent: number - readonly resPhysical: number - readonly resFire: number - readonly resLightning: number - readonly resCold: number - readonly resPoison: number - readonly resMagic: number - readonly color: string - readonly tier?: TargetTier - readonly drainEffectiveness?: number - readonly size?: MonsterSize - readonly regenRate?: number -} - -const MONSTER_TEMPLATES: Record = { - fallen1: { - id: 'fallen1', name: '沉沦魔 (Fallen)', category: '恶魔 (Demon)', - baseMinHp: 21, baseMaxHp: 61, baseAc: 84, baseTh: 101, baseMinD: 51, baseMaxD: 101, baseExp: 61, - blockPercent: 0, resPhysical: 0, resFire: 0, resLightning: 0, resCold: 0, resPoison: 0, resMagic: 0, - color: '#e64a19', tier: 'normal', drainEffectiveness: 100, size: 'small', regenRate: 0, - }, - zombie1: { - id: 'zombie1', name: '僵尸 (Zombie)', category: '不死 (Undead)', - baseMinHp: 31, baseMaxHp: 61, baseAc: 64, baseTh: 81, baseMinD: 51, baseMaxD: 101, baseExp: 111, - blockPercent: 0, resPhysical: 0, resFire: -20, resLightning: 0, resCold: 0, resPoison: 0, resMagic: 0, - color: '#558b2f', tier: 'normal', drainEffectiveness: 50, size: 'medium', regenRate: 5, - }, - quillrat1: { - id: 'quillrat1', name: '刺鼠 (Quill Rat)', category: '动物 (Animal)', - baseMinHp: 21, baseMaxHp: 41, baseAc: 64, baseTh: 101, baseMinD: 51, baseMaxD: 101, baseExp: 71, - blockPercent: 0, resPhysical: 0, resFire: 0, resLightning: 0, resCold: 0, resPoison: 0, resMagic: 0, - color: '#8d6e63', tier: 'normal', drainEffectiveness: 100, size: 'small', regenRate: 0, - }, - skeleton1: { - id: 'skeleton1', name: '骷髅战士 (Skeleton)', category: '不死 (Undead)', - baseMinHp: 21, baseMaxHp: 51, baseAc: 84, baseTh: 81, baseMinD: 51, baseMaxD: 101, baseExp: 81, - blockPercent: 15, resPhysical: 0, resFire: 0, resLightning: 0, resCold: 0, resPoison: 25, resMagic: 0, - color: '#cfd8dc', tier: 'normal', drainEffectiveness: 0, size: 'medium', regenRate: 0, - }, - corruptrogue1: { - id: 'corruptrogue1', name: '黑暗猎手 (Dark Hunter)', category: '恶魔 (Demon)', - baseMinHp: 25, baseMaxHp: 55, baseAc: 101, baseTh: 121, baseMinD: 61, baseMaxD: 111, baseExp: 91, - blockPercent: 20, resPhysical: 0, resFire: 0, resLightning: 0, resCold: 0, resPoison: 0, resMagic: 0, - color: '#7b1fa2', tier: 'normal', drainEffectiveness: 100, size: 'medium', regenRate: 0, - }, - goatman1: { - id: 'goatman1', name: '月亮氏族羊头人 (Moon Clan)', category: '动物 (Animal)', - baseMinHp: 41, baseMaxHp: 81, baseAc: 101, baseTh: 101, baseMinD: 81, baseMaxD: 141, baseExp: 131, - blockPercent: 0, resPhysical: 0, resFire: 0, resLightning: 0, resCold: 20, resPoison: 0, resMagic: 0, - color: '#4e342e', tier: 'normal', drainEffectiveness: 100, size: 'large', regenRate: 0, - }, - bloodraven: { - id: 'bloodraven', name: '血鸟 (Blood Raven)', category: '暗金Boss (SuperUnique)', - baseMinHp: 120, baseMaxHp: 200, baseAc: 150, baseTh: 180, baseMinD: 100, baseMaxD: 180, baseExp: 500, - blockPercent: 30, resPhysical: 10, resFire: 20, resLightning: 20, resCold: 20, resPoison: 20, resMagic: 0, - color: '#c2185b', tier: 'boss', drainEffectiveness: 50, size: 'medium', regenRate: 8, - }, - andariel: { - id: 'andariel', name: '安达利尔 (Andariel)', category: '幕终首领 (Act Boss)', - baseMinHp: 1025, baseMaxHp: 1200, baseAc: 175, baseTh: 220, baseMinD: 150, baseMaxD: 260, baseExp: 3200, - blockPercent: 0, resPhysical: 15, resFire: -50, resLightning: 50, resCold: 50, resPoison: 80, resMagic: 0, - color: '#2e7d32', tier: 'boss', drainEffectiveness: 100, size: 'large', regenRate: 15, - }, - duriel: { - id: 'duriel', name: '督瑞尔 (Duriel)', category: '幕终首领 (Act Boss)', - baseMinHp: 3995, baseMaxHp: 4500, baseAc: 300, baseTh: 350, baseMinD: 250, baseMaxD: 400, baseExp: 8500, - blockPercent: 0, resPhysical: 20, resFire: 20, resLightning: 20, resCold: 50, resPoison: 20, resMagic: 0, - color: '#00838f', tier: 'boss', drainEffectiveness: 100, size: 'large', regenRate: 20, - }, - mephisto: { - id: 'mephisto', name: '墨菲斯托 (Mephisto)', category: '幕终首领 (Act Boss)', - baseMinHp: 7300, baseMaxHp: 8000, baseAc: 450, baseTh: 500, baseMinD: 350, baseMaxD: 550, baseExp: 18000, - blockPercent: 25, resPhysical: 20, resFire: 50, resLightning: 75, resCold: 50, resPoison: 75, resMagic: 0, - color: '#1565c0', tier: 'boss', drainEffectiveness: 20, size: 'large', regenRate: 25, - }, - diablo: { - id: 'diablo', name: '暗黑破坏神 (Diablo)', category: '幕终首领 (Act Boss)', - baseMinHp: 13800, baseMaxHp: 15000, baseAc: 600, baseTh: 700, baseMinD: 500, baseMaxD: 800, baseExp: 35000, - blockPercent: 30, resPhysical: 30, resFire: 50, resLightning: 50, resCold: 50, resPoison: 50, resMagic: 0, - color: '#d32f2f', tier: 'boss', drainEffectiveness: 50, size: 'large', regenRate: 35, - }, - baal: { - id: 'baal', name: '毁灭之王巴尔 (Baal)', category: '幕终首领 (Act Boss)', - baseMinHp: 26000, baseMaxHp: 30000, baseAc: 800, baseTh: 900, baseMinD: 600, baseMaxD: 1000, baseExp: 60000, - blockPercent: 40, resPhysical: 50, resFire: 50, resLightning: 50, resCold: 50, resPoison: 50, resMagic: 0, - color: '#ff8f00', tier: 'boss', drainEffectiveness: 33, size: 'large', regenRate: 45, - }, - dummy: { - id: 'dummy', name: '测伤木桩 (Training Dummy)', category: '无敌靶桩 (Dummy)', - baseMinHp: 100000, baseMaxHp: 100000, baseAc: 100, baseTh: 100, baseMinD: 10, baseMaxD: 20, baseExp: 100, - blockPercent: 0, resPhysical: 0, resFire: 0, resLightning: 0, resCold: 0, resPoison: 0, resMagic: 0, - color: '#ffb300', tier: 'normal', drainEffectiveness: 100, size: 'large', regenRate: 0, - }, -} - -/** Scale multipliers derived from MonLvl.txt per level */ -function computeMonsterScale(level: number) { - const l = Math.max(1, Math.min(99, level)) - return { - ac: Math.round(6 + 2.2 * l), - th: Math.round(8 + 2.8 * l), - hp: Math.round(7 + 3.0 * l), - dm: Math.round(2 + 1.2 * l), - xp: Math.round(30 + 10 * l), - } -} - -// --- Visual & Particle FX State --------------------------------------------- - -interface FloatingText { - x: number - y: number - text: string - color: string - size: number - alpha: number - vy: number - life: number -} - -interface ProjectileFx { - x: number - y: number - targetX: number - targetY: number - type: 'fire' | 'cold' | 'light' | 'magic' - progress: number - speed: number - onHit: () => void -} - -interface ParticleFx { - x: number - y: number - vx: number - vy: number - color: string - size: number - life: number - maxLife: number -} - -// --- App State -------------------------------------------------------------- - -class CombatArenaApp { - private canvas!: HTMLCanvasElement - private ctx!: CanvasRenderingContext2D - - // Player State - private player = { - class: 'Sorceress', - level: 30, - hp: 350, - maxHp: 350, - mana: 200, - maxMana: 200, - ar: 1200, - dr: 650, - dmgMin: 25, - dmgMax: 65, - fire: 15, - cold: 10, - light: 25, - poison: 0, - magic: 0, - block: 45, - cs: 20, - ds: 15, - cb: 25, - ow: 25, - ll: 10, - ml: 5, - itd: false, - defRed: 25, - freezeBonus: 1, - knockback: 25, - difficulty: 'normal' as GameDifficulty, - flatDr: 2, - flatMdr: 2, - fhr: 30, - // PvE mechanics - pmh: true, - isMerc: false, - conviction: 0, - lowerRes: 0, - minusLightRes: 0, - minusFireRes: 0, - minusColdRes: 0, - minusPoisonRes: 0, - slow: 0, - hbt: 0, - hcmtf: 0, - flatDmg: 0, - offWeaponEd: 0, - statBonus: 0, - // Animation state - action: 'idle' as 'idle' | 'attack' | 'cast' | 'hit' | 'dead', - actionTimer: 0, - hitFlash: 0, - } - - // Monster State - private monster = { - typeKey: 'fallen1', - name: '沉沦魔 (Fallen)', - category: '恶魔 (Demon)', - tier: 'normal' as TargetTier, - posture: 'stand' as 'stand' | 'run', - level: 28, - hp: 180, - maxHp: 180, - dr: 480, - ar: 520, - dmgMin: 12, - dmgMax: 28, - block: 0, - drainEffectiveness: 100, - resPhysical: 0, - resFire: 0, - resCold: 0, - resLightning: 0, - resPoison: 0, - resMagic: 0, - flatDr: 0, - flatMdr: 0, - size: 'medium' as MonsterSize, - regenRate: 0, - isUber: false, - // Animation & Status state - action: 'idle' as 'idle' | 'attack' | 'hit' | 'dead', - actionTimer: 0, - hitFlash: 0, - color: '#e64a19', - stunTimer: 0, - chillTimer: 0, - freezeTimer: 0, - openWoundsTimer: 0, - openWoundsDmgPerFrame: 0, - knockbackOffset: 0, - pmhTimer: 0, - blindTimer: 0, - fleeTimer: 0, - slowTimer: 0, - slowPercent: 0, - } - - // Visuals - private floatingTexts: FloatingText[] = [] - private projectiles: ProjectileFx[] = [] - private particles: ParticleFx[] = [] - private autoBattleActive = false - private autoBattleInterval: number | null = null - - // Statistics - private totalDamageDealt = 0 - private combatStartTime = 0 - - constructor() { - this.initDom() - this.initEvents() - this.updateMonsterStatsFromTypeAndLevel() - this.startRenderLoop() - } - - private initDom() { - this.canvas = document.getElementById('view') as HTMLCanvasElement - this.ctx = this.canvas.getContext('2d')! - this.resizeCanvas() - window.addEventListener('resize', () => this.resizeCanvas()) - } - - private resizeCanvas() { - const wrapper = document.getElementById('viewport-wrapper')! - this.canvas.width = wrapper.clientWidth - this.canvas.height = wrapper.clientHeight - } - - private initEvents() { - // Player presets - const presetSelect = document.getElementById('char-preset') as HTMLSelectElement - presetSelect.addEventListener('change', () => this.applyPlayerPreset(presetSelect.value)) - - // Player inputs sync - const syncPlayerInputs = () => { - this.player.class = (document.getElementById('char-class') as HTMLSelectElement).value - this.player.level = Number((document.getElementById('char-level') as HTMLInputElement).value) || 1 - this.player.hp = Number((document.getElementById('char-hp') as HTMLInputElement).value) || 1 - this.player.maxHp = this.player.hp - this.player.mana = Number((document.getElementById('char-mana') as HTMLInputElement).value) || 0 - this.player.maxMana = this.player.mana - this.player.ar = Number((document.getElementById('char-ar') as HTMLInputElement).value) || 0 - this.player.dr = Number((document.getElementById('char-dr') as HTMLInputElement).value) || 0 - this.player.dmgMin = Number((document.getElementById('char-dmg-min') as HTMLInputElement).value) || 1 - this.player.dmgMax = Number((document.getElementById('char-dmg-max') as HTMLInputElement).value) || 1 - this.player.fire = Number((document.getElementById('char-fire') as HTMLInputElement).value) || 0 - this.player.cold = Number((document.getElementById('char-cold') as HTMLInputElement).value) || 0 - this.player.light = Number((document.getElementById('char-light') as HTMLInputElement).value) || 0 - this.player.poison = Number((document.getElementById('char-poison') as HTMLInputElement).value) || 0 - this.player.magic = Number((document.getElementById('char-magic') as HTMLInputElement).value) || 0 - this.player.block = Number((document.getElementById('char-block') as HTMLInputElement).value) || 0 - this.player.cs = Number((document.getElementById('char-cs') as HTMLInputElement)?.value) || 0 - this.player.ds = Number((document.getElementById('char-ds') as HTMLInputElement)?.value) || 0 - this.player.cb = Number((document.getElementById('char-cb') as HTMLInputElement)?.value) || 0 - this.player.ow = Number((document.getElementById('char-ow') as HTMLInputElement)?.value) || 0 - this.player.ll = Number((document.getElementById('char-ll') as HTMLInputElement)?.value) || 0 - this.player.ml = Number((document.getElementById('char-ml') as HTMLInputElement)?.value) || 0 - this.player.itd = (document.getElementById('char-itd') as HTMLInputElement)?.checked ?? false - this.player.defRed = Number((document.getElementById('char-def-red') as HTMLInputElement)?.value) || 0 - this.player.freezeBonus = Number((document.getElementById('char-freeze') as HTMLInputElement)?.value) || 0 - this.player.knockback = Number((document.getElementById('char-knockback') as HTMLInputElement)?.value) || 0 - this.player.difficulty = ((document.getElementById('game-difficulty') as HTMLSelectElement)?.value ?? 'normal') as GameDifficulty - this.player.flatDr = Number((document.getElementById('char-flat-dr') as HTMLInputElement).value) || 0 - this.player.flatMdr = Number((document.getElementById('char-flat-mdr') as HTMLInputElement).value) || 0 - this.player.pmh = (document.getElementById('char-pmh') as HTMLInputElement)?.checked ?? false - this.player.isMerc = (document.getElementById('char-is-merc') as HTMLInputElement)?.checked ?? false - this.player.conviction = Number((document.getElementById('char-conviction') as HTMLInputElement)?.value) || 0 - this.player.lowerRes = Number((document.getElementById('char-lower-res') as HTMLInputElement)?.value) || 0 - this.player.minusLightRes = Number((document.getElementById('char-minus-light-res') as HTMLInputElement)?.value) || 0 - this.player.minusFireRes = Number((document.getElementById('char-minus-fire-res') as HTMLInputElement)?.value) || 0 - this.player.minusColdRes = Number((document.getElementById('char-minus-cold-res') as HTMLInputElement)?.value) || 0 - this.player.minusPoisonRes = Number((document.getElementById('char-minus-poison-res') as HTMLInputElement)?.value) || 0 - this.player.slow = Number((document.getElementById('char-slow') as HTMLInputElement)?.value) || 0 - this.player.hbt = Number((document.getElementById('char-hbt') as HTMLInputElement)?.value) || 0 - this.player.hcmtf = Number((document.getElementById('char-hcmtf') as HTMLInputElement)?.value) || 0 - this.player.flatDmg = Number((document.getElementById('char-flat-dmg') as HTMLInputElement)?.value) || 0 - this.player.offWeaponEd = Number((document.getElementById('char-off-weapon-ed') as HTMLInputElement)?.value) || 0 - this.player.statBonus = Number((document.getElementById('char-stat-bonus') as HTMLInputElement)?.value) || 0 - } - - const playerInputIds = [ - 'char-class', 'char-level', 'char-hp', 'char-mana', 'char-ar', 'char-dr', - 'char-dmg-min', 'char-dmg-max', 'char-fire', 'char-cold', 'char-light', - 'char-poison', 'char-magic', 'char-block', 'char-cs', 'char-ds', 'char-cb', 'char-ow', - 'char-ll', 'char-ml', 'char-itd', 'char-def-red', 'char-freeze', 'char-knockback', - 'game-difficulty', 'char-flat-dr', 'char-flat-mdr', - 'char-pmh', 'char-is-merc', 'char-conviction', 'char-lower-res', - 'char-minus-light-res', 'char-minus-fire-res', 'char-minus-cold-res', 'char-minus-poison-res', - 'char-slow', 'char-hbt', 'char-hcmtf', 'char-flat-dmg', 'char-off-weapon-ed', 'char-stat-bonus', - ] - for (const id of playerInputIds) { - const el = document.getElementById(id) - el?.addEventListener('input', syncPlayerInputs) - el?.addEventListener('change', syncPlayerInputs) - } - - // Monster Type & Level - const monTypeSelect = document.getElementById('mon-type') as HTMLSelectElement - const monLevelInput = document.getElementById('mon-level') as HTMLInputElement - monTypeSelect.addEventListener('change', () => this.updateMonsterStatsFromTypeAndLevel()) - monLevelInput.addEventListener('input', () => this.updateMonsterStatsFromTypeAndLevel()) - - // Monster custom inputs - const syncMonsterInputs = () => { - this.monster.tier = ((document.getElementById('mon-tier') as HTMLSelectElement)?.value ?? 'normal') as TargetTier - this.monster.posture = ((document.getElementById('mon-posture') as HTMLSelectElement)?.value ?? 'stand') as 'stand' | 'run' - this.monster.drainEffectiveness = Number((document.getElementById('mon-drain-eff') as HTMLInputElement)?.value) ?? 100 - this.monster.size = ((document.getElementById('mon-size') as HTMLSelectElement)?.value ?? 'medium') as MonsterSize - this.monster.regenRate = Number((document.getElementById('mon-regen') as HTMLInputElement)?.value) || 0 - this.monster.isUber = (document.getElementById('mon-is-uber') as HTMLInputElement)?.checked ?? false - this.monster.hp = Number((document.getElementById('mon-hp') as HTMLInputElement).value) || 1 - this.monster.maxHp = Number((document.getElementById('mon-max-hp') as HTMLInputElement).value) || 1 - this.monster.dr = Number((document.getElementById('mon-dr') as HTMLInputElement).value) || 0 - this.monster.ar = Number((document.getElementById('mon-ar') as HTMLInputElement).value) || 0 - this.monster.dmgMin = Number((document.getElementById('mon-dmg-min') as HTMLInputElement).value) || 1 - this.monster.dmgMax = Number((document.getElementById('mon-dmg-max') as HTMLInputElement).value) || 1 - this.monster.block = Number((document.getElementById('mon-block') as HTMLInputElement).value) || 0 - this.monster.resPhysical = Number((document.getElementById('mon-res-phys') as HTMLInputElement).value) || 0 - this.monster.resFire = Number((document.getElementById('mon-res-fire') as HTMLInputElement).value) || 0 - this.monster.resCold = Number((document.getElementById('mon-res-cold') as HTMLInputElement).value) || 0 - this.monster.resLightning = Number((document.getElementById('mon-res-light') as HTMLInputElement).value) || 0 - this.monster.resPoison = Number((document.getElementById('mon-res-poison') as HTMLInputElement).value) || 0 - this.monster.resMagic = Number((document.getElementById('mon-res-magic') as HTMLInputElement).value) || 0 - this.monster.flatDr = Number((document.getElementById('mon-flat-dr') as HTMLInputElement).value) || 0 - this.monster.flatMdr = Number((document.getElementById('mon-flat-mdr') as HTMLInputElement).value) || 0 - this.updateMonsterBadge() - } - - const monsterInputIds = [ - 'mon-tier', 'mon-posture', 'mon-drain-eff', 'mon-size', 'mon-regen', 'mon-is-uber', - 'mon-hp', 'mon-max-hp', 'mon-dr', 'mon-ar', 'mon-dmg-min', 'mon-dmg-max', - 'mon-block', 'mon-res-phys', 'mon-res-fire', 'mon-res-cold', 'mon-res-light', - 'mon-res-poison', 'mon-res-magic', 'mon-flat-dr', 'mon-flat-mdr', - ] - for (const id of monsterInputIds) { - const el = document.getElementById(id) - el?.addEventListener('input', syncMonsterInputs) - el?.addEventListener('change', syncMonsterInputs) - } - - // Buttons - document.getElementById('btn-attack')?.addEventListener('click', () => this.executeMeleeAttack()) - document.getElementById('btn-fire')?.addEventListener('click', () => this.castFireSpell()) - document.getElementById('btn-cold')?.addEventListener('click', () => this.castColdSpell()) - document.getElementById('btn-light')?.addEventListener('click', () => this.castLightningSpell()) - document.getElementById('btn-magic')?.addEventListener('click', () => this.castMagicSpell()) - document.getElementById('btn-monster-attack')?.addEventListener('click', () => this.executeMonsterAttack()) - document.getElementById('btn-auto-battle')?.addEventListener('click', () => this.toggleAutoBattle()) - document.getElementById('btn-reset')?.addEventListener('click', () => this.resetCombat()) - document.getElementById('btn-clear-log')?.addEventListener('click', () => { - const container = document.getElementById('log-entries')! - container.innerHTML = '' - }) - - // Keyboard Shortcuts - window.addEventListener('keydown', (e) => { - if (e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement) return - if (e.key === ' ' || e.key === '1') { - e.preventDefault() - this.executeMeleeAttack() - } else if (e.key === '2') { - e.preventDefault() - this.castFireSpell() - } else if (e.key === '3') { - e.preventDefault() - this.castColdSpell() - } else if (e.key === '4') { - e.preventDefault() - this.castLightningSpell() - } else if (e.key === '5') { - e.preventDefault() - this.castMagicSpell() - } else if (e.key === 'a' || e.key === 'A') { - e.preventDefault() - this.toggleAutoBattle() - } else if (e.key === 'r' || e.key === 'R') { - e.preventDefault() - this.resetCombat() - } - }) - } - - // --- Presets & Data Sync -------------------------------------------------- - - private applyPlayerPreset(preset: string) { - if (preset === 'novice') { - this.setPlayerForm({ - class: 'Barbarian', level: 1, hp: 45, mana: 20, ar: 110, dr: 35, - dmgMin: 2, dmgMax: 6, fire: 0, cold: 0, light: 0, poison: 0, magic: 0, - block: 10, cs: 0, ds: 0, cb: 0, ow: 0, ll: 0, ml: 0, itd: false, defRed: 0, - freezeBonus: 0, knockback: 0, difficulty: 'normal', flatDr: 0, flatMdr: 0, - pmh: true, isMerc: false, conviction: 0, lowerRes: 0, - minusLightRes: 0, minusFireRes: 0, minusColdRes: 0, minusPoisonRes: 0, - slow: 0, hbt: 0, hcmtf: 0, flatDmg: 0, offWeaponEd: 0, statBonus: 0, - }) - } else if (preset === 'zealot') { - this.setPlayerForm({ - class: 'Paladin', level: 45, hp: 550, mana: 120, ar: 3800, dr: 2200, - dmgMin: 45, dmgMax: 110, fire: 20, cold: 15, light: 40, poison: 0, magic: 0, - block: 75, cs: 15, ds: 35, cb: 50, ow: 33, ll: 8, ml: 4, itd: true, defRed: 25, - freezeBonus: 1, knockback: 0, difficulty: 'nightmare', flatDr: 15, flatMdr: 10, - pmh: true, isMerc: false, conviction: 0, lowerRes: 0, - minusLightRes: 0, minusFireRes: 0, minusColdRes: 0, minusPoisonRes: 0, - slow: 50, hbt: 0, hcmtf: 0, flatDmg: 400, offWeaponEd: 300, statBonus: 150, - }) - } else if (preset === 'sorc_fire') { - this.setPlayerForm({ - class: 'Sorceress', level: 75, hp: 620, mana: 750, ar: 400, dr: 950, - dmgMin: 10, dmgMax: 25, fire: 1450, cold: 0, light: 0, poison: 0, magic: 0, - block: 20, cs: 0, ds: 0, cb: 0, ow: 0, ll: 0, ml: 0, itd: false, defRed: 0, - freezeBonus: 0, knockback: 0, difficulty: 'hell', flatDr: 5, flatMdr: 25, - pmh: true, isMerc: false, conviction: 85, lowerRes: 60, - minusLightRes: 0, minusFireRes: 20, minusColdRes: 0, minusPoisonRes: 0, - slow: 0, hbt: 0, hcmtf: 0, flatDmg: 0, offWeaponEd: 0, statBonus: 0, - }) - } else if (preset === 'sorc_orb') { - this.setPlayerForm({ - class: 'Sorceress', level: 80, hp: 700, mana: 800, ar: 500, dr: 1100, - dmgMin: 12, dmgMax: 30, fire: 350, cold: 680, light: 0, poison: 0, magic: 0, - block: 25, cs: 0, ds: 0, cb: 0, ow: 0, ll: 0, ml: 0, itd: false, defRed: 0, - freezeBonus: 3, knockback: 0, difficulty: 'hell', flatDr: 8, flatMdr: 30, - pmh: true, isMerc: false, conviction: 85, lowerRes: 0, - minusLightRes: 0, minusFireRes: 0, minusColdRes: 35, minusPoisonRes: 0, - slow: 0, hbt: 0, hcmtf: 0, flatDmg: 0, offWeaponEd: 0, statBonus: 0, - }) - } else if (preset === 'hell_slayer') { - this.setPlayerForm({ - class: 'Barbarian', level: 90, hp: 1650, mana: 260, ar: 11500, dr: 7500, - dmgMin: 180, dmgMax: 420, fire: 80, cold: 60, light: 120, poison: 100, magic: 0, - block: 75, cs: 28, ds: 65, cb: 75, ow: 50, ll: 15, ml: 8, itd: true, defRed: 50, - freezeBonus: 2, knockback: 25, difficulty: 'hell', flatDr: 35, flatMdr: 25, - pmh: true, isMerc: false, conviction: 0, lowerRes: 0, - minusLightRes: 0, minusFireRes: 0, minusColdRes: 0, minusPoisonRes: 0, - slow: 35, hbt: 2, hcmtf: 15, flatDmg: 120, offWeaponEd: 450, statBonus: 220, - }) - } - } - - private setPlayerForm(p: any) { - ;(document.getElementById('char-class') as HTMLSelectElement).value = p.class - ;(document.getElementById('char-level') as HTMLInputElement).value = String(p.level) - ;(document.getElementById('char-hp') as HTMLInputElement).value = String(p.hp) - ;(document.getElementById('char-mana') as HTMLInputElement).value = String(p.mana) - ;(document.getElementById('char-ar') as HTMLInputElement).value = String(p.ar) - ;(document.getElementById('char-dr') as HTMLInputElement).value = String(p.dr) - ;(document.getElementById('char-dmg-min') as HTMLInputElement).value = String(p.dmgMin) - ;(document.getElementById('char-dmg-max') as HTMLInputElement).value = String(p.dmgMax) - ;(document.getElementById('char-fire') as HTMLInputElement).value = String(p.fire) - ;(document.getElementById('char-cold') as HTMLInputElement).value = String(p.cold) - ;(document.getElementById('char-light') as HTMLInputElement).value = String(p.light) - ;(document.getElementById('char-poison') as HTMLInputElement).value = String(p.poison) - ;(document.getElementById('char-magic') as HTMLInputElement).value = String(p.magic) - ;(document.getElementById('char-block') as HTMLInputElement).value = String(p.block) - ;(document.getElementById('char-cs') as HTMLInputElement).value = String(p.cs ?? 0) - ;(document.getElementById('char-ds') as HTMLInputElement).value = String(p.ds ?? 0) - ;(document.getElementById('char-cb') as HTMLInputElement).value = String(p.cb ?? 0) - ;(document.getElementById('char-ow') as HTMLInputElement).value = String(p.ow ?? 0) - ;(document.getElementById('char-ll') as HTMLInputElement).value = String(p.ll ?? 0) - ;(document.getElementById('char-ml') as HTMLInputElement).value = String(p.ml ?? 0) - ;(document.getElementById('char-itd') as HTMLInputElement).checked = Boolean(p.itd) - ;(document.getElementById('char-def-red') as HTMLInputElement).value = String(p.defRed ?? 0) - ;(document.getElementById('char-freeze') as HTMLInputElement).value = String(p.freezeBonus ?? 0) - ;(document.getElementById('char-knockback') as HTMLInputElement).value = String(p.knockback ?? 0) - ;(document.getElementById('game-difficulty') as HTMLSelectElement).value = p.difficulty ?? 'normal' - ;(document.getElementById('char-flat-dr') as HTMLInputElement).value = String(p.flatDr) - ;(document.getElementById('char-flat-mdr') as HTMLInputElement).value = String(p.flatMdr) - const charPmh = document.getElementById('char-pmh') as HTMLInputElement | null - if (charPmh) charPmh.checked = Boolean(p.pmh ?? true) - const charMerc = document.getElementById('char-is-merc') as HTMLInputElement | null - if (charMerc) charMerc.checked = Boolean(p.isMerc ?? false) - const charConv = document.getElementById('char-conviction') as HTMLInputElement | null - if (charConv) charConv.value = String(p.conviction ?? 0) - const charLr = document.getElementById('char-lower-res') as HTMLInputElement | null - if (charLr) charLr.value = String(p.lowerRes ?? 0) - const charMinL = document.getElementById('char-minus-light-res') as HTMLInputElement | null - if (charMinL) charMinL.value = String(p.minusLightRes ?? 0) - const charMinF = document.getElementById('char-minus-fire-res') as HTMLInputElement | null - if (charMinF) charMinF.value = String(p.minusFireRes ?? 0) - const charMinC = document.getElementById('char-minus-cold-res') as HTMLInputElement | null - if (charMinC) charMinC.value = String(p.minusColdRes ?? 0) - const charMinP = document.getElementById('char-minus-poison-res') as HTMLInputElement | null - if (charMinP) charMinP.value = String(p.minusPoisonRes ?? 0) - const charSlow = document.getElementById('char-slow') as HTMLInputElement | null - if (charSlow) charSlow.value = String(p.slow ?? 0) - const charHbt = document.getElementById('char-hbt') as HTMLInputElement | null - if (charHbt) charHbt.value = String(p.hbt ?? 0) - const charHcmtf = document.getElementById('char-hcmtf') as HTMLInputElement | null - if (charHcmtf) charHcmtf.value = String(p.hcmtf ?? 0) - const charFlatD = document.getElementById('char-flat-dmg') as HTMLInputElement | null - if (charFlatD) charFlatD.value = String(p.flatDmg ?? 0) - const charOffEd = document.getElementById('char-off-weapon-ed') as HTMLInputElement | null - if (charOffEd) charOffEd.value = String(p.offWeaponEd ?? 0) - const charStatB = document.getElementById('char-stat-bonus') as HTMLInputElement | null - if (charStatB) charStatB.value = String(p.statBonus ?? 0) - - Object.assign(this.player, p) - this.player.maxHp = p.hp - this.player.maxMana = p.mana - } - - private updateMonsterStatsFromTypeAndLevel() { - const typeKey = (document.getElementById('mon-type') as HTMLSelectElement).value - const level = Number((document.getElementById('mon-level') as HTMLInputElement).value) || 1 - const t = MONSTER_TEMPLATES[typeKey] ?? MONSTER_TEMPLATES['fallen1']! - - const scale = computeMonsterScale(level) - const hp = Math.max(1, Math.round(((t.baseMinHp + t.baseMaxHp) / 2) * (scale.hp / 100))) - const dr = Math.max(1, Math.round(t.baseAc * (scale.ac / 100))) - const ar = Math.max(1, Math.round(t.baseTh * (scale.th / 100))) - const dmgMin = Math.max(1, Math.round(t.baseMinD * (scale.dm / 100))) - const dmgMax = Math.max(dmgMin + 1, Math.round(t.baseMaxD * (scale.dm / 100))) - - this.monster.typeKey = t.id - this.monster.name = t.name - this.monster.category = t.category - this.monster.level = level - this.monster.hp = hp - this.monster.maxHp = hp - this.monster.dr = dr - this.monster.ar = ar - this.monster.dmgMin = dmgMin - this.monster.dmgMax = dmgMax - this.monster.block = t.blockPercent - this.monster.resPhysical = t.resPhysical - this.monster.resFire = t.resFire - this.monster.resCold = t.resCold - this.monster.resLightning = t.resLightning - this.monster.resPoison = t.resPoison - this.monster.resMagic = t.resMagic - this.monster.color = t.color - this.monster.size = t.size ?? 'medium' - this.monster.regenRate = t.regenRate ?? 0 - - ;(document.getElementById('mon-hp') as HTMLInputElement).value = String(hp) - ;(document.getElementById('mon-max-hp') as HTMLInputElement).value = String(hp) - ;(document.getElementById('mon-dr') as HTMLInputElement).value = String(dr) - ;(document.getElementById('mon-ar') as HTMLInputElement).value = String(ar) - ;(document.getElementById('mon-dmg-min') as HTMLInputElement).value = String(dmgMin) - ;(document.getElementById('mon-dmg-max') as HTMLInputElement).value = String(dmgMax) - ;(document.getElementById('mon-block') as HTMLInputElement).value = String(t.blockPercent) - ;(document.getElementById('mon-res-phys') as HTMLInputElement).value = String(t.resPhysical) - ;(document.getElementById('mon-res-fire') as HTMLInputElement).value = String(t.resFire) - ;(document.getElementById('mon-res-cold') as HTMLInputElement).value = String(t.resCold) - ;(document.getElementById('mon-res-light') as HTMLInputElement).value = String(t.resLightning) - ;(document.getElementById('mon-res-poison') as HTMLInputElement).value = String(t.resPoison) - ;(document.getElementById('mon-res-magic') as HTMLInputElement).value = String(t.resMagic) - - const monSizeEl = document.getElementById('mon-size') as HTMLSelectElement | null - if (monSizeEl) monSizeEl.value = this.monster.size - const monRegenEl = document.getElementById('mon-regen') as HTMLInputElement | null - if (monRegenEl) monRegenEl.value = String(this.monster.regenRate) - - this.monster.tier = (t.tier ?? 'normal') as TargetTier - this.monster.drainEffectiveness = t.drainEffectiveness ?? 100 - const monTierEl = document.getElementById('mon-tier') as HTMLSelectElement | null - if (monTierEl) monTierEl.value = this.monster.tier - const monDrainEl = document.getElementById('mon-drain-eff') as HTMLInputElement | null - if (monDrainEl) monDrainEl.value = String(this.monster.drainEffectiveness) - - this.updateMonsterBadge() - } - - private updateMonsterBadge() { - const badge = document.getElementById('monster-badge')! - const immunes: string[] = [] - if (this.monster.resPhysical >= 100) immunes.push('物免') - if (this.monster.resFire >= 100) immunes.push('火免') - if (this.monster.resCold >= 100) immunes.push('冰免') - if (this.monster.resLightning >= 100) immunes.push('电免') - if (this.monster.resPoison >= 100) immunes.push('毒免') - if (this.monster.resMagic >= 100) immunes.push('魔免') - - if (immunes.length > 0) { - badge.className = 'tag immune' - badge.textContent = immunes.join('·') - } else { - badge.className = 'tag res' - badge.textContent = this.monster.category - } - } - - // --- Combat Action Triggers ----------------------------------------------- - - public executeMeleeAttack() { - if (this.player.hp <= 0) return - this.player.action = 'attack' - this.player.actionTimer = 15 - - let pMin = this.player.dmgMin - let pMax = this.player.dmgMax - if (this.player.flatDmg > 0 || this.player.offWeaponEd > 0 || this.player.statBonus > 0) { - const weaponCalc = calculatePhysicalWeaponDamage({ - weaponBaseMin: pMin, - weaponBaseMax: pMax, - flatDamage: this.player.flatDmg, - offWeaponEdPercent: this.player.offWeaponEd, - statBonusPercent: this.player.statBonus, - }) - pMin = weaponCalc.finalMinDamage - pMax = weaponCalc.finalMaxDamage - } - - const result = calculateAttack( - this.player.level, - this.player.ar, - { - physicalMin: pMin, - physicalMax: pMax, - fire: this.player.fire, - cold: this.player.cold, - lightning: this.player.light, - poison: this.player.poison, - magic: this.player.magic, - }, - { - level: this.monster.level, - defense: this.monster.dr, - blockPercent: this.monster.block, - resPhysical: this.monster.resPhysical, - resFire: this.monster.resFire, - resCold: this.monster.resCold, - resLightning: this.monster.resLightning, - resPoison: this.monster.resPoison, - resMagic: this.monster.resMagic, - flatDamageReduction: this.monster.flatDr, - flatMagicReduction: this.monster.flatMdr, - currentHp: this.monster.hp, - maxHp: this.monster.maxHp, - tier: this.monster.tier, - isRunning: this.monster.posture === 'run', - drainEffectiveness: this.monster.drainEffectiveness, - size: this.monster.size, - regenRate: this.monster.regenRate, - isUber: this.monster.isUber, - }, - { - criticalStrikePercent: this.player.cs, - deadlyStrikePercent: this.player.ds, - crushingBlowPercent: this.player.cb, - openWoundsPercent: this.player.ow, - lifeLeechPercent: this.player.ll, - manaLeechPercent: this.player.ml, - ignoreTargetDefense: this.player.itd, - targetDefenseReductionPercent: this.player.defRed, - freezeBonus: this.player.freezeBonus, - knockbackPercent: this.player.knockback, - difficulty: this.player.difficulty, - isSpell: false, - isRanged: false, - // PvE Depth mechanics - convictionPenalty: this.player.conviction, - lowerResPenalty: this.player.lowerRes, - itemMinusFireRes: this.player.minusFireRes, - itemMinusLightRes: this.player.minusLightRes, - itemMinusColdRes: this.player.minusColdRes, - itemMinusPoisonRes: this.player.minusPoisonRes, - slowPercent: this.player.slow, - hitBlindsTargetBonus: this.player.hbt, - hitCausesFleePercent: this.player.hcmtf, - preventMonsterHeal: this.player.pmh, - attackerType: this.player.isMerc ? 'mercenary' : 'player', - }, - ) - - this.applyCombatResult(result, 'melee') - } - - public castFireSpell() { - if (this.player.hp <= 0) return - this.player.action = 'cast' - this.player.actionTimer = 16 - - const spellDmg = Math.round((this.player.fire > 0 ? this.player.fire * 1.5 : 45) + Math.random() * 20) - const pX = this.canvas.width * 0.35 - const pY = this.canvas.height * 0.52 - const mX = this.canvas.width * 0.65 - const mY = this.canvas.height * 0.52 - - this.projectiles.push({ - x: pX, y: pY - 20, targetX: mX, targetY: mY - 20, - type: 'fire', progress: 0, speed: 0.08, - onHit: () => { - const result = calculateAttack( - this.player.level, 0, - { physicalMin: 0, physicalMax: 0, fire: spellDmg }, - { - level: this.monster.level, - defense: this.monster.dr, - blockPercent: this.monster.block, - resFire: this.monster.resFire, - flatMagicReduction: this.monster.flatMdr, - currentHp: this.monster.hp, - maxHp: this.monster.maxHp, - tier: this.monster.tier, - isRunning: this.monster.posture === 'run', - size: this.monster.size, - isUber: this.monster.isUber, - }, - { - isSpell: true, - difficulty: this.player.difficulty, - convictionPenalty: this.player.conviction, - lowerResPenalty: this.player.lowerRes, - itemMinusFireRes: this.player.minusFireRes, - }, - ) - this.applyCombatResult(result, 'fire') - this.spawnExplosion(mX, mY - 20, '#ff5722', 20) - }, - }) - } - - public castColdSpell() { - if (this.player.hp <= 0) return - this.player.action = 'cast' - this.player.actionTimer = 16 - - const spellDmg = Math.round((this.player.cold > 0 ? this.player.cold * 1.6 : 38) + Math.random() * 15) - const pX = this.canvas.width * 0.35 - const pY = this.canvas.height * 0.52 - const mX = this.canvas.width * 0.65 - const mY = this.canvas.height * 0.52 - - this.projectiles.push({ - x: pX, y: pY - 20, targetX: mX, targetY: mY - 20, - type: 'cold', progress: 0, speed: 0.07, - onHit: () => { - const result = calculateAttack( - this.player.level, 0, - { physicalMin: 0, physicalMax: 0, cold: spellDmg }, - { - level: this.monster.level, - defense: this.monster.dr, - blockPercent: this.monster.block, - resCold: this.monster.resCold, - flatMagicReduction: this.monster.flatMdr, - currentHp: this.monster.hp, - maxHp: this.monster.maxHp, - tier: this.monster.tier, - isRunning: this.monster.posture === 'run', - size: this.monster.size, - isUber: this.monster.isUber, - }, - { - isSpell: true, - difficulty: this.player.difficulty, - convictionPenalty: this.player.conviction, - lowerResPenalty: this.player.lowerRes, - itemMinusColdRes: this.player.minusColdRes, - }, - ) - this.applyCombatResult(result, 'cold') - if (result.finalColdDamage > 0) { - this.monster.chillTimer = 60 - } - this.spawnExplosion(mX, mY - 20, '#29b6f6', 24) - }, - }) - } - - public castLightningSpell() { - if (this.player.hp <= 0) return - this.player.action = 'cast' - this.player.actionTimer = 16 - - // Lightning has iconic wide damage range [1 .. High] - const spellDmg = Math.round(1 + Math.random() * (this.player.light > 0 ? this.player.light * 2.5 : 90)) - const mX = this.canvas.width * 0.65 - const mY = this.canvas.height * 0.52 - - const result = calculateAttack( - this.player.level, 0, - { physicalMin: 0, physicalMax: 0, lightning: spellDmg }, - { - level: this.monster.level, - defense: this.monster.dr, - blockPercent: this.monster.block, - resLightning: this.monster.resLightning, - flatMagicReduction: this.monster.flatMdr, - currentHp: this.monster.hp, - maxHp: this.monster.maxHp, - tier: this.monster.tier, - isRunning: this.monster.posture === 'run', - size: this.monster.size, - isUber: this.monster.isUber, - }, - { - isSpell: true, - difficulty: this.player.difficulty, - convictionPenalty: this.player.conviction, - lowerResPenalty: this.player.lowerRes, - itemMinusLightRes: this.player.minusLightRes, - }, - ) - this.applyCombatResult(result, 'light') - this.spawnExplosion(mX, mY - 20, '#ffee58', 25) - } - - public castMagicSpell() { - if (this.player.hp <= 0) return - this.player.action = 'cast' - this.player.actionTimer = 16 - - const spellDmg = Math.round((this.player.magic > 0 ? this.player.magic * 1.8 : 55) + Math.random() * 25) - const pX = this.canvas.width * 0.35 - const pY = this.canvas.height * 0.52 - const mX = this.canvas.width * 0.65 - const mY = this.canvas.height * 0.52 - - this.projectiles.push({ - x: pX, y: pY - 20, targetX: mX, targetY: mY - 20, - type: 'magic', progress: 0, speed: 0.09, - onHit: () => { - const result = calculateAttack( - this.player.level, 0, - { physicalMin: 0, physicalMax: 0, magic: spellDmg }, - { - level: this.monster.level, - defense: this.monster.dr, - blockPercent: this.monster.block, - resMagic: this.monster.resMagic, - flatMagicReduction: this.monster.flatMdr, - currentHp: this.monster.hp, - maxHp: this.monster.maxHp, - tier: this.monster.tier, - isRunning: this.monster.posture === 'run', - size: this.monster.size, - isUber: this.monster.isUber, - }, - { isSpell: true, difficulty: this.player.difficulty }, - ) - this.applyCombatResult(result, 'magic') - this.spawnExplosion(mX, mY - 20, '#ab47bc', 20) - }, - }) - } - - public executeMonsterAttack() { - if (this.monster.hp <= 0 || this.monster.freezeTimer > 0 || this.monster.stunTimer > 0 || this.monster.fleeTimer > 0) return - this.monster.action = 'attack' - const slowFactor = 1 + (this.monster.slowPercent / 100) - this.monster.actionTimer = Math.round(18 * slowFactor) - - // Monster strikes Player - const result = calculateAttack( - this.monster.level, - this.monster.ar, - { - physicalMin: this.monster.dmgMin, - physicalMax: this.monster.dmgMax, - }, - { - level: this.player.level, - defense: this.player.dr, - blockPercent: this.player.block, - flatDamageReduction: this.player.flatDr, - flatMagicReduction: this.player.flatMdr, - currentHp: this.player.hp, - maxHp: this.player.maxHp, - tier: 'player', - }, - { - difficulty: this.player.difficulty, - }, - ) - - const pX = this.canvas.width * 0.35 - const pY = this.canvas.height * 0.52 - - if (!result.isHit) { - this.addFloatingText(pX, pY - 40, 'MISS', '#90a4ae') - } else if (result.isBlocked) { - this.addFloatingText(pX, pY - 40, '🛡️ BLOCKED', '#64b5f6') - } else { - this.player.hp = Math.max(0, Math.round((this.player.hp - result.totalDamage) * 10) / 10) - this.player.hitFlash = 6 - ;(document.getElementById('char-hp') as HTMLInputElement).value = String(this.player.hp) - this.addFloatingText(pX, pY - 40, `-${result.totalDamage}`, '#e53935', 18) - if (this.player.hp <= 0) { - this.player.action = 'dead' - this.addFloatingText(pX, pY - 60, 'YOU DIED', '#d32f2f', 22) - } - } - - this.appendLog(result.log, result.isHit ? (result.isBlocked ? 'blocked' : 'hit') : 'miss') - } - - private applyCombatResult(result: CombatCalculationResult, kind: string) { - const mX = this.canvas.width * 0.65 - const mY = this.canvas.height * 0.52 - const pX = this.canvas.width * 0.35 - const pY = this.canvas.height * 0.52 - - if (!result.isHit) { - this.addFloatingText(mX, mY - 40, 'MISS', '#90a4ae') - this.appendLog(result.log, 'miss') - return - } - - if (result.isBlocked) { - this.addFloatingText(mX, mY - 40, '🛡️ BLOCKED', '#64b5f6') - this.appendLog(result.log, 'blocked') - return - } - - // Apply Damage - this.monster.hp = Math.max(0, Math.round((this.monster.hp - result.totalDamage) * 10) / 10) - this.monster.hitFlash = 6 - this.totalDamageDealt += result.totalDamage - ;(document.getElementById('mon-hp') as HTMLInputElement).value = String(this.monster.hp) - - // Damage floating text - let dmgColor = '#ffffff' - if (kind === 'fire' || result.finalFireDamage > 0) dmgColor = '#ff7043' - if (kind === 'cold' || result.finalColdDamage > 0) dmgColor = '#4fc3f7' - if (kind === 'light' || result.finalLightningDamage > 0) dmgColor = '#ffee58' - if (kind === 'magic' || result.finalMagicDamage > 0) dmgColor = '#ba68c8' - - this.addFloatingText(mX, mY - 40, `-${result.totalDamage}`, dmgColor, result.isCritical ? 20 : 16) - - // Critical & Deadly Strike distinction - if (result.isCritical) { - if (result.criticalTrigger === 'critical_strike') { - this.addFloatingText(mX, mY - 60, '⚡ CRITICAL STRIKE! (2x)', '#ffd54f', 14) - } else if (result.criticalTrigger === 'deadly_strike') { - this.addFloatingText(mX, mY - 60, '⚔️ DEADLY STRIKE! (2x)', '#ffb74d', 14) - } else { - this.addFloatingText(mX, mY - 60, '⚡ CRITICAL!', '#ffd54f', 14) - } - } - - // Crushing Blow - if (result.isCrushingBlow) { - this.addFloatingText(mX, mY - 75, `💥 CRUSHING BLOW! (-${result.crushingBlowDamage})`, '#ef5350', 14) - this.spawnExplosion(mX, mY - 20, '#ff1744', 24) - } - - // Open Wounds - if (result.isOpenWounds && result.openWoundsResult) { - this.monster.openWoundsTimer = 200 - this.monster.openWoundsDmgPerFrame = result.openWoundsResult.damagePerFrame - this.addFloatingText(mX, mY - 90, '🩸 OPEN WOUNDS (8s)', '#d32f2f', 13) - this.spawnExplosion(mX, mY - 20, '#b71c1c', 16) - } - - // Life & Mana Leech - if (result.lifeLeeched > 0) { - this.player.hp = Math.min(this.player.maxHp, this.player.hp + result.lifeLeeched) - ;(document.getElementById('char-hp') as HTMLInputElement).value = String(Math.round(this.player.hp)) - this.addFloatingText(pX, pY - 45, `+${result.lifeLeeched} HP`, '#4caf50', 13) - } - if (result.manaLeeched > 0) { - this.player.mana = Math.min(this.player.maxMana, this.player.mana + result.manaLeeched) - ;(document.getElementById('char-mana') as HTMLInputElement).value = String(Math.round(this.player.mana)) - this.addFloatingText(pX, pY - 60, `+${result.manaLeeched} MP`, '#42a5f5', 13) - } - - // Freeze Target & Chilled - if (result.freezeResult?.willFreeze) { - this.monster.freezeTimer = result.freezeResult.durationFrames - this.addFloatingText(mX, mY - 105, `❄️ FROZEN (${result.freezeResult.durationSeconds}s)`, '#80deea', 14) - this.spawnExplosion(mX, mY - 20, '#80deea', 18) - } else if (result.freezeResult?.isChilledOnly) { - this.monster.chillTimer = result.freezeResult.durationFrames - this.addFloatingText(mX, mY - 105, `❄️ CHILLED (${result.freezeResult.durationSeconds}s)`, '#81d4fa', 13) - } - - // Knockback - if (result.knockbackTriggered) { - this.monster.knockbackOffset = 25 - this.addFloatingText(mX, mY - 30, '💨 KNOCKBACK', '#b0bec5', 12) - } - - // Hit recovery - if (result.hitRecoveryTriggered) { - this.monster.stunTimer = 25 - this.addFloatingText(mX, mY - 120, '💫 FHR STUN', '#ffca28', 12) - } - - // Resistance Break Floating Text - if (result.resistanceBreakResults) { - for (const [elem, brk] of Object.entries(result.resistanceBreakResults)) { - if (brk.immunityBroken) { - const name = elem === 'physical' ? '物' : elem === 'fire' ? '火' : elem === 'lightning' ? '电' : elem === 'cold' ? '冰' : '毒' - this.addFloatingText(mX, mY - 135, `⚡ 破${name}免成功! (${brk.finalRes}%)`, '#ffd54f', 13) - } - } - } - - // Prevent Monster Heal (PMH) - if (result.pmhResult) { - if (result.pmhResult.applied) { - this.monster.pmhTimer = result.pmhResult.durationFrames - this.addFloatingText(mX, mY - 150, '🚫 PMH 自愈阻断 (80分)', '#81c784', 13) - } else if (this.player.pmh) { - this.addFloatingText(mX, mY - 150, `🚫 PMH无效: ${result.pmhResult.reason}`, '#ef5350', 11) - } - } - - // Slows Target - if (result.slowResult && result.slowResult.effectiveSlowPercent > 0) { - this.monster.slowTimer = 150 - this.monster.slowPercent = result.slowResult.effectiveSlowPercent - this.addFloatingText(mX, mY - 120, `🐌 目标减速 -${result.slowResult.effectiveSlowPercent}%`, '#ffb74d', 13) - } - - // Hit Blinds Target - if (result.blindResult?.triggered) { - this.monster.blindTimer = result.blindResult.durationFrames - this.addFloatingText(mX, mY - 165, `👁️ 目标致盲 (${result.blindResult.durationSeconds}s)`, '#ba68c8', 13) - this.spawnExplosion(mX, mY - 20, '#ab47bc', 14) - } - - // Hit Causes Monster to Flee - if (result.fleeResult?.triggered) { - this.monster.fleeTimer = result.fleeResult.durationFrames - this.addFloatingText(mX, mY - 180, '😱 恐吓逃跑 (5s)', '#ff8a65', 13) - this.spawnExplosion(mX, mY - 20, '#ff7043', 14) - } - - // Death Check - if (this.monster.hp <= 0) { - this.monster.action = 'dead' - this.addFloatingText(mX, mY - 60, '💀 SLAIN', '#e53935', 22) - const earnedXp = Math.round(50 * (this.monster.level / Math.max(1, this.player.level))) - this.addFloatingText(mX, mY - 80, `+${earnedXp} EXP`, '#81c784', 13) - } - - this.appendLog(result.log, result.isCritical ? 'crit' : 'hit') - this.updateDpsMeter() - } - - private toggleAutoBattle() { - this.autoBattleActive = !this.autoBattleActive - const btnLabel = document.getElementById('auto-battle-label')! - if (this.autoBattleActive) { - btnLabel.textContent = '⏸️ 停止对决' - this.combatStartTime = performance.now() - this.totalDamageDealt = 0 - this.autoBattleInterval = window.setInterval(() => { - if (this.monster.hp <= 0 || this.player.hp <= 0) { - this.toggleAutoBattle() - return - } - this.executeMeleeAttack() - if (Math.random() < 0.6) { - this.executeMonsterAttack() - } - }, 400) - } else { - btnLabel.textContent = '🔄 开启自动对决' - if (this.autoBattleInterval) { - clearInterval(this.autoBattleInterval) - this.autoBattleInterval = null - } - } - } - - public resetCombat() { - this.player.hp = this.player.maxHp - this.player.mana = this.player.maxMana - this.player.action = 'idle' - ;(document.getElementById('char-hp') as HTMLInputElement).value = String(this.player.hp) - ;(document.getElementById('char-mana') as HTMLInputElement).value = String(this.player.mana) - - this.monster.hp = this.monster.maxHp - this.monster.action = 'idle' - this.monster.stunTimer = 0 - this.monster.chillTimer = 0 - this.monster.freezeTimer = 0 - this.monster.openWoundsTimer = 0 - this.monster.openWoundsDmgPerFrame = 0 - this.monster.knockbackOffset = 0 - this.monster.pmhTimer = 0 - this.monster.blindTimer = 0 - this.monster.fleeTimer = 0 - this.monster.slowTimer = 0 - this.monster.slowPercent = 0 - ;(document.getElementById('mon-hp') as HTMLInputElement).value = String(this.monster.hp) - - this.floatingTexts = [] - this.projectiles = [] - this.totalDamageDealt = 0 - this.combatStartTime = 0 - this.updateDpsMeter() - - this.appendLog(['[重置]: 玩家与怪物恢复满生命值,所有受击硬直、流血、冻结与击退状态已清空。'], 'hit') - } - - private updateDpsMeter() { - const dpsEl = document.getElementById('stat-dps')! - if (this.combatStartTime > 0) { - const elapsed = (performance.now() - this.combatStartTime) / 1000 - if (elapsed > 0.5) { - const dps = this.totalDamageDealt / elapsed - dpsEl.textContent = `DPS: ${dps.toFixed(1)} (累计: ${this.totalDamageDealt.toFixed(0)})` - return - } - } - dpsEl.textContent = `DPS: 0.0 (累计: ${this.totalDamageDealt.toFixed(0)})` - } - - private appendLog(lines: readonly string[], style: 'hit' | 'miss' | 'blocked' | 'crit') { - const container = document.getElementById('log-entries')! - const item = document.createElement('div') - item.className = `log-item ${style}` - item.innerHTML = lines.map(line => `
${escapeHtml(line)}
`).join('') - container.insertBefore(item, container.firstChild) - // Keep max 80 entries - while (container.children.length > 80) { - container.removeChild(container.lastChild!) - } - } - - private addFloatingText(x: number, y: number, text: string, color: string, size = 15) { - this.floatingTexts.push({ - x: x + (Math.random() - 0.5) * 30, - y: y + (Math.random() - 0.5) * 15, - text, color, size, alpha: 1.0, vy: -1.2, life: 50, - }) - } - - private spawnExplosion(x: number, y: number, color: string, count = 16) { - for (let i = 0; i < count; i++) { - const angle = Math.random() * Math.PI * 2 - const speed = 1.5 + Math.random() * 3.5 - this.particles.push({ - x, y, - vx: Math.cos(angle) * speed, - vy: Math.sin(angle) * speed, - color, - size: 2 + Math.random() * 3, - life: 25, - maxLife: 25, - }) - } - } - - // --- Render Loop ---------------------------------------------------------- - - private startRenderLoop() { - const loop = () => { - this.update() - this.render() - requestAnimationFrame(loop) - } - requestAnimationFrame(loop) - } - - private update() { - // Player timers - if (this.player.actionTimer > 0) { - this.player.actionTimer -= 1 - if (this.player.actionTimer === 0 && this.player.action !== 'dead') { - this.player.action = 'idle' - } - } - if (this.player.hitFlash > 0) this.player.hitFlash -= 1 - - // Monster timers - if (this.monster.actionTimer > 0) { - this.monster.actionTimer -= 1 - if (this.monster.actionTimer === 0 && this.monster.action !== 'dead') { - this.monster.action = 'idle' - } - } - if (this.monster.hitFlash > 0) this.monster.hitFlash -= 1 - if (this.monster.stunTimer > 0) this.monster.stunTimer -= 1 - if (this.monster.chillTimer > 0) this.monster.chillTimer -= 1 - if (this.monster.freezeTimer > 0) this.monster.freezeTimer -= 1 - if (this.monster.knockbackOffset > 0) { - this.monster.knockbackOffset = Math.max(0, this.monster.knockbackOffset - 1.5) - } - if (this.monster.pmhTimer > 0) this.monster.pmhTimer -= 1 - if (this.monster.blindTimer > 0) this.monster.blindTimer -= 1 - if (this.monster.fleeTimer > 0) this.monster.fleeTimer -= 1 - if (this.monster.slowTimer > 0) { - this.monster.slowTimer -= 1 - if (this.monster.slowTimer === 0) this.monster.slowPercent = 0 - } - - // Natural Monster Regeneration (blocked by OW or PMH) - if (this.monster.hp > 0 && this.monster.hp < this.monster.maxHp && this.monster.regenRate > 0) { - const regen = calculateMonsterRegeneration( - this.monster.maxHp, - this.monster.regenRate, - this.monster.openWoundsTimer > 0, - this.monster.pmhTimer > 0, - ) - if (regen.effectiveRegenPerFrame > 0) { - this.monster.hp = Math.min(this.monster.maxHp, Math.round((this.monster.hp + regen.effectiveRegenPerFrame) * 100) / 100) - const hpInput = document.getElementById('mon-hp') as HTMLInputElement | null - if (hpInput) hpInput.value = String(Math.round(this.monster.hp)) - } - } - - // Open Wounds bleeding tick - if (this.monster.openWoundsTimer > 0 && this.monster.hp > 0) { - this.monster.openWoundsTimer -= 1 - const tickDmg = this.monster.openWoundsDmgPerFrame - this.monster.hp = Math.max(0, Math.round((this.monster.hp - tickDmg) * 10) / 10) - const hpInput = document.getElementById('mon-hp') as HTMLInputElement | null - if (hpInput) hpInput.value = String(Math.round(this.monster.hp)) - - const mX = this.canvas.width * 0.65 - const mY = this.canvas.height * 0.52 - - // Visual tick every 25 frames (1 second in 25 fps game clock) - if (this.monster.openWoundsTimer % 25 === 0) { - const secDmg = Math.round(tickDmg * 25 * 10) / 10 - this.addFloatingText(mX + (Math.random() - 0.5) * 20, mY - 35, `-${secDmg} 🩸`, '#d32f2f', 12) - this.spawnExplosion(mX, mY - 20, '#b71c1c', 6) - } - - if (this.monster.hp <= 0 && this.monster.action !== 'dead') { - this.monster.action = 'dead' - this.addFloatingText(mX, mY - 60, '💀 BLED TO DEATH', '#b71c1c', 22) - } - } - - // Update Floating Text - for (let i = this.floatingTexts.length - 1; i >= 0; i--) { - const ft = this.floatingTexts[i]! - ft.y += ft.vy - ft.life -= 1 - ft.alpha = Math.max(0, ft.life / 50) - if (ft.life <= 0) this.floatingTexts.splice(i, 1) - } - - // Update Projectiles - for (let i = this.projectiles.length - 1; i >= 0; i--) { - const p = this.projectiles[i]! - p.progress += p.speed - p.x = p.x + (p.targetX - p.x) * p.speed * 2 - p.y = p.y + (p.targetY - p.y) * p.speed * 2 - if (p.progress >= 1 || Math.hypot(p.targetX - p.x, p.targetY - p.y) < 15) { - p.onHit() - this.projectiles.splice(i, 1) - } - } - - // Update Particles - for (let i = this.particles.length - 1; i >= 0; i--) { - const pt = this.particles[i]! - pt.x += pt.vx - pt.y += pt.vy - pt.life -= 1 - if (pt.life <= 0) this.particles.splice(i, 1) - } - } - - private render() { - const ctx = this.ctx - const w = this.canvas.width - const h = this.canvas.height - - // Background - ctx.fillStyle = '#0a0806' - ctx.fillRect(0, 0, w, h) - - // Draw Isometric Duel Arena - this.renderArenaGround(ctx, w, h) - - // Positions - const pX = w * 0.35 - const pY = h * 0.52 - const mX = w * 0.65 - const mY = h * 0.52 - - // Shadows - this.renderShadow(ctx, pX, pY + 8, 28, 12) - this.renderShadow(ctx, mX + this.monster.knockbackOffset, mY + 8, 32, 14) - - // Render Characters - this.renderPlayerAvatar(ctx, pX, pY) - this.renderMonsterAvatar(ctx, mX, mY) - - // Render Projectiles & Particles - this.renderProjectiles(ctx) - this.renderParticles(ctx) - - // Overhead Status Bars - this.renderOverheadPlayerBar(ctx, pX, pY - 80) - this.renderOverheadMonsterBar(ctx, mX + this.monster.knockbackOffset, mY - 85) - - // Floating combat text - this.renderFloatingTexts(ctx) - } - - private renderArenaGround(ctx: CanvasRenderingContext2D, w: number, h: number) { - const cX = w * 0.5 - const cY = h * 0.52 - - // Large outer rune diamond - ctx.save() - ctx.beginPath() - ctx.moveTo(cX, cY - 140) - ctx.lineTo(cX + 280, cY) - ctx.lineTo(cX, cY + 140) - ctx.lineTo(cX - 280, cY) - ctx.closePath() - ctx.fillStyle = '#14100c' - ctx.fill() - ctx.lineWidth = 3 - ctx.strokeStyle = '#3d2f20' - ctx.stroke() - - // Inner patterned ring - ctx.beginPath() - ctx.ellipse(cX, cY, 200, 95, 0, 0, Math.PI * 2) - ctx.fillStyle = '#1a140f' - ctx.fill() - ctx.lineWidth = 1.5 - ctx.strokeStyle = '#5a432b' - ctx.stroke() - - // Runic circle lines - ctx.beginPath() - ctx.ellipse(cX, cY, 130, 60, 0, 0, Math.PI * 2) - ctx.lineWidth = 1 - ctx.strokeStyle = '#8c693a' - ctx.stroke() - - // Isometric grid lines inside arena - ctx.strokeStyle = '#271f16' - ctx.lineWidth = 1 - for (let x = -4; x <= 4; x++) { - ctx.beginPath() - ctx.moveTo(cX + x * 40, cY - 70 + Math.abs(x) * 15) - ctx.lineTo(cX + x * 40, cY + 70 - Math.abs(x) * 15) - ctx.stroke() - } - - // Torch poles on corners - this.renderTorch(ctx, cX - 250, cY) - this.renderTorch(ctx, cX + 250, cY) - this.renderTorch(ctx, cX, cY - 120) - this.renderTorch(ctx, cX, cY + 120) - - ctx.restore() - } - - private renderTorch(ctx: CanvasRenderingContext2D, x: number, y: number) { - ctx.fillStyle = '#3a2b1c' - ctx.fillRect(x - 3, y - 25, 6, 25) - ctx.fillStyle = '#1c150e' - ctx.fillRect(x - 6, y - 28, 12, 4) - - // Torch Fire Animation - const flick = Math.sin(Date.now() * 0.01 + x) * 2 - ctx.beginPath() - ctx.arc(x, y - 32 + flick, 6, 0, Math.PI * 2) - ctx.fillStyle = '#ff9800' - ctx.fill() - - ctx.beginPath() - ctx.arc(x, y - 34 + flick, 3, 0, Math.PI * 2) - ctx.fillStyle = '#ffeb3b' - ctx.fill() - } - - private renderShadow(ctx: CanvasRenderingContext2D, x: number, y: number, rx: number, ry: number) { - ctx.save() - ctx.beginPath() - ctx.ellipse(x, y, rx, ry, 0, 0, Math.PI * 2) - ctx.fillStyle = 'rgba(0, 0, 0, 0.45)' - ctx.fill() - ctx.restore() - } - - private renderPlayerAvatar(ctx: CanvasRenderingContext2D, x: number, y: number) { - ctx.save() - - // Hit flash overlay - if (this.player.hitFlash > 0) { - ctx.filter = 'brightness(1.8) drop-shadow(0 0 8px rgba(255,50,50,0.8))' - } - - const isAttacking = this.player.action === 'attack' - const isCasting = this.player.action === 'cast' - const isDead = this.player.action === 'dead' - const breath = Math.sin(Date.now() * 0.004) * 2 - - if (isDead) { - ctx.translate(x, y) - ctx.rotate(-Math.PI / 2) - ctx.fillStyle = '#555' - ctx.fillRect(-15, -40, 30, 40) - ctx.restore() - return - } - - // Body base - ctx.translate(x, y + breath) - - // Cloak / Robes - ctx.fillStyle = this.player.class === 'Sorceress' ? '#283593' : '#37474f' - ctx.beginPath() - ctx.moveTo(-16, 0) - ctx.lineTo(16, 0) - ctx.lineTo(10, -40) - ctx.lineTo(-10, -40) - ctx.closePath() - ctx.fill() - - // Torso / Armour - ctx.fillStyle = '#d4af37' - ctx.fillRect(-9, -42, 18, 22) - - // Belt - ctx.fillStyle = '#4e342e' - ctx.fillRect(-9, -23, 18, 4) - - // Head - ctx.fillStyle = '#ffcc80' - ctx.beginPath() - ctx.arc(0, -48, 8, 0, Math.PI * 2) - ctx.fill() - - // Hair / Hood - ctx.fillStyle = this.player.class === 'Sorceress' ? '#bf360c' : '#212121' - ctx.beginPath() - ctx.arc(0, -50, 8.5, Math.PI * 0.8, Math.PI * 2.2) - ctx.fill() - - // Weapon / Staff - ctx.save() - if (isAttacking) { - ctx.rotate(0.35) - ctx.fillStyle = '#b0bec5' - ctx.fillRect(10, -50, 4, 45) // Sword slash - ctx.fillStyle = '#ffd54f' - ctx.fillRect(6, -25, 12, 4) - } else if (isCasting) { - ctx.fillStyle = '#81d4fa' - ctx.fillRect(8, -65, 3, 55) // Staff raised - ctx.beginPath() - ctx.arc(9.5, -67, 7, 0, Math.PI * 2) - ctx.fillStyle = 'rgba(129, 212, 250, 0.6)' - ctx.fill() - } else { - ctx.fillStyle = '#b0bec5' - ctx.fillRect(8, -40, 3, 40) - } - ctx.restore() - - // Shield (if has block) - if (this.player.block > 0) { - ctx.fillStyle = '#cfd8dc' - ctx.beginPath() - ctx.moveTo(-18, -35) - ctx.lineTo(-6, -35) - ctx.lineTo(-8, -18) - ctx.lineTo(-12, -12) - ctx.lineTo(-16, -18) - ctx.closePath() - ctx.fill() - ctx.strokeStyle = '#d4af37' - ctx.lineWidth = 1.5 - ctx.stroke() - } - - ctx.restore() - } - - private renderMonsterAvatar(ctx: CanvasRenderingContext2D, x: number, y: number) { - ctx.save() - - const drawX = x + this.monster.knockbackOffset - - if (this.monster.hitFlash > 0) { - ctx.filter = 'brightness(2.0) drop-shadow(0 0 10px rgba(255,30,30,0.9))' - } else if (this.monster.freezeTimer > 0) { - ctx.filter = 'drop-shadow(0 0 12px rgba(0,229,255,0.9)) hue-rotate(190deg) brightness(1.2)' - } else if (this.monster.chillTimer > 0 || this.monster.slowTimer > 0) { - ctx.filter = 'drop-shadow(0 0 8px rgba(33,150,243,0.8)) hue-rotate(180deg)' - } else if (this.monster.blindTimer > 0) { - ctx.filter = 'drop-shadow(0 0 8px rgba(171,71,188,0.8)) brightness(0.6)' - } else if (this.monster.fleeTimer > 0) { - ctx.filter = 'drop-shadow(0 0 8px rgba(255,112,67,0.8))' - } else if (this.monster.openWoundsTimer > 0) { - ctx.filter = 'drop-shadow(0 0 8px rgba(183,28,28,0.8))' - } - - const isAttacking = this.monster.action === 'attack' - const isDead = this.monster.action === 'dead' - const isStunned = this.monster.stunTimer > 0 - const breath = Math.sin(Date.now() * 0.005) * 2 - - if (isDead) { - ctx.translate(drawX, y) - ctx.rotate(Math.PI / 2) - ctx.fillStyle = '#424242' - ctx.fillRect(-15, -40, 30, 40) - ctx.restore() - return - } - - ctx.translate(drawX, y + (isStunned ? -4 : breath)) - if (this.monster.fleeTimer > 0) { - ctx.scale(-1, 1) // Turn around in terror - } - - // Monster Base Shape according to template - const color = this.monster.color - ctx.fillStyle = color - - if (this.monster.typeKey.startsWith('fallen')) { - // Small hunched demon - ctx.beginPath() - ctx.ellipse(0, -22, 14, 18, -0.2, 0, Math.PI * 2) - ctx.fill() - // Horns - ctx.fillStyle = '#212121' - ctx.beginPath() - ctx.moveTo(-8, -36); ctx.lineTo(-12, -44); ctx.lineTo(-4, -38) - ctx.moveTo(4, -38); ctx.lineTo(8, -44); ctx.lineTo(6, -36) - ctx.fill() - // Small dagger - ctx.fillStyle = '#90a4ae' - ctx.fillRect(isAttacking ? -22 : -14, -26, 12, 3) - } else if (this.monster.typeKey.startsWith('zombie')) { - // Tall shambling corpse - ctx.fillRect(-12, -48, 24, 45) - ctx.fillStyle = '#33691e' - ctx.beginPath() - ctx.arc(0, -52, 10, 0, Math.PI * 2) - ctx.fill() - } else if (this.monster.typeKey === 'diablo' || this.monster.typeKey === 'baal') { - // Huge boss figure - ctx.beginPath() - ctx.ellipse(0, -45, 26, 42, 0, 0, Math.PI * 2) - ctx.fill() - // Giant Horns - ctx.fillStyle = '#212121' - ctx.beginPath() - ctx.moveTo(-16, -75); ctx.lineTo(-32, -95); ctx.lineTo(-12, -80) - ctx.moveTo(12, -80); ctx.lineTo(32, -95); ctx.lineTo(16, -75) - ctx.fill() - // Glowing Eyes - ctx.fillStyle = '#ffeb3b' - ctx.beginPath() - ctx.arc(-8, -70, 3, 0, Math.PI * 2) - ctx.arc(8, -70, 3, 0, Math.PI * 2) - ctx.fill() - } else if (this.monster.typeKey === 'dummy') { - // Wood target dummy - ctx.fillStyle = '#8d6e63' - ctx.fillRect(-6, -45, 12, 45) - ctx.fillRect(-22, -38, 44, 10) - ctx.beginPath() - ctx.arc(0, -45, 12, 0, Math.PI * 2) - ctx.fillStyle = '#d7ccc8' - ctx.fill() - ctx.strokeStyle = '#c62828' - ctx.lineWidth = 2 - ctx.stroke() - } else { - // Generic humanoid monster - ctx.fillRect(-12, -42, 24, 40) - ctx.beginPath() - ctx.arc(0, -46, 9, 0, Math.PI * 2) - ctx.fill() - } - - // Frozen Ice Crystal Overlay - if (this.monster.freezeTimer > 0 && !isDead) { - ctx.save() - ctx.fillStyle = 'rgba(128, 222, 234, 0.45)' - ctx.strokeStyle = '#80deea' - ctx.lineWidth = 2 - ctx.beginPath() - ctx.rect(-24, -68, 48, 72) - ctx.fill() - ctx.stroke() - // Ice crack lines - ctx.beginPath() - ctx.moveTo(-14, -50); ctx.lineTo(-2, -36); ctx.lineTo(14, -46) - ctx.moveTo(-2, -36); ctx.lineTo(-6, -18) - ctx.moveTo(3, -28); ctx.lineTo(12, -14) - ctx.strokeStyle = '#e0f7fa' - ctx.lineWidth = 1.5 - ctx.stroke() - ctx.restore() - } - - // Open Wounds Bleeding Aura - if (this.monster.openWoundsTimer > 0 && !isDead) { - ctx.save() - ctx.fillStyle = '#d32f2f' - const t = Date.now() * 0.008 - for (let i = 0; i < 4; i++) { - const dropY = ((t * 40 + i * 14) % 45) - 38 - const dropX = (i % 2 === 0 ? 1 : -1) * (8 + i * 3) - ctx.beginPath() - ctx.arc(dropX, dropY, 2, 0, Math.PI * 2) - ctx.fill() - } - ctx.restore() - } - - ctx.restore() - } - - private renderProjectiles(ctx: CanvasRenderingContext2D) { - ctx.save() - for (const p of this.projectiles) { - ctx.beginPath() - ctx.arc(p.x, p.y, 6, 0, Math.PI * 2) - if (p.type === 'fire') { - ctx.fillStyle = '#ff5722' - ctx.shadowColor = '#ff9800' - ctx.shadowBlur = 12 - } else if (p.type === 'cold') { - ctx.fillStyle = '#00e5ff' - ctx.shadowColor = '#80d8ff' - ctx.shadowBlur = 10 - } else if (p.type === 'light') { - ctx.fillStyle = '#ffff00' - ctx.shadowColor = '#ffeb3b' - ctx.shadowBlur = 12 - } else { - ctx.fillStyle = '#e040fb' - ctx.shadowColor = '#ea80fc' - ctx.shadowBlur = 12 - } - ctx.fill() - } - ctx.restore() - } - - private renderParticles(ctx: CanvasRenderingContext2D) { - ctx.save() - for (const pt of this.particles) { - ctx.beginPath() - ctx.arc(pt.x, pt.y, pt.size, 0, Math.PI * 2) - ctx.fillStyle = pt.color - ctx.globalAlpha = Math.max(0, pt.life / pt.maxLife) - ctx.fill() - } - ctx.restore() - } - - private renderOverheadPlayerBar(ctx: CanvasRenderingContext2D, x: number, y: number) { - const barW = 80 - const barH = 7 - - // HP Bar - ctx.fillStyle = '#1c150e' - ctx.fillRect(x - barW / 2, y, barW, barH) - const hpRatio = Math.max(0, Math.min(1, this.player.hp / this.player.maxHp)) - ctx.fillStyle = '#d32f2f' - ctx.fillRect(x - barW / 2, y, barW * hpRatio, barH) - ctx.strokeStyle = '#4e342e' - ctx.lineWidth = 1 - ctx.strokeRect(x - barW / 2, y, barW, barH) - - // Mana Bar - ctx.fillStyle = '#1c150e' - ctx.fillRect(x - barW / 2, y + 8, barW, 4) - const manaRatio = Math.max(0, Math.min(1, this.player.mana / this.player.maxMana)) - ctx.fillStyle = '#1976d2' - ctx.fillRect(x - barW / 2, y + 8, barW * manaRatio, 4) - - // Text Label - ctx.font = '10px ui-monospace, monospace' - ctx.fillStyle = '#fff' - ctx.textAlign = 'center' - ctx.fillText(`${this.player.class} (Lv${this.player.level})`, x, y - 5) - ctx.fillStyle = '#ffcdd2' - ctx.fillText(`${Math.round(this.player.hp)}/${this.player.maxHp}`, x, y + barH + 16) - } - - private renderOverheadMonsterBar(ctx: CanvasRenderingContext2D, x: number, y: number) { - const barW = 100 - const barH = 8 - - // Status Badges (PMH, Slow, Blind, Flee, Frozen, Chill, OW) - const badges: { text: string; color: string }[] = [] - if (this.monster.pmhTimer > 0) badges.push({ text: '🚫PMH', color: '#81c784' }) - if (this.monster.slowTimer > 0) badges.push({ text: `🐌-${this.monster.slowPercent}%`, color: '#ffb74d' }) - if (this.monster.blindTimer > 0) badges.push({ text: '👁️BLIND', color: '#ba68c8' }) - if (this.monster.fleeTimer > 0) badges.push({ text: '😱FLEE', color: '#ff8a65' }) - if (this.monster.freezeTimer > 0) badges.push({ text: '❄️FROZEN', color: '#80deea' }) - else if (this.monster.chillTimer > 0) badges.push({ text: '❄️CHILL', color: '#81d4fa' }) - if (this.monster.openWoundsTimer > 0) badges.push({ text: '🩸OW', color: '#e53935' }) - - if (badges.length > 0) { - ctx.font = 'bold 9px ui-monospace, monospace' - let badgeX = x - (badges.length * 44) / 2 + 22 - for (const b of badges) { - ctx.fillStyle = 'rgba(0, 0, 0, 0.7)' - ctx.fillRect(badgeX - 20, y - 22, 40, 13) - ctx.fillStyle = b.color - ctx.textAlign = 'center' - ctx.fillText(b.text, badgeX, y - 12) - badgeX += 44 - } - } - - // HP Bar - ctx.fillStyle = '#1c150e' - ctx.fillRect(x - barW / 2, y, barW, barH) - const hpRatio = Math.max(0, Math.min(1, this.monster.hp / this.monster.maxHp)) - ctx.fillStyle = '#c62828' - ctx.fillRect(x - barW / 2, y, barW * hpRatio, barH) - ctx.strokeStyle = '#5d4037' - ctx.lineWidth = 1 - ctx.strokeRect(x - barW / 2, y, barW, barH) - - // Text Label - ctx.font = '11px ui-monospace, monospace' - ctx.fillStyle = '#f0c070' - ctx.textAlign = 'center' - ctx.fillText(`${this.monster.name} Lv${this.monster.level}`, x, y - 5) - ctx.font = '10px ui-monospace, monospace' - ctx.fillStyle = '#ffcdd2' - ctx.fillText(`${Math.round(this.monster.hp)}/${this.monster.maxHp}`, x, y + barH + 12) - } - - private renderFloatingTexts(ctx: CanvasRenderingContext2D) { - ctx.save() - ctx.textAlign = 'center' - for (const ft of this.floatingTexts) { - ctx.font = `bold ${ft.size}px ui-monospace, monospace` - ctx.globalAlpha = ft.alpha - ctx.strokeStyle = '#000' - ctx.lineWidth = 3 - ctx.strokeText(ft.text, ft.x, ft.y) - ctx.fillStyle = ft.color - ctx.fillText(ft.text, ft.x, ft.y) - } - ctx.restore() - } -} - -function escapeHtml(str: string): string { - return str.replace(/&/g, '&').replace(//g, '>') -} - -// Start application when DOM is ready -window.addEventListener('DOMContentLoaded', () => { - new CombatArenaApp() -}) diff --git a/src/boss.ts b/src/boss.ts deleted file mode 100644 index c8ef3d4..0000000 --- a/src/boss.ts +++ /dev/null @@ -1,658 +0,0 @@ -/** - * Diablo II Act Boss Loot Simulator (Issue #110). - * - * Implements: - * - Full Act 1-5 Boss selection (Andariel, Duriel, Mephisto, Diablo, Baal) across 3 difficulties. - * - Player count (1-8 Players / PP) scaling on NoDrop. - * - Magic Find (0-1000% MF) quality scaling with diminishing returns. - * - Quest Kill (First kill / Bug kill) TreasureClass toggle. - * - Complete 1.13c drop execution with fully rolled affixes and item properties. - * - Canonical D2 item tooltip presentation with equipment stats, blue affix mods, runes, and sets. - */ - -import { getEmbeddedDropTables } from './game/embedded-drop-tables.ts' -import { executeDropPipeline, type DropTables } from './game/drop-pipeline.ts' -import { formatItemTooltip, type FormattedItemTooltip } from './game/item-tooltip.ts' -import { getTooltipLocale, setTooltipLocale, type TooltipLocale } from './game/tooltip-i18n.ts' -import type { Item } from './game/items.ts' -import { computeScaledNoDrop, computeEffectivePlayers } from './game/treasure-engine.ts' - -export type Difficulty = 'normal' | 'nightmare' | 'hell' - -export interface BossPreset { - readonly id: string - readonly act: 1 | 2 | 3 | 4 | 5 - readonly nameCn: string - readonly nameEn: string - readonly titleCn: string - readonly areaCn: string - readonly icon: string - readonly mlvl: { normal: number; nightmare: number; hell: number } - readonly tc: { normal: string; nightmare: string; hell: string } - readonly questTc: { normal: string; nightmare: string; hell: string } - readonly baseNoDrop: number - readonly picks: number - readonly monsterType?: number - readonly descCn: string - readonly highlightDrops: string -} - -export type BossDefinition = BossPreset - -export const ACT_BOSSES: readonly BossPreset[] = [ - { - id: 'andariel', - act: 1, - nameCn: '安达利尔 (Andariel)', - nameEn: 'Maiden of Anguish', - titleCn: '第一幕 · 痛苦之貌', - areaCn: '地下墓穴第四层 (Catacombs Level 4)', - icon: '🕷️', - mlvl: { normal: 12, nightmare: 49, hell: 75 }, - tc: { normal: 'Andariel', nightmare: 'Andariel (N)', hell: 'Andariel (H)' }, - questTc: { normal: 'Andarielq', nightmare: 'Andarielq (N)', hell: 'Andarielq (H)' }, - baseNoDrop: 19, - picks: 7, - monsterType: 4, - descCn: '第一幕关底首领,弱火(-50%火抗)。暗黑2历史上著名的Bug首杀核心首领,乔丹之石 (SOJ) 掉率全游戏最高!', - highlightDrops: '乔丹之石 (SOJ)、暗金戒指/项链、海蛇皮甲', - }, - { - id: 'duriel', - act: 2, - nameCn: '督瑞尔 (Duriel)', - nameEn: 'Lord of Pain', - titleCn: '第二幕 · 苦痛之王', - areaCn: '塔·拉夏的古墓 (Tal Rasha\'s Chamber)', - icon: '🐛', - mlvl: { normal: 22, nightmare: 55, hell: 88 }, - tc: { normal: 'Duriel', nightmare: 'Duriel (N)', hell: 'Duriel (H)' }, - questTc: { normal: 'Durielq', nightmare: 'Durielq (N)', hell: 'Durielq (H)' }, - baseNoDrop: 19, - picks: 7, - monsterType: 4, - descCn: '第二幕关底首领,神圣冰冻光环。回城卷轴掉落特化,首杀可获得丰富的中级暗金装备与首饰。', - highlightDrops: '督瑞尔的壳、暗金首饰、高级符文', - }, - { - id: 'mephisto', - act: 3, - nameCn: '墨菲斯托 (Mephisto)', - nameEn: 'Lord of Hatred', - titleCn: '第三幕 · 憎恨之王', - areaCn: '憎恨囚牢第三层 (Durance of Hate Level 3)', - icon: '💀', - mlvl: { normal: 26, nightmare: 59, hell: 87 }, - tc: { normal: 'Mephisto', nightmare: 'Mephisto (N)', hell: 'Mephisto (H)' }, - questTc: { normal: 'Mephistoq', nightmare: 'Mephistoq (N)', hell: 'Mephistoq (H)' }, - baseNoDrop: 15, - picks: 7, - monsterType: 4, - descCn: '第三幕关底首领,全暗黑2玩家公认最受爱戴的「劳模」。地形隔河击杀简单,全游戏刷军帽、眼球、暴风盾等核心日用品效率之王!', - highlightDrops: '军帽 (Shako)、暴风之盾、眼球、海蛇皮、亚瑞特的面容', - }, - { - id: 'diablo', - act: 4, - nameCn: '暗黑破坏神 (Diablo)', - nameEn: 'Lord of Terror', - titleCn: '第四幕 · 恐惧之王', - areaCn: '混沌避难所 (Chaos Sanctuary)', - icon: '👹', - mlvl: { normal: 40, nightmare: 62, hell: 94 }, - tc: { normal: 'Diablo', nightmare: 'Diablo (N)', hell: 'Diablo (H)' }, - questTc: { normal: 'Diabloq', nightmare: 'Diabloq (N)', hell: 'Diabloq (H)' }, - baseNoDrop: 15, - picks: 7, - monsterType: 4, - descCn: '第四幕关底首领,红色闪电地狱火。mlvl高达94,能掉落大君之怒、马拉的万花筒等全系列高级暗金护身符与首饰。', - highlightDrops: '马拉的万花筒、大君之怒、丧钟、风之锤', - }, - { - id: 'baal', - act: 5, - nameCn: '巴尔 (Baal)', - nameEn: 'Lord of Destruction', - titleCn: '第五幕 · 毁灭之王', - areaCn: '世界之石大厅 (Worldstone Chamber)', - icon: '👑', - mlvl: { normal: 60, nightmare: 75, hell: 99 }, - tc: { normal: 'Baal', nightmare: 'Baal (N)', hell: 'Baal (H)' }, - questTc: { normal: 'Baalq', nightmare: 'Baalq (N)', hell: 'Baalq (H)' }, - baseNoDrop: 15, - picks: 7, - monsterType: 4, - descCn: '终局最高首领,地狱难度怪物等级 mlvl 99!能掉落全暗黑2所有最高等级的宝藏(tc87/qlvl85全部封顶物品:风之力、泰瑞尔的力量、祖父等)。', - highlightDrops: '风之力 (Windforce)、泰瑞尔的力量、祖父、死亡之网、全符文', - }, -] - -export const BOSS_LIST: readonly BossDefinition[] = ACT_BOSSES - -// State -let dropTables: DropTables | null = null -let currentBoss: BossPreset = ACT_BOSSES[2]! // Default: Mephisto -let currentDifficulty: Difficulty = 'hell' -let isQuestKill: boolean = true -let currentPlayers: number = 1 -let currentMf: number = 250 - -interface DropSessionRecord { - readonly killIndex: number - readonly timestamp: number - readonly boss: BossPreset - readonly difficulty: Difficulty - readonly isQuest: boolean - readonly players: number - readonly mf: number - readonly items: Item[] -} - -const sessionHistory: DropSessionRecord[] = [] -let latestDroppedItems: Item[] = [] -let totalKills = 0 -let totalItemsDropped = 0 -let totalUniques = 0 -let totalSets = 0 -let totalRares = 0 -let totalMagics = 0 -let totalRunes = 0 - -// Initialize -export function initBossApp(): void { - // 1. Initialize Drop Tables - try { - dropTables = getEmbeddedDropTables() - console.log('Diablo II Drop Tables loaded successfully from canonical data.') - } catch (err) { - console.error('Failed to load drop tables:', err) - } - - // 2. Bind DOM Events - bindBossSelector() - bindDifficultySelector() - bindSettings() - bindActionButtons() - bindLanguageSelector() - bindKeyboardShortcuts() - - // 3. Initial UI update - updateBossCard() - updateParamTips() - renderLootShowcase([]) -} - -function bindBossSelector(): void { - const container = document.getElementById('boss-list-container') - if (!container) return - - container.innerHTML = ACT_BOSSES.map(b => { - const isSelected = b.id === currentBoss.id - return ` -
-
${b.icon}
-
-
${b.nameCn}
-
${b.titleCn}
-
-
- ` - }).join('') - - container.querySelectorAll('.boss-select-card').forEach(el => { - el.addEventListener('click', () => { - const id = el.getAttribute('data-boss-id') - const found = ACT_BOSSES.find(b => b.id === id) - if (found) { - currentBoss = found - container.querySelectorAll('.boss-select-card').forEach(c => c.classList.remove('selected')) - el.classList.add('selected') - updateBossCard() - updateParamTips() - } - }) - }) -} - -function bindDifficultySelector(): void { - const pills = document.querySelectorAll('.diff-pill') - pills.forEach(pill => { - pill.addEventListener('click', () => { - pills.forEach(p => p.classList.remove('active')) - pill.classList.add('active') - currentDifficulty = (pill.getAttribute('data-diff') as Difficulty) || 'hell' - updateBossCard() - updateParamTips() - }) - }) -} - -function bindSettings(): void { - // Quest Kill checkbox - const questCheck = document.getElementById('quest-kill-toggle') as HTMLInputElement | null - if (questCheck) { - questCheck.checked = isQuestKill - questCheck.addEventListener('change', () => { - isQuestKill = questCheck.checked - updateBossCard() - updateParamTips() - }) - } - - // Players slider & pills - const playersRange = document.getElementById('players-range') as HTMLInputElement | null - const playersVal = document.getElementById('players-val') - if (playersRange && playersVal) { - playersRange.value = String(currentPlayers) - playersVal.textContent = `${currentPlayers}P` - playersRange.addEventListener('input', () => { - currentPlayers = Number(playersRange.value) - playersVal.textContent = `${currentPlayers}P` - updatePlayersPillActive(currentPlayers) - updateParamTips() - }) - } - - document.querySelectorAll('.pp-pill').forEach(btn => { - btn.addEventListener('click', () => { - const p = Number(btn.getAttribute('data-pp')) - if (p >= 1 && p <= 8) { - currentPlayers = p - if (playersRange) playersRange.value = String(p) - if (playersVal) playersVal.textContent = `${p}P` - updatePlayersPillActive(p) - updateParamTips() - } - }) - }) - - // MF slider & input - const mfRange = document.getElementById('mf-range') as HTMLInputElement | null - const mfInput = document.getElementById('mf-input') as HTMLInputElement | null - if (mfRange && mfInput) { - mfRange.value = String(currentMf) - mfInput.value = String(currentMf) - - mfRange.addEventListener('input', () => { - currentMf = Math.max(0, Math.min(1000, Number(mfRange.value))) - mfInput.value = String(currentMf) - updateParamTips() - }) - - mfInput.addEventListener('change', () => { - currentMf = Math.max(0, Math.min(1000, Number(mfInput.value) || 0)) - mfRange.value = String(currentMf) - mfInput.value = String(currentMf) - updateParamTips() - }) - } - - // MF quick presets - document.querySelectorAll('.mf-preset-btn').forEach(btn => { - btn.addEventListener('click', () => { - const v = Number(btn.getAttribute('data-mf')) - currentMf = v - if (mfRange) mfRange.value = String(v) - if (mfInput) mfInput.value = String(v) - updateParamTips() - }) - }) -} - -function updatePlayersPillActive(p: number): void { - document.querySelectorAll('.pp-pill').forEach(btn => { - btn.classList.toggle('active', Number(btn.getAttribute('data-pp')) === p) - }) -} - -function updateBossCard(): void { - const iconEl = document.getElementById('boss-preview-icon') - const nameEl = document.getElementById('boss-preview-name') - const titleEl = document.getElementById('boss-preview-title') - const areaEl = document.getElementById('boss-preview-area') - const mlvlEl = document.getElementById('boss-preview-mlvl') - const tcEl = document.getElementById('boss-preview-tc') - const questEl = document.getElementById('boss-preview-quest') - const descEl = document.getElementById('boss-preview-desc') - const dropsEl = document.getElementById('boss-preview-highlights') - - const mlvl = currentBoss.mlvl[currentDifficulty] - const effectiveTc = isQuestKill ? currentBoss.questTc[currentDifficulty] : currentBoss.tc[currentDifficulty] - - if (iconEl) iconEl.textContent = currentBoss.icon - if (nameEl) nameEl.textContent = currentBoss.nameCn - if (titleEl) titleEl.textContent = `${currentBoss.titleCn} · [${currentDifficulty.toUpperCase()}]` - if (areaEl) areaEl.textContent = currentBoss.areaCn - if (mlvlEl) mlvlEl.textContent = `mlvl ${mlvl}` - if (tcEl) tcEl.textContent = `TC: ${effectiveTc}` - if (questEl) { - questEl.textContent = isQuestKill ? '🌟 首杀判定启用 (Quest Kill)' : '常规击杀 (Standard Kill)' - questEl.className = isQuestKill ? 'boss-quest-badge quest-on' : 'boss-quest-badge quest-off' - } - if (descEl) descEl.textContent = currentBoss.descCn - if (dropsEl) dropsEl.textContent = currentBoss.highlightDrops -} - -function updateParamTips(): void { - const nodropTip = document.getElementById('tip-nodrop') - const mfTip = document.getElementById('tip-mf') - - const effectivePlayers = computeEffectivePlayers(currentPlayers, 1) - const scaledNoDrop = computeScaledNoDrop(currentBoss.baseNoDrop, 65, effectivePlayers) - const nodropPercent = ((scaledNoDrop / (scaledNoDrop + 65)) * 100).toFixed(1) - - if (nodropTip) { - nodropTip.textContent = `${currentPlayers}P 模式下有效玩家数 ${effectivePlayers.toFixed(1)},单次挑选无掉落率: ${nodropPercent}%` - } - - if (mfTip) { - // 1.13c Diminishing returns: Unique = (MF * 250) / (MF + 250) - const effectiveUniqueMf = Math.trunc((currentMf * 250) / (currentMf + 250)) - const effectiveRareMf = Math.trunc((currentMf * 600) / (currentMf + 600)) - mfTip.textContent = `实际寻宝率 ${currentMf}% · 暗金等效: +${effectiveUniqueMf}% · 黄金等效: +${effectiveRareMf}%` - } -} - -function bindActionButtons(): void { - const killBtn = document.getElementById('btn-kill-boss') - const kill10Btn = document.getElementById('btn-kill-10') - const clearBtn = document.getElementById('btn-clear-loot') - - if (killBtn) { - killBtn.addEventListener('click', () => { - executeBossKill(1) - }) - } - - if (kill10Btn) { - kill10Btn.addEventListener('click', () => { - executeBossKill(10) - }) - } - - if (clearBtn) { - clearBtn.addEventListener('click', () => { - latestDroppedItems = [] - renderLootShowcase([]) - }) - } -} - -function bindLanguageSelector(): void { - const pills = document.querySelectorAll('.lang-pill') - pills.forEach(pill => { - pill.addEventListener('click', () => { - const lang = pill.dataset.lang as TooltipLocale | undefined - if (!lang) return - setTooltipLocale(lang) - pills.forEach(p => p.classList.toggle('active', p === pill)) - renderLootShowcase(latestDroppedItems) - }) - }) -} - -function bindKeyboardShortcuts(): void { - window.addEventListener('keydown', (e: KeyboardEvent) => { - // Don't trigger if typing in an input - if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { - return - } - if (e.code === 'Space' || e.code === 'Enter') { - e.preventDefault() - executeBossKill(1) - } - }) -} - -// Boss Kill Execution -export function executeBossKill(times: number = 1): void { - if (!dropTables) { - alert('掉落数据表仍在加载中,请稍候...') - return - } - - const effectiveTc = isQuestKill ? currentBoss.questTc[currentDifficulty] : currentBoss.tc[currentDifficulty] - const nLevel = currentBoss.mlvl[currentDifficulty] - - const batchItems: Item[] = [] - const startTime = performance.now() - - for (let i = 0; i < times; i++) { - const drops = executeDropPipeline({ - tcName: effectiveTc, - nLevel, - monsterType: currentBoss.monsterType ?? 4, - dropTables, - difficulty: currentDifficulty, - playerMf: currentMf, - gamePlayers: currentPlayers, - partyPlayers: 1, - }) - - totalKills += 1 - totalItemsDropped += drops.length - - for (const item of drops) { - const q = (item.rarity || 'normal').toLowerCase() - if (q === 'unique') totalUniques++ - else if (q === 'set') totalSets++ - else if (q === 'rare') totalRares++ - else if (q === 'magic') totalMagics++ - else if (item.code?.trim().startsWith('r')) totalRunes++ - - batchItems.push(item) - } - - sessionHistory.push({ - killIndex: totalKills, - timestamp: Date.now(), - boss: currentBoss, - difficulty: currentDifficulty, - isQuest: isQuestKill, - players: currentPlayers, - mf: currentMf, - items: drops, - }) - } - - const durationMs = (performance.now() - startTime).toFixed(1) - - // Trigger Animation - flashKillEffect() - - // Update Stats & UI - updateStatsDisplay() - renderKillBanner(times, batchItems.length, durationMs) - renderLootShowcase(batchItems) -} - -function flashKillEffect(): void { - const showcase = document.getElementById('loot-showcase-panel') - if (showcase) { - showcase.classList.remove('kill-flash') - void showcase.offsetWidth // force reflow - showcase.classList.add('kill-flash') - } -} - -function updateStatsDisplay(): void { - const killsEl = document.getElementById('stat-total-kills') - const itemsEl = document.getElementById('stat-total-items') - const uniqEl = document.getElementById('stat-uniques') - const setEl = document.getElementById('stat-sets') - const rareEl = document.getElementById('stat-rares') - const magicEl = document.getElementById('stat-magics') - const runeEl = document.getElementById('stat-runes') - - if (killsEl) killsEl.textContent = totalKills.toLocaleString() - if (itemsEl) itemsEl.textContent = totalItemsDropped.toLocaleString() - if (uniqEl) uniqEl.textContent = totalUniques.toLocaleString() - if (setEl) setEl.textContent = totalSets.toLocaleString() - if (rareEl) rareEl.textContent = totalRares.toLocaleString() - if (magicEl) magicEl.textContent = totalMagics.toLocaleString() - if (runeEl) runeEl.textContent = totalRunes.toLocaleString() -} - -function renderKillBanner(times: number, dropCount: number, durationMs: string): void { - const banner = document.getElementById('kill-result-banner') - if (!banner) return - - const diffNames = { normal: '普通', nightmare: '噩梦', hell: '地狱' } - const bossText = `${diffNames[currentDifficulty]}难度 · ${currentBoss.nameCn}` - const questText = isQuestKill ? '🌟 任务首杀' : '常规击杀' - - banner.innerHTML = ` - - ` -} - -function renderLootShowcase(items: Item[]): void { - latestDroppedItems = items - const container = document.getElementById('loot-items-grid') - if (!container) return - - const locale = getTooltipLocale() - - if (items.length === 0) { - const isZh = locale === 'zh' || locale === 'bilingual' - container.innerHTML = ` -
-
⚔️
-
${isZh ? '尚未击杀首领' : 'No Boss Slain Yet'}
-
${isZh ? '点击左侧「模拟击杀 Boss」或按下 [空格键] 体验 1.13c 真实掉落与词缀属性生成' : 'Click "Kill Boss" on the left sidebar or press [Space] to simulate authentic 1.13c drops & affixes.'}
-
- ` - return - } - - // Render Item Cards - container.innerHTML = items.map((item, idx) => { - const tooltip = formatItemTooltip(item, locale) - return renderSingleItemCard(tooltip, idx + 1, locale) - }).join('') -} - -function renderSingleItemCard(t: FormattedItemTooltip, index: number, locale: TooltipLocale = getTooltipLocale()): string { - const linesHtml = t.lines.map(l => { - const color = l.color || 'blue' - const colorClass = color === 'instruction' ? 'line-white line-instruction' : `line-${color}` - return `
${escapeHtml(l.text)}
` - }).join('') - - const isZh = locale === 'zh' || locale === 'bilingual' - const lblDefense = isZh ? '防御: ' : 'Defense: ' - const lblOneHand = isZh ? '单手伤害: ' : 'One-Hand Damage: ' - const lblTwoHand = isZh ? '双手伤害: ' : 'Two-Hand Damage: ' - const lblDurability = isZh ? '耐久度: ' : 'Durability: ' - const lblReqLevel = isZh ? '需要等级: ' : 'Required Level: ' - const lblReqStr = isZh ? '需要力量: ' : 'Required Strength: ' - const lblReqDex = isZh ? '需要敏捷: ' : 'Required Dexterity: ' - const wordOf = isZh ? ' / ' : ' of ' - const wordTo = isZh ? ' 至 ' : ' to ' - - const baseStatsHtml: string[] = [] - if (t.defense !== undefined) { - const defText = - t.baseDefense !== undefined && t.baseDefense !== t.defense - ? `${t.defense} (${t.baseDefense})` - : `${t.defense}` - baseStatsHtml.push(`
${lblDefense}${defText}
`) - } - if (t.oneHandDamage) { - const d = t.oneHandDamage - const dmgText = - d.isEnhanced && d.baseMin !== undefined && d.baseMax !== undefined - ? `${d.min}${wordTo}${d.max} (${d.baseMin}${wordTo}${d.baseMax})` - : `${d.min}${wordTo}${d.max}` - baseStatsHtml.push(`
${lblOneHand}${dmgText}
`) - } - if (t.twoHandDamage) { - const d = t.twoHandDamage - const dmgText = - d.isEnhanced && d.baseMin !== undefined && d.baseMax !== undefined - ? `${d.min}${wordTo}${d.max} (${d.baseMin}${wordTo}${d.baseMax})` - : `${d.min}${wordTo}${d.max}` - baseStatsHtml.push(`
${lblTwoHand}${dmgText}
`) - } - if (t.durability) { - const dur = t.durability - const durText = - dur.isEnhanced && dur.baseMax !== undefined - ? `${dur.current}${wordOf}${dur.max} (${dur.baseMax})` - : `${dur.current}${wordOf}${dur.max}` - baseStatsHtml.push(`
${lblDurability}${durText}
`) - } - if (t.reqLevel !== undefined && !t.isSocketable) { - const lvlText = - t.baseReqLevel !== undefined && t.baseReqLevel !== t.reqLevel - ? `${t.reqLevel} (${t.baseReqLevel})` - : `${t.reqLevel}` - baseStatsHtml.push(`
${lblReqLevel}${lvlText}
`) - } - if (t.reqStr !== undefined) { - const strText = - t.baseReqStr !== undefined && t.baseReqStr !== t.reqStr - ? `${t.reqStr} (${t.baseReqStr})` - : `${t.reqStr}` - baseStatsHtml.push(`
${lblReqStr}${strText}
`) - } - if (t.reqDex !== undefined) { - const dexText = - t.baseReqDex !== undefined && t.baseReqDex !== t.reqDex - ? `${t.reqDex} (${t.baseReqDex})` - : `${t.reqDex}` - baseStatsHtml.push(`
${lblReqDex}${dexText}
`) - } - - const qLower = t.quality.toLowerCase() - const qualityClass = t.isSocketable - ? (t.qualityColor === '#ff8c00' ? 'quality-rune' : 'quality-gem') - : `quality-${qLower}` - - return ` -
-
-
${escapeHtml(t.title)}
- ${t.subTitle ? `
${escapeHtml(t.subTitle)}
` : ''} -
- ${escapeHtml(t.quality.toUpperCase())} - ilvl ${t.ilvl} -
-
- - ${baseStatsHtml.length > 0 ? ` -
- ${baseStatsHtml.join('')} -
- ` : ''} - - ${t.lines.length > 0 ? ` -
- ${linesHtml} -
- ` : ''} -
- ` -} - -function escapeHtml(str: string): string { - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') -} - -// Auto-run on DOM ready -if (typeof document !== 'undefined') { - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => initBossApp()) - } else { - initBossApp() - } -} diff --git a/src/loot.ts b/src/loot.ts deleted file mode 100644 index 730b65e..0000000 --- a/src/loot.ts +++ /dev/null @@ -1,2137 +0,0 @@ -/** - * Diablo II Drop Simulator & Exact Probability Calculator (Issue #110). - * - * Provides: - * - Real production Monte Carlo simulation using rollTreasureClass, D2Rng, and ItemRatio. - * - Non-blocking chunked simulation loop with live progress bar and cancellation. - * - Exact analytic drop probability table recursively expanding the TC tree. - * - Player count scaling for NoDrop (computeScaledNoDrop). - * - Compound probability 1 - (1 - p)^picks for multiple picks. - * - Reverse lookup search indexing all eligible items to their source TCs. - */ - -import { D2Rng } from './game/d2-rng.ts' -import { - rollTreasureClass, - computeEffectivePlayers, - computeScaledNoDrop, - type DroppedItemEntry, -} from './game/treasure-engine.ts' -import { - parseTreasureClassTable, - type TreasureClassTable, - type TreasureClassNode, -} from './game/treasure-class.ts' -import { parseTable } from './game/acts.ts' -import { - mergeAutoTreasureClasses, - autoTcToTreasureClassNode, - type AutoTcNode, -} from './game/auto-tc.ts' -import { - createItemRatioTable, - rollItemQuality, - type ItemRatioTable, - type ItemRatioRow, - type ItemQualityTier, -} from './game/item-ratio.ts' - -// --- Canonical 1.13c Data Embedded ------------------------------------------ - -export const CANONICAL_TC_EX_TXT: string = "Treasure Class\tgroup\tlevel\tPicks\tUnique\tSet\tRare\tMagic\tNoDrop\tItem1\tProb1\tItem2\tProb2\tItem3\tProb3\tItem4\tProb4\tItem5\tProb5\tItem6\tProb6\tItem7\tProb7\tItem8\tProb8\tItem9\tProb9\tItem10\tProb10\tSumItems\tTotalProb\tDropChance\tTerm\r\nGold\t\t\t1\t\t\t\t\t\tgld\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nJewelry A\t\t\t1\t\t\t\t\t\trin\t8\tamu\t4\tjew\t2\tcm3\t2\tcm2\t2\tcm1\t2\t\t\t\t\t\t\t\t\t\t\t\t0\r\nJewelry B\t\t\t1\t\t\t\t\t\trin\t8\tamu\t4\tjew\t2\tcm3\t2\tcm2\t2\tcm1\t2\t\t\t\t\t\t\t\t\t\t\t\t0\r\nJewelry C\t\t\t1\t\t\t\t\t\trin\t8\tamu\t4\tjew\t2\tcm3\t2\tcm2\t2\tcm1\t2\t\t\t\t\t\t\t\t\t\t\t\t0\r\nChipped Gem\t\t\t1\t\t\t\t\t\tgcv\t3\tgcy\t3\tgcb\t3\tgcg\t3\tgcr\t3\tgcw\t3\tskc\t2\t\t\t\t\t\t\t\t\t\t0\r\nFlawed Gem\t\t\t1\t\t\t\t\t\tgfv\t3\tgfy\t3\tgfb\t3\tgfg\t3\tgfr\t3\tgfw\t3\tskf\t2\t\t\t\t\t\t\t\t\t\t0\r\nNormal Gem\t\t\t1\t\t\t\t\t\tgsv\t3\tgsy\t3\tgsb\t3\tgsg\t3\tgsr\t3\tgsw\t3\tsku\t2\t\t\t\t\t\t\t\t\t\t0\r\nFlawless Gem\t\t\t1\t\t\t\t\t\tgzv\t3\tgly\t3\tglb\t3\tglg\t3\tglr\t3\tglw\t3\tskl\t2\t\t\t\t\t\t\t\t\t\t0\r\nPerfect Gem\t\t\t1\t\t\t\t\t\tgpv\t3\tgpy\t3\tgpb\t3\tgpg\t3\tgpr\t3\tgpw\t3\tskz\t2\t\t\t\t\t\t\t\t\t\t0\r\nAmmo\t\t\t1\t\t\t\t\t\taqv\t1\tcqv\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nMisc 0\t\t\t1\t\t\t\t\t\tkey\t3\ttsc\t3\tisc\t3\topl\t2\tgpl\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nMisc 1\t\t\t1\t\t\t\t\t\tkey\t3\ttsc\t3\tisc\t3\topm\t2\tgpm\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nMisc 2\t\t\t1\t\t\t\t\t\tkey\t3\ttsc\t3\tisc\t3\tops\t2\tgps\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nHpotion 1\t\t\t1\t\t\t\t\t\thp1\t8\thp2\t3\tmp1\t3\tmp2\t2\trvs\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nHpotion 2\t\t\t1\t\t\t\t\t\thp1\t1\thp2\t4\thp3\t3\tmp2\t2\tmp3\t2\trvs\t1\t\t\t\t\t\t\t\t\t\t\t\t0\r\nHpotion 3\t\t\t1\t\t\t\t\t\thp2\t1\thp3\t4\thp4\t3\tmp3\t2\tmp4\t2\trvs\t1\t\t\t\t\t\t\t\t\t\t\t\t0\r\nHpotion 4\t\t\t1\t\t\t\t\t\thp3\t1\thp4\t4\thp5\t3\tmp4\t2\tmp5\t2\trvs\t1\t\t\t\t\t\t\t\t\t\t\t\t0\r\nHpotion 5\t\t\t1\t\t\t\t\t\thp3\t1\thp4\t4\thp5\t4\tmp5\t4\trvs\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nHpotion 6\t\t\t1\t\t\t\t\t\thp4\t4\thp5\t6\tmp4\t2\tmp5\t4\trvs\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nPotion 1\t\t\t1\t\t\t\t\t\thp1\t12\thp2\t4\tmp1\t5\tmp2\t4\trvs\t1\tvps\t4\t\t\t\t\t\t\t\t\t\t\t\t0\r\nPotion 2\t\t\t1\t\t\t\t\t\thp1\t4\thp2\t8\thp3\t5\tmp2\t2\tmp3\t5\trvs\t1\tvps\t2\typs\t3\twms\t1\t\t\t\t\t\t0\r\nPotion 3\t\t\t1\t\t\t\t\t\thp2\t6\thp3\t6\thp4\t4\tmp3\t3\tmp4\t3\trvs\t1\tvps\t2\typs\t2\twms\t1\t\t\t\t\t\t0\r\nPotion 4\t\t\t1\t\t\t\t\t\thp3\t6\thp4\t6\thp5\t4\tmp4\t3\tmp5\t3\trvs\t1\tvps\t2\typs\t2\twms\t1\t\t\t\t\t\t0\r\nPotion 5\t\t\t1\t\t\t\t\t\thp3\t4\thp4\t6\thp5\t6\tmp5\t5\trvs\t1\tvps\t2\typs\t2\twms\t1\t\t\t\t\t\t\t\t0\r\nPotion 6\t\t\t1\t\t\t\t\t\thp4\t6\thp5\t8\tmp4\t1\tmp5\t5\trvs\t1\tvps\t2\typs\t2\twms\t1\t\t\t\t\t\t\t\t0\r\nRunes 1\t\t\t1\t\t\t\t\t\tr01\t3\tr02\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nRunes 2\t\t\t1\t\t\t\t\t\tr03\t3\tr04\t2\tRunes 1\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nRunes 3\t\t\t1\t\t\t\t\t\tr05\t3\tr06\t2\tRunes 2\t5\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nRunes 4\t\t\t1\t\t\t\t\t\tr07\t3\tr08\t2\tRunes 3\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nRunes 5\t\t\t1\t\t\t\t\t\tr09\t3\tr10\t2\tRunes 4\t12\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nRunes 6\t\t\t1\t\t\t\t\t\tr11\t3\tr12\t2\tRunes 5\t22\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nRunes 7\t\t\t1\t\t\t\t\t\tr13\t3\tr14\t2\tRunes 6\t45\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nRunes 8\t\t\t1\t\t\t\t\t\tr15\t3\tr16\t2\tRunes 7\t90\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nRunes 9\t\t\t1\t\t\t\t\t\tr17\t3\tr18\t2\tRunes 8\t180\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nRunes 10\t\t\t1\t\t\t\t\t\tr19\t3\tr20\t2\tRunes 9\t360\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nRunes 11\t\t\t1\t\t\t\t\t\tr21\t3\tr22\t2\tRunes 10\t720\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nRunes 12\t\t\t1\t\t\t\t\t\tr23\t3\tr24\t2\tRunes 11\t1066\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nRunes 13\t\t\t1\t\t\t\t\t\tr25\t3\tr26\t2\tRunes 12\t1519\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nRunes 14\t\t\t1\t\t\t\t\t\tr27\t3\tr28\t2\tRunes 13\t2170\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nRunes 15\t\t\t1\t\t\t\t\t\tr29\t3\tr30\t2\tRunes 14\t2941\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nRunes 16\t\t\t1\t\t\t\t\t\tr31\t3\tr32\t2\tRunes 15\t3957\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nRunes 17\t\t\t1\t\t\t\t\t\tr33\t1\tRunes 16\t5170\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Junk\t\t\t1\t\t\t\t\t\tPotion 1\t8\tMisc 0\t4\tAmmo\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Junk\t\t\t1\t\t\t\t\t\tAct 1 Junk\t2\tPotion 2\t8\tMisc 0\t4\tAmmo\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Junk\t\t\t1\t\t\t\t\t\tAct 2 Junk\t2\tPotion 3\t8\tMisc 1\t4\tAmmo\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Junk\t\t\t1\t\t\t\t\t\tAct 3 Junk\t2\tPotion 4\t8\tMisc 1\t4\tAmmo\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Junk\t\t\t1\t\t\t\t\t\tAct 4 Junk\t2\tPotion 5\t8\tMisc 2\t4\tAmmo\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Junk\t\t\t1\t\t\t\t\t\tAct 5 Junk\t2\tPotion 6\t8\tMisc 2\t4\tAmmo\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Junk\t\t\t1\t\t\t\t\t\tAct 5 Junk\t2\tPotion 6\t8\tMisc 2\t4\tAmmo\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Junk\t\t\t1\t\t\t\t\t\tAct 5 Junk\t2\tPotion 6\t8\tMisc 2\t4\tAmmo\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Junk\t\t\t1\t\t\t\t\t\tAct 5 Junk\t2\tPotion 6\t8\tMisc 2\t4\tAmmo\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Junk\t\t\t1\t\t\t\t\t\tAct 5 Junk\t2\tPotion 6\t8\tMisc 2\t4\tAmmo\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Junk\t\t\t1\t\t\t\t\t\tAct 5 Junk\t2\tPotion 6\t8\tMisc 2\t4\tAmmo\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Junk\t\t\t1\t\t\t\t\t\tAct 5 Junk\t2\tPotion 6\t8\tMisc 2\t4\tAmmo\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Junk\t\t\t1\t\t\t\t\t\tAct 5 Junk\t2\tPotion 6\t8\tMisc 2\t4\tAmmo\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Junk\t\t\t1\t\t\t\t\t\tAct 5 Junk\t2\tPotion 6\t8\tMisc 2\t4\tAmmo\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Junk\t\t\t1\t\t\t\t\t\tAct 5 Junk\t2\tPotion 6\t8\tMisc 2\t4\tAmmo\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Equip A\t1\t0\t1\t\t\t\t\t\tweap3\t7\tarmo3\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Equip B\t1\t5\t1\t\t\t\t\t\tweap3\t3\tarmo3\t3\tweap6\t7\tarmo6\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Equip C\t1\t9\t1\t\t\t\t\t\tweap3\t2\tarmo3\t1\tweap6\t6\tarmo6\t3\tweap9\t14\tarmo9\t7\tweap12\t2\tarmo12\t1\t\t\t\t\t\t\t\t0\r\nAct 2 Equip A\t1\t12\t1\t\t\t\t\t\tweap6\t2\tarmo6\t1\tweap9\t6\tarmo9\t3\tweap12\t14\tarmo12\t7\tweap15\t2\tarmo15\t1\tAct 1 Equip C\t30\t\t\t36\t66\t0.4545\t0\r\nAct 2 Equip B\t1\t15\t1\t\t\t\t\t\tweap9\t2\tarmo9\t1\tweap12\t6\tarmo12\t3\tweap15\t14\tarmo15\t7\tweap18\t2\tarmo18\t1\tAct 2 Equip A\t60\t\t\t36\t96\t0.6250\t0\r\nAct 2 Equip C\t1\t18\t1\t\t\t\t\t\tweap12\t2\tarmo12\t1\tweap15\t6\tarmo15\t3\tweap18\t14\tarmo18\t7\tweap21\t2\tarmo21\t1\tAct 2 Equip B\t80\t\t\t36\t116\t0.6897\t0\r\nAct 3 Equip A\t1\t20\t1\t\t\t\t\t\tweap15\t2\tarmo15\t1\tweap18\t6\tarmo18\t3\tweap21\t14\tarmo21\t7\tweap24\t2\tarmo24\t1\tAct 2 Equip C\t100\t\t\t36\t136\t0.7353\t0\r\nAct 3 Equip B\t1\t22\t1\t\t\t\t\t\tweap18\t2\tarmo18\t1\tweap21\t6\tarmo21\t3\tweap24\t14\tarmo24\t7\tweap27\t2\tarmo27\t1\tAct 3 Equip A\t200\t\t\t36\t236\t0.8475\t0\r\nAct 3 Equip C\t1\t24\t1\t\t\t\t\t\tweap21\t2\tarmo21\t1\tweap24\t6\tarmo24\t3\tweap27\t14\tarmo27\t7\tweap30\t2\tarmo30\t1\tAct 3 Equip B\t340\t\t\t36\t376\t0.9043\t0\r\nAct 4 Equip A\t1\t26\t1\t\t\t\t\t\tweap24\t2\tarmo24\t1\tweap27\t6\tarmo27\t3\tweap30\t14\tarmo30\t4\tweap33\t2\tarmo33\t1\tAct 3 Equip C\t430\t\t\t33\t463\t0.9287\t0\r\nAct 4 Equip B\t1\t29\t1\t\t\t\t\t\tweap27\t2\tarmo27\t1\tweap30\t6\tarmo30\t3\tweap33\t14\tarmo33\t3\tweap36\t2\tarmo36\t1\tAct 4 Equip A\t500\t\t\t32\t532\t0.9398\t0\r\nAct 5 Equip A\t1\t32\t1\t\t\t\t\t\tweap27\t2\tarmo27\t1\tweap30\t6\tarmo30\t3\tweap33\t14\tarmo33\t3\tweap36\t7\tarmo36\t5\tAct 4 Equip B\t629\t\t\t41\t670\t0.9388\t0\r\nAct 5 Equip B\t1\t34\t1\t\t\t\t\t\tweap30\t2\tarmo30\t1\tweap33\t6\tarmo33\t2\tweap36\t14\tarmo36\t7\tweap39\t2\tarmo39\t1\tAct 5 Equip A\t731\t\t\t35\t766\t0.9543\t0\r\nAct 5 Equip C\t1\t36\t1\t\t\t\t\t\tweap30\t2\tarmo30\t1\tweap33\t6\tarmo33\t2\tweap36\t14\tarmo36\t7\tweap39\t2\tarmo39\t1\tAct 5 Equip B\t833\t\t\t35\t868\t0.9597\t0\r\nAct 1 (N) Equip A\t1\t38\t1\t\t\t\t\t\tweap33\t2\tarmo33\t1\tweap36\t6\tarmo36\t3\tweap39\t14\tarmo39\t7\tweap42\t2\tarmo42\t1\tAct 5 Equip C\t935\t\t\t36\t971\t0.9629\t0\r\nAct 1 (N) Equip B\t1\t40\t1\t\t\t\t\t\tweap33\t2\tarmo33\t1\tweap36\t6\tarmo36\t3\tweap39\t14\tarmo39\t7\tweap42\t6\tarmo42\t3\tAct 1 (N) Equip A\t1105\t\t\t42\t1147\t0.9634\t0\r\nAct 1 (N) Equip C\t1\t42\t1\t\t\t\t\t\tweap36\t2\tarmo36\t1\tweap39\t6\tarmo39\t3\tweap42\t14\tarmo42\t7\tweap45\t2\tarmo45\t1\tAct 1 (N) Equip A\t1020\t\t\t36\t1056\t0.9659\t0\r\nAct 2 (N) Equip A\t1\t44\t1\t\t\t\t\t\tweap36\t2\tarmo36\t1\tweap39\t6\tarmo39\t3\tweap42\t14\tarmo42\t7\tweap45\t6\tarmo45\t3\tAct 1 (N) Equip B\t1233\t\t\t42\t1275\t0.9671\t0\r\nAct 2 (N) Equip B\t1\t46\t1\t\t\t\t\t\tweap39\t2\tarmo39\t1\tweap42\t6\tarmo42\t3\tweap45\t14\tarmo45\t7\tweap48\t2\tarmo48\t1\tAct 2 (N) Equip A\t1105\t\t\t36\t1141\t0.9684\t0\r\nAct 2 (N) Equip C\t1\t48\t1\t\t\t\t\t\tweap39\t2\tarmo39\t1\tweap42\t6\tarmo42\t3\tweap45\t14\tarmo45\t7\tweap48\t6\tarmo48\t3\tAct 2 (N) Equip A\t1318\t\t\t42\t1360\t0.9691\t0\r\nAct 3 (N) Equip A\t1\t50\t1\t\t\t\t\t\tweap42\t2\tarmo42\t1\tweap45\t6\tarmo45\t3\tweap48\t14\tarmo48\t7\tweap51\t2\tarmo51\t1\tAct 2 (N) Equip B\t1190\t\t\t36\t1226\t0.9706\t0\r\nAct 3 (N) Equip B\t1\t52\t1\t\t\t\t\t\tweap42\t2\tarmo42\t1\tweap45\t6\tarmo45\t3\tweap48\t14\tarmo48\t7\tweap51\t6\tarmo51\t3\tAct 3 (N) Equip A\t1445\t\t\t42\t1487\t0.9718\t0\r\nAct 3 (N) Equip C\t1\t54\t1\t\t\t\t\t\tweap45\t2\tarmo45\t1\tweap48\t6\tarmo48\t3\tweap51\t14\tarmo51\t7\tweap54\t2\tarmo54\t1\tAct 3 (N) Equip A\t1275\t\t\t36\t1311\t0.9725\t0\r\nAct 4 (N) Equip A\t1\t56\t1\t\t\t\t\t\tweap45\t2\tarmo45\t1\tweap48\t6\tarmo48\t3\tweap51\t14\tarmo51\t7\tweap54\t6\tarmo54\t3\tAct 3 (N) Equip B\t1530\t\t\t42\t1572\t0.9733\t0\r\nAct 4 (N) Equip B\t1\t58\t1\t\t\t\t\t\tweap48\t2\tarmo48\t1\tweap51\t6\tarmo51\t3\tweap54\t14\tarmo54\t7\tweap57\t2\tarmo57\t1\tAct 4 (N) Equip A\t1360\t\t\t36\t1396\t0.9742\t0\r\nAct 4 (N) Equip C\t1\t60\t1\t\t\t\t\t\tweap48\t2\tarmo48\t1\tweap51\t6\tarmo51\t3\tweap54\t14\tarmo54\t7\tweap57\t6\tarmo57\t3\tAct 4 (N) Equip A\t1658\t\t\t42\t1700\t0.9753\t0\r\nAct 5 (N) Equip A\t1\t62\t1\t\t\t\t\t\tweap51\t2\tarmo51\t1\tweap54\t6\tarmo54\t3\tweap57\t14\tarmo57\t7\tweap60\t2\tarmo60\t1\tAct 4 (N) Equip B\t1445\t\t\t36\t1481\t0.9757\t0\r\nAct 5 (N) Equip B\t1\t64\t1\t\t\t\t\t\tweap51\t2\tarmo51\t1\tweap54\t6\tarmo54\t3\tweap57\t14\tarmo57\t7\tweap60\t6\tarmo60\t3\tAct 5 (N) Equip A\t1743\t\t\t42\t1785\t0.9765\t0\r\nAct 5 (N) Equip C\t1\t66\t1\t\t\t\t\t\tweap54\t2\tarmo54\t1\tweap57\t6\tarmo57\t3\tweap60\t14\tarmo60\t7\tweap63\t2\tarmo63\t1\tAct 5 (N) Equip B\t1530\t\t\t36\t1566\t0.9770\t0\r\nAct 1 (H) Equip A\t1\t68\t1\t\t\t\t\t\tweap54\t2\tarmo54\t1\tweap57\t6\tarmo57\t3\tweap60\t14\tarmo60\t7\tweap63\t6\tarmo63\t3\tAct 5 (N) Equip C\t1785\t\t\t42\t1827\t0.9770\t0\r\nAct 1 (H) Equip B\t1\t70\t1\t\t\t\t\t\tweap57\t2\tarmo57\t1\tweap60\t6\tarmo60\t3\tweap63\t14\tarmo63\t7\tweap66\t2\tarmo66\t1\tAct 1 (H) Equip A\t1530\t\t\t36\t1566\t0.9770\t0\r\nAct 1 (H) Equip C\t1\t71\t1\t\t\t\t\t\tweap57\t2\tarmo57\t1\tweap60\t6\tarmo60\t3\tweap63\t14\tarmo63\t7\tweap66\t6\tarmo66\t3\tAct 1 (H) Equip A\t1785\t\t\t42\t1827\t0.9770\t0\r\nAct 2 (H) Equip A\t1\t73\t1\t\t\t\t\t\tweap60\t2\tarmo60\t1\tweap63\t6\tarmo63\t3\tweap66\t14\tarmo66\t7\tweap69\t2\tarmo69\t1\tAct 1 (H) Equip B\t1530\t\t\t36\t1566\t0.9770\t0\r\nAct 2 (H) Equip B\t1\t75\t1\t\t\t\t\t\tweap60\t2\tarmo60\t1\tweap63\t6\tarmo63\t3\tweap66\t14\tarmo66\t7\tweap69\t6\tarmo69\t3\tAct 2 (H) Equip A\t1785\t\t\t42\t1827\t0.9770\t0\r\nAct 2 (H) Equip C\t1\t76\t1\t\t\t\t\t\tweap63\t2\tarmo63\t1\tweap66\t6\tarmo66\t3\tweap69\t14\tarmo69\t7\tweap72\t2\tarmo72\t1\tAct 2 (H) Equip A\t1530\t\t\t36\t1566\t0.9770\t0\r\nAct 3 (H) Equip A\t1\t77\t1\t\t\t\t\t\tweap63\t2\tarmo63\t1\tweap66\t6\tarmo66\t3\tweap69\t14\tarmo69\t7\tweap72\t6\tarmo72\t3\tAct 2 (H) Equip B\t1785\t\t\t42\t1827\t0.9770\t0\r\nAct 3 (H) Equip B\t1\t78\t1\t\t\t\t\t\tweap66\t2\tarmo66\t1\tweap69\t6\tarmo69\t3\tweap72\t14\tarmo72\t7\tweap75\t2\tarmo75\t1\tAct 3 (H) Equip A\t1530\t\t\t36\t1566\t0.9770\t0\r\nAct 3 (H) Equip C\t1\t79\t1\t\t\t\t\t\tweap66\t2\tarmo66\t1\tweap69\t6\tarmo69\t3\tweap72\t14\tarmo72\t7\tweap75\t6\tarmo75\t3\tAct 3 (H) Equip A\t1785\t\t\t42\t1827\t0.9770\t0\r\nAct 4 (H) Equip A\t1\t80\t1\t\t\t\t\t\tweap69\t2\tarmo69\t1\tweap72\t6\tarmo72\t3\tweap75\t14\tarmo75\t7\tweap78\t2\tarmo78\t1\tAct 3 (H) Equip B\t1530\t\t\t36\t1566\t0.9770\t0\r\nAct 4 (H) Equip B\t1\t81\t1\t\t\t\t\t\tweap69\t2\tarmo69\t1\tweap72\t6\tarmo72\t3\tweap75\t14\tarmo75\t7\tweap78\t6\tarmo78\t3\tAct 4 (H) Equip A\t1785\t\t\t42\t1827\t0.9770\t0\r\nAct 4 (H) Equip C\t1\t82\t1\t\t\t\t\t\tweap72\t2\tarmo72\t1\tweap75\t6\tarmo75\t3\tweap78\t14\tarmo78\t7\tweap81\t2\tarmo81\t1\tAct 4 (H) Equip A\t1530\t\t\t36\t1566\t0.9770\t0\r\nAct 5 (H) Equip A\t1\t83\t1\t\t\t\t\t\tweap75\t2\tarmo75\t1\tweap78\t6\tarmo78\t3\tweap81\t14\tarmo81\t7\tweap84\t2\tarmo84\t1\tAct 4 (H) Equip B\t1530\t\t\t36\t1566\t0.9770\t0\r\nAct 5 (H) Equip B\t1\t84\t1\t\t\t\t\t\tweap78\t2\tarmo78\t1\tweap81\t6\tarmo81\t3\tweap84\t14\tarmo84\t7\tweap87\t2\tarmo87\t1\tAct 5 (H) Equip A\t1530\t\t\t36\t1566\t0.9770\t0\r\nAct 5 (H) Equip C\t1\t85\t1\t\t\t\t\t\tweap81\t2\tarmo81\t1\tweap84\t6\tarmo84\t3\tweap87\t14\tarmo87\t7\tweap87\t1\tarmo87\t1\tAct 5 (H) Equip B\t1530\t\t\t35\t1565\t0.9776\t0\r\nAct 1 Melee A\t2\t0\t1\t\t\t\t\t\tmele3\t7\tarmo3\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Melee B\t2\t5\t1\t\t\t\t\t\tmele3\t3\tarmo3\t3\tmele6\t7\tarmo6\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Melee C\t2\t9\t1\t\t\t\t\t\tmele3\t2\tarmo3\t1\tmele6\t6\tarmo6\t3\tmele9\t14\tarmo9\t7\tmele12\t2\tarmo12\t1\t\t\t\t\t\t\t\t0\r\nAct 2 Melee A\t2\t12\t1\t\t\t\t\t\tmele6\t2\tarmo6\t1\tmele9\t6\tarmo9\t3\tmele12\t14\tarmo12\t7\tmele15\t2\tarmo15\t1\tAct 1 Melee C\t30\t\t\t36\t66\t0.4545\t0\r\nAct 2 Melee B\t2\t15\t1\t\t\t\t\t\tmele9\t2\tarmo9\t1\tmele12\t6\tarmo12\t3\tmele15\t14\tarmo15\t7\tmele18\t2\tarmo18\t1\tAct 2 Melee A\t60\t\t\t36\t96\t0.6250\t0\r\nAct 2 Melee C\t2\t18\t1\t\t\t\t\t\tmele12\t2\tarmo12\t1\tmele15\t6\tarmo15\t3\tmele18\t14\tarmo18\t7\tmele21\t2\tarmo21\t1\tAct 2 Melee B\t90\t\t\t36\t126\t0.7143\t0\r\nAct 3 Melee A\t2\t20\t1\t\t\t\t\t\tmele15\t2\tarmo15\t1\tmele18\t6\tarmo18\t3\tmele21\t14\tarmo21\t7\tmele24\t2\tarmo24\t1\tAct 2 Melee C\t120\t\t\t36\t156\t0.7692\t0\r\nAct 3 Melee B\t2\t22\t1\t\t\t\t\t\tmele18\t2\tarmo18\t1\tmele21\t6\tarmo21\t3\tmele24\t14\tarmo24\t7\tmele27\t2\tarmo27\t1\tAct 3 Melee A\t150\t\t\t36\t186\t0.8065\t0\r\nAct 3 Melee C\t2\t24\t1\t\t\t\t\t\tmele21\t2\tarmo21\t1\tmele24\t6\tarmo24\t3\tmele27\t14\tarmo27\t7\tmele30\t2\tarmo30\t1\tAct 3 Melee B\t180\t\t\t36\t216\t0.8333\t0\r\nAct 4 Melee A\t2\t26\t1\t\t\t\t\t\tmele24\t2\tarmo24\t1\tmele27\t6\tarmo27\t3\tmele30\t14\tarmo30\t4\tmele33\t2\tarmo33\t1\tAct 3 Melee C\t210\t\t\t33\t243\t0.8642\t0\r\nAct 4 Melee B\t2\t29\t1\t\t\t\t\t\tmele27\t2\tarmo27\t1\tmele30\t6\tarmo30\t3\tmele33\t14\tarmo33\t3\tmele36\t2\tarmo36\t1\tAct 4 Melee A\t260\t\t\t32\t292\t0.8904\t0\r\nAct 5 Melee A\t2\t32\t1\t\t\t\t\t\tmele27\t2\tarmo27\t1\tmele30\t6\tarmo30\t3\tmele33\t14\tarmo33\t3\tmele36\t7\tarmo36\t5\tAct 4 Melee B\t400\t\t\t41\t441\t0.9070\t0\r\nAct 5 Melee B\t2\t34\t1\t\t\t\t\t\tmele30\t2\tarmo30\t1\tmele33\t6\tarmo33\t2\tmele36\t14\tarmo36\t7\tmele39\t2\tarmo39\t1\tAct 5 Melee A\t489\t\t\t35\t524\t0.9332\t0\r\nAct 5 Melee C\t2\t36\t1\t\t\t\t\t\tmele30\t2\tarmo30\t1\tmele33\t6\tarmo33\t2\tmele36\t14\tarmo36\t7\tmele39\t2\tarmo39\t1\tAct 5 Melee B\t629\t\t\t35\t664\t0.9473\t0\r\nAct 1 (N) Melee A\t2\t38\t1\t\t\t\t\t\tweap33\t2\tarmo33\t1\tweap36\t6\tarmo36\t3\tweap39\t14\tarmo39\t7\tweap42\t2\tarmo42\t1\tAct 5 Melee C\t769\t\t\t36\t805\t0.9553\t0\r\nAct 1 (N) Melee B\t2\t40\t1\t\t\t\t\t\tweap36\t2\tarmo36\t1\tweap39\t6\tarmo39\t3\tweap42\t14\tarmo42\t7\tweap45\t2\tarmo45\t1\tAct 1 (N) Melee A\t935\t\t\t36\t971\t0.9629\t0\r\nAct 2 (N) Melee A\t2\t42\t1\t\t\t\t\t\tweap36\t2\tarmo36\t1\tweap39\t6\tarmo39\t3\tweap42\t14\tarmo42\t7\tweap45\t6\tarmo45\t3\tAct 1 (N) Melee B\t1148\t\t\t42\t1190\t0.9647\t0\r\nAct 2 (N) Melee B\t2\t44\t1\t\t\t\t\t\tweap39\t2\tarmo39\t1\tweap42\t6\tarmo42\t3\tweap45\t14\tarmo45\t7\tweap48\t2\tarmo48\t1\tAct 2 (N) Melee A\t1020\t\t\t36\t1056\t0.9659\t0\r\nAct 3 (N) Melee A\t2\t46\t1\t\t\t\t\t\tweap42\t2\tarmo42\t1\tweap45\t6\tarmo45\t3\tweap48\t14\tarmo48\t7\tweap51\t2\tarmo51\t1\tAct 2 (N) Melee B\t1063\t\t\t36\t1099\t0.9672\t0\r\nAct 3 (N) Melee B\t2\t48\t1\t\t\t\t\t\tweap45\t2\tarmo45\t1\tweap48\t6\tarmo48\t3\tweap51\t14\tarmo51\t7\tweap54\t2\tarmo54\t1\tAct 3 (N) Melee A\t1105\t\t\t36\t1141\t0.9684\t0\r\nAct 4 (N) Melee A\t2\t51\t1\t\t\t\t\t\tweap45\t2\tarmo45\t1\tweap48\t6\tarmo48\t3\tweap51\t14\tarmo51\t7\tweap54\t6\tarmo54\t3\tAct 3 (N) Melee B\t1339\t\t\t42\t1381\t0.9696\t0\r\nAct 4 (N) Melee B\t2\t54\t1\t\t\t\t\t\tweap48\t2\tarmo48\t1\tweap51\t6\tarmo51\t3\tweap54\t14\tarmo54\t7\tweap57\t2\tarmo57\t1\tAct 4 (N) Melee A\t1190\t\t\t36\t1226\t0.9706\t0\r\nAct 5 (N) Melee A\t2\t57\t1\t\t\t\t\t\tweap51\t2\tarmo51\t1\tweap54\t6\tarmo54\t3\tweap57\t14\tarmo57\t7\tweap60\t2\tarmo60\t1\tAct 4 (N) Melee B\t1275\t\t\t36\t1311\t0.9725\t0\r\nAct 5 (N) Melee B\t2\t60\t1\t\t\t\t\t\tweap51\t2\tarmo51\t1\tweap54\t6\tarmo54\t3\tweap57\t14\tarmo57\t7\tweap60\t6\tarmo60\t3\tAct 5 (N) Melee A\t1743\t\t\t42\t1785\t0.9765\t0\r\nAct 5 (N) Melee C\t2\t63\t1\t\t\t\t\t\tweap54\t2\tarmo54\t1\tweap57\t6\tarmo57\t3\tweap60\t14\tarmo60\t7\tweap63\t2\tarmo63\t1\tAct 5 (N) Melee B\t1530\t\t\t36\t1566\t0.9770\t0\r\nAct 1 (H) Melee A\t2\t66\t1\t\t\t\t\t\tweap54\t2\tarmo54\t1\tweap57\t6\tarmo57\t3\tweap60\t14\tarmo60\t7\tweap63\t6\tarmo63\t3\tAct 5 (N) Melee C\t1781\t\t\t42\t1823\t0.9770\t0\r\nAct 1 (H) Melee B\t2\t68\t1\t\t\t\t\t\tweap57\t2\tarmo57\t1\tweap60\t6\tarmo60\t3\tweap63\t14\tarmo63\t7\tweap66\t2\tarmo66\t1\tAct 1 (H) Melee A\t1530\t\t\t36\t1566\t0.9770\t0\r\nAct 2 (H) Melee A\t2\t71\t1\t\t\t\t\t\tweap60\t2\tarmo60\t1\tweap63\t6\tarmo63\t3\tweap66\t14\tarmo66\t7\tweap69\t2\tarmo69\t1\tAct 1 (H) Melee B\t1530\t\t\t36\t1566\t0.9770\t0\r\nAct 2 (H) Melee B\t2\t73\t1\t\t\t\t\t\tweap63\t2\tarmo63\t1\tweap66\t6\tarmo66\t3\tweap69\t14\tarmo69\t7\tweap72\t2\tarmo72\t1\tAct 2 (H) Melee A\t1530\t\t\t36\t1566\t0.9770\t0\r\nAct 3 (H) Melee A\t2\t75\t1\t\t\t\t\t\tweap63\t2\tarmo63\t1\tweap66\t6\tarmo66\t3\tweap69\t14\tarmo69\t7\tweap72\t6\tarmo72\t3\tAct 2 (H) Melee B\t1781\t\t\t42\t1823\t0.9770\t0\r\nAct 3 (H) Melee B\t2\t77\t1\t\t\t\t\t\tweap66\t2\tarmo66\t1\tweap69\t6\tarmo69\t3\tweap72\t14\tarmo72\t7\tweap75\t2\tarmo75\t1\tAct 3 (H) Melee A\t1530\t\t\t36\t1566\t0.9770\t0\r\nAct 4 (H) Melee A\t2\t79\t1\t\t\t\t\t\tweap69\t2\tarmo69\t1\tweap72\t6\tarmo72\t3\tweap75\t14\tarmo75\t7\tweap78\t2\tarmo78\t1\tAct 3 (H) Melee B\t1530\t\t\t36\t1566\t0.9770\t0\r\nAct 4 (H) Melee B\t2\t80\t1\t\t\t\t\t\tweap72\t2\tarmo72\t1\tweap75\t6\tarmo75\t3\tweap78\t14\tarmo78\t7\tweap81\t2\tarmo81\t1\tAct 4 (H) Melee A\t1530\t\t\t36\t1566\t0.9770\t0\r\nAct 5 (H) Melee A\t2\t81\t1\t\t\t\t\t\tweap75\t2\tarmo75\t1\tweap78\t6\tarmo78\t3\tweap81\t14\tarmo81\t7\tweap84\t2\tarmo84\t1\tAct 4 (H) Melee B\t1530\t\t\t36\t1566\t0.9770\t0\r\nAct 5 (H) Melee B\t2\t83\t1\t\t\t\t\t\tweap78\t2\tarmo78\t1\tweap81\t6\tarmo81\t3\tweap84\t14\tarmo84\t7\tweap87\t2\tarmo87\t1\tAct 5 (H) Melee A\t1530\t\t\t36\t1566\t0.9770\t0\r\nAct 5 (H) Melee C\t2\t85\t1\t\t\t\t\t\tweap81\t2\tarmo81\t1\tweap84\t6\tarmo84\t3\tweap87\t14\tarmo87\t7\tweap87\t1\tarmo87\t1\tAct 5 (H) Melee B\t1530\t\t\t35\t1565\t0.9776\t0\r\nAct 1 Bow A\t3\t0\t1\t\t\t\t\t\tbow3\t36\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Bow B\t3\t5\t1\t\t\t\t\t\tbow3\t20\tbow6\t16\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Bow C\t3\t9\t1\t\t\t\t\t\tbow3\t10\tbow6\t16\tbow9\t10\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Bow A\t3\t12\t1\t\t\t\t\t\tbow3\t3\tbow6\t15\tbow9\t15\tbow12\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Bow B\t3\t15\t1\t\t\t\t\t\tbow3\t1\tbow6\t7\tbow9\t20\tbow12\t7\tbow15\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Bow C\t3\t18\t1\t\t\t\t\t\tbow3\t1\tbow6\t3\tbow9\t14\tbow12\t14\tbow15\t3\tbow18\t1\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Bow A\t3\t20\t1\t\t\t\t\t\tbow3\t1\tbow6\t3\tbow9\t6\tbow12\t12\tbow15\t6\tbow18\t3\tbow21\t1\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Bow B\t3\t22\t1\t\t\t\t\t\tbow3\t1\tbow6\t3\tbow9\t6\tbow12\t8\tbow15\t8\tbow18\t6\tbow21\t3\tbow24\t1\t\t\t\t\t\t\t\t0\r\nAct 3 Bow C\t3\t24\t1\t\t\t\t\t\tbow3\t1\tbow6\t2\tbow9\t5\tbow12\t7\tbow15\t8\tbow18\t7\tbow21\t4\tbow24\t2\t\t\t\t\t\t\t\t0\r\nAct 4 Bow A\t3\t26\t1\t\t\t\t\t\tbow6\t1\tbow9\t3\tbow12\t6\tbow15\t8\tbow18\t8\tbow21\t6\tbow24\t3\tbow27\t1\tAct 3 Bow C\t110\t\t\t36\t146\t0.7534\t0\r\nAct 4 Bow B\t3\t29\t1\t\t\t\t\t\tbow6\t1\tbow9\t2\tbow12\t5\tbow15\t7\tbow18\t8\tbow21\t7\tbow24\t4\tbow27\t2\tAct 4 Bow A\t180\t\t\t36\t216\t0.8333\t0\r\nAct 5 Bow A\t3\t32\t1\t\t\t\t\t\tbow9\t1\tbow12\t3\tbow15\t6\tbow18\t8\tbow21\t8\tbow24\t6\tbow27\t3\tbow30\t1\tAct 4 Bow B\t270\t\t\t36\t306\t0.8824\t0\r\nAct 5 Bow B\t3\t34\t1\t\t\t\t\t\tbow9\t1\tbow12\t2\tbow15\t5\tbow18\t7\tbow21\t8\tbow24\t7\tbow27\t4\tbow30\t2\tAct 5 Bow A\t380\t\t\t36\t416\t0.9135\t0\r\nAct 5 Bow C\t3\t36\t1\t\t\t\t\t\tbow9\t1\tbow12\t2\tbow15\t5\tbow18\t7\tbow21\t8\tbow24\t7\tbow27\t4\tbow30\t2\tAct 5 Bow B\t500\t\t\t36\t536\t0.9328\t0\r\nAct 1 (N) Bow A\t3\t38\t1\t\t\t\t\t\tbow12\t1\tbow15\t2\tbow18\t5\tbow21\t7\tbow24\t8\tbow27\t7\tbow30\t4\tbow33\t2\tAct 5 Bow C\t630\t\t\t36\t666\t0.9459\t0\r\nAct 1 (N) Bow B\t3\t40\t1\t\t\t\t\t\tbow15\t1\tbow18\t2\tbow21\t5\tbow24\t7\tbow27\t8\tbow30\t7\tbow33\t4\tbow36\t2\tAct 1 (N) Bow A\t770\t\t\t36\t806\t0.9553\t0\r\nAct 2 (N) Bow A\t3\t42\t1\t\t\t\t\t\tbow18\t1\tbow21\t2\tbow24\t5\tbow27\t7\tbow30\t8\tbow33\t7\tbow36\t4\tbow39\t2\tAct 1 (N) Bow B\t920\t\t\t36\t956\t0.9623\t0\r\nAct 2 (N) Bow B\t3\t44\t1\t\t\t\t\t\tbow21\t1\tbow24\t2\tbow27\t5\tbow30\t7\tbow33\t8\tbow36\t7\tbow39\t4\tbow42\t2\tAct 2 (N) Bow A\t1070\t\t\t36\t1106\t0.9675\t0\r\nAct 3 (N) Bow A\t3\t46\t1\t\t\t\t\t\tbow24\t1\tbow27\t2\tbow30\t5\tbow33\t7\tbow36\t8\tbow39\t7\tbow42\t4\tbow45\t2\tAct 2 (N) Bow B\t1220\t\t\t36\t1256\t0.9713\t0\r\nAct 3 (N) Bow B\t3\t48\t1\t\t\t\t\t\tbow27\t1\tbow30\t2\tbow33\t5\tbow36\t7\tbow39\t8\tbow42\t7\tbow45\t4\tbow48\t2\tAct 3 (N) Bow A\t1370\t\t\t36\t1406\t0.9744\t0\r\nAct 4 (N) Bow A\t3\t51\t1\t\t\t\t\t\tbow30\t1\tbow33\t2\tbow36\t5\tbow39\t7\tbow42\t8\tbow45\t7\tbow48\t4\tbow51\t2\tAct 3 (N) Bow B\t1520\t\t\t36\t1556\t0.9769\t0\r\nAct 4 (N) Bow B\t3\t54\t1\t\t\t\t\t\tbow33\t1\tbow36\t2\tbow39\t5\tbow42\t7\tbow45\t8\tbow48\t7\tbow51\t4\tbow54\t2\tAct 4 (N) Bow A\t1670\t\t\t36\t1706\t0.9789\t0\r\nAct 5 (N) Bow A\t3\t57\t1\t\t\t\t\t\tbow36\t1\tbow39\t2\tbow42\t5\tbow45\t7\tbow48\t8\tbow51\t7\tbow54\t4\tbow57\t2\tAct 4 (N) Bow B\t1820\t\t\t36\t1856\t0.9806\t0\r\nAct 5 (N) Bow B\t3\t60\t1\t\t\t\t\t\tbow39\t1\tbow42\t2\tbow45\t5\tbow48\t7\tbow51\t8\tbow54\t7\tbow57\t4\tbow60\t2\tAct 5 (N) Bow A\t1970\t\t\t36\t2006\t0.9821\t0\r\nAct 5 (N) Bow C\t3\t63\t1\t\t\t\t\t\tbow42\t1\tbow45\t2\tbow48\t5\tbow51\t7\tbow54\t8\tbow57\t7\tbow60\t4\tbow63\t2\tAct 5 (N) Bow B\t2120\t\t\t36\t2156\t0.9833\t0\r\nAct 1 (H) Bow A\t3\t66\t1\t\t\t\t\t\tbow45\t1\tbow48\t2\tbow51\t5\tbow54\t7\tbow57\t8\tbow60\t7\tbow63\t4\tbow66\t2\tAct 5 (N) Bow C\t2270\t\t\t36\t2306\t0.9844\t0\r\nAct 1 (H) Bow B\t3\t69\t1\t\t\t\t\t\tbow48\t1\tbow51\t2\tbow54\t5\tbow57\t7\tbow60\t8\tbow63\t7\tbow66\t4\tbow69\t2\tAct 1 (H) Bow A\t2270\t\t\t36\t2306\t0.9844\t0\r\nAct 2 (H) Bow A\t3\t71\t1\t\t\t\t\t\tbow51\t1\tbow54\t2\tbow57\t5\tbow60\t7\tbow63\t8\tbow66\t7\tbow69\t4\tbow72\t2\tAct 1 (H) Bow B\t2270\t\t\t36\t2306\t0.9844\t0\r\nAct 2 (H) Bow B\t3\t73\t1\t\t\t\t\t\tbow54\t1\tbow57\t2\tbow60\t5\tbow63\t7\tbow66\t8\tbow69\t7\tbow72\t4\tbow75\t2\tAct 2 (H) Bow A\t2270\t\t\t36\t2306\t0.9844\t0\r\nAct 3 (H) Bow A\t3\t75\t1\t\t\t\t\t\tbow57\t1\tbow60\t2\tbow63\t5\tbow66\t7\tbow69\t8\tbow72\t7\tbow75\t4\tbow78\t2\tAct 2 (H) Bow B\t2270\t\t\t36\t2306\t0.9844\t0\r\nAct 3 (H) Bow B\t3\t77\t1\t\t\t\t\t\tbow60\t1\tbow63\t2\tbow66\t5\tbow69\t7\tbow72\t8\tbow75\t7\tbow78\t4\tbow81\t2\tAct 3 (H) Bow A\t2270\t\t\t36\t2306\t0.9844\t0\r\nAct 4 (H) Bow A\t3\t79\t1\t\t\t\t\t\tbow63\t1\tbow66\t2\tbow69\t5\tbow72\t7\tbow75\t8\tbow78\t7\tbow81\t4\tbow84\t2\tAct 3 (H) Bow B\t2270\t\t\t36\t2306\t0.9844\t0\r\nAct 4 (H) Bow B\t3\t80\t1\t\t\t\t\t\tbow66\t1\tbow69\t2\tbow72\t5\tbow75\t7\tbow78\t8\tbow81\t7\tbow84\t2\tbow84\t1\tAct 4 (H) Bow A\t2080\t\t\t33\t2113\t0.9844\t0\r\nAct 5 (H) Bow A\t3\t82\t1\t\t\t\t\t\tbow69\t1\tbow72\t2\tbow75\t5\tbow78\t7\tbow81\t8\tbow84\t7\tbow87\t2\tbow87\t1\tAct 4 (H) Bow B\t2080\t\t\t33\t2113\t0.9844\t0\r\nAct 5 (H) Bow B\t3\t83\t1\t\t\t\t\t\tbow72\t1\tbow75\t2\tbow78\t5\tbow81\t7\tbow84\t8\tbow87\t7\tbow87\t2\tbow87\t1\tAct 5 (H) Bow A\t2080\t\t\t33\t2113\t0.9844\t0\r\nAct 5 (H) Bow C\t3\t85\t1\t\t\t\t\t\tbow75\t1\tbow78\t2\tbow81\t5\tbow84\t7\tbow87\t8\tbow87\t1\tbow87\t1\tbow87\t1\tAct 5 (H) Bow B\t1655\t\t\t26\t1681\t0.9845\t0\r\nAct 1 Magic A\t4\t0\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp1\t6\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Magic B\t4\t5\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp1\t4\tmp2\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Magic C\t4\t9\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp1\t2\tmp2\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Magic A\t4\t12\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp1\t1\tmp2\t4\tmp3\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Magic B\t4\t15\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp1\t1\tmp2\t3\tmp3\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Magic C\t4\t18\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp2\t2\tmp3\t3\tmp4\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Magic A\t4\t20\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp3\t4\tmp4\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Magic B\t4\t22\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp3\t3\tmp4\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Magic C\t4\t24\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp3\t2\tmp4\t3\tmp5\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Magic A\t4\t26\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp3\t1\tmp4\t4\tmp5\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Magic B\t4\t29\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t4\tmp5\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Magic A\t4\t32\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t3\tmp5\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Magic B\t4\t34\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Magic C\t4\t36\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Magic A\t4\t38\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Magic B\t4\t40\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Magic A\t4\t42\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Magic B\t4\t44\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Magic A\t4\t46\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Magic B\t4\t48\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Magic A\t4\t51\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Magic B\t4\t54\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Magic A\t4\t57\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Magic B\t4\t60\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Magic C\t4\t63\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Magic A\t4\t66\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Magic B\t4\t69\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Magic A\t4\t71\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Magic B\t4\t73\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Magic A\t4\t75\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Magic B\t4\t77\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Magic A\t4\t79\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Magic B\t4\t80\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Magic A\t4\t82\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Magic B\t4\t83\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Magic C\t4\t85\t1\t\t\t\t\t\ttsc\t2\tisc\t4\tmp4\t2\tmp5\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Good\t5\t0\t1\t\t\t\t\t\tJewelry A\t5\tChipped Gem\t5\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Good\t5\t12\t1\t\t\t\t\t\tJewelry B\t20\tChipped Gem\t12\tFlawed Gem\t4\tRunes 1\t3\tRunes 2\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Good\t5\t18\t1\t\t\t\t\t\tJewelry C\t20\tChipped Gem\t8\tFlawed Gem\t8\tRunes 3\t3\tRunes 4\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Good\t5\t26\t1\t\t\t\t\t\tJewelry C\t50\tChipped Gem\t14\tFlawed Gem\t28\tNormal Gem\t42\tRunes 5\t14\tRunes 6\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Good\t5\t32\t1\t\t\t\t\t\tJewelry C\t50\tChipped Gem\t6\tFlawed Gem\t8\tNormal Gem\t24\tFlawless Gem\t4\tRunes 7\t10\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Good\t5\t38\t1\t\t\t\t\t\tJewelry C\t60\tChipped Gem\t6\tFlawed Gem\t8\tNormal Gem\t28\tFlawless Gem\t14\tRunes 8\t14\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Good\t5\t42\t1\t\t\t\t\t\tJewelry C\t60\tChipped Gem\t4\tFlawed Gem\t10\tNormal Gem\t14\tFlawless Gem\t28\tRunes 9\t14\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Good\t5\t46\t1\t\t\t\t\t\tJewelry C\t60\tChipped Gem\t4\tFlawed Gem\t10\tNormal Gem\t14\tFlawless Gem\t28\tRunes 10\t14\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Good\t5\t51\t1\t\t\t\t\t\tJewelry C\t60\tChipped Gem\t4\tFlawed Gem\t10\tNormal Gem\t14\tFlawless Gem\t28\tRunes 11\t14\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Good\t5\t57\t1\t\t\t\t\t\tJewelry C\t60\tChipped Gem\t4\tFlawed Gem\t10\tNormal Gem\t14\tFlawless Gem\t28\tRunes 12\t14\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Good\t5\t66\t1\t\t\t\t\t\tJewelry C\t60\tChipped Gem\t4\tFlawed Gem\t10\tNormal Gem\t14\tFlawless Gem\t28\tRunes 13\t14\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Good\t5\t72\t1\t\t\t\t\t\tJewelry C\t60\tChipped Gem\t4\tFlawed Gem\t10\tNormal Gem\t14\tFlawless Gem\t28\tRunes 14\t14\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Good\t5\t78\t1\t\t\t\t\t\tJewelry C\t60\tChipped Gem\t4\tFlawed Gem\t10\tNormal Gem\t14\tFlawless Gem\t28\tRunes 15\t14\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Good\t5\t82\t1\t\t\t\t\t\tJewelry C\t60\tChipped Gem\t4\tFlawed Gem\t10\tNormal Gem\t14\tFlawless Gem\t28\tRunes 16\t14\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Good\t5\t85\t1\t\t\t\t\t\tJewelry C\t60\tChipped Gem\t4\tFlawed Gem\t10\tNormal Gem\t14\tFlawless Gem\t28\tRunes 17\t14\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Chest A\t6\t0\t4\t\t\t\t\t100\tgld\t15\tAct 1 Junk\t15\tAct 1 Equip A\t10\tAct 1 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Chest B\t6\t5\t4\t\t\t\t\t100\tgld\t15\tAct 1 Junk\t15\tAct 1 Equip B\t10\tAct 1 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Chest C\t6\t9\t4\t\t\t\t\t100\tgld\t15\tAct 1 Junk\t12\tAct 2 Junk\t3\tAct 1 Equip C\t15\tAct 1 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Chest A\t6\t12\t4\t\t\t\t\t100\tgld\t15\tAct 2 Junk\t15\tAct 2 Equip A\t10\tAct 2 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Chest B\t6\t15\t4\t\t\t\t\t100\tgld\t15\tAct 2 Junk\t15\tAct 2 Equip B\t10\tAct 2 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Chest C\t6\t18\t4\t\t\t\t\t100\tgld\t15\tAct 2 Junk\t12\tAct 3 Junk\t3\tAct 2 Equip C\t15\tAct 2 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Chest A\t6\t20\t4\t\t\t\t\t100\tgld\t15\tAct 3 Junk\t15\tAct 3 Equip A\t10\tAct 3 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Chest B\t6\t22\t4\t\t\t\t\t100\tgld\t15\tAct 3 Junk\t15\tAct 3 Equip B\t10\tAct 3 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Chest C\t6\t24\t4\t\t\t\t\t100\tgld\t15\tAct 3 Junk\t15\tAct 3 Equip C\t10\tAct 3 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Chest A\t6\t26\t4\t\t\t\t\t100\tgld\t15\tAct 4 Junk\t15\tAct 4 Equip A\t10\tAct 4 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Chest B\t6\t28\t4\t\t\t\t\t100\tgld\t15\tAct 4 Junk\t15\tAct 4 Equip B\t10\tAct 4 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Chest C\t6\t30\t4\t\t\t\t\t100\tgld\t15\tAct 4 Junk\t15\tAct 4 Equip B\t10\tAct 4 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Chest A\t6\t32\t4\t\t\t\t\t100\tgld\t15\tAct 5 Junk\t15\tAct 5 Equip A\t10\tAct 5 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Chest B\t6\t34\t4\t\t\t\t\t100\tgld\t15\tAct 5 Junk\t15\tAct 5 Equip B\t10\tAct 5 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Chest C\t6\t36\t4\t\t\t\t\t100\tgld\t15\tAct 5 Junk\t15\tAct 5 Equip B\t10\tAct 5 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Chest A\t6\t38\t4\t\t\t\t\t100\tgld\t15\tAct 1 (N) Junk\t15\tAct 1 (N) Equip A\t10\tAct 1 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Chest B\t6\t40\t4\t\t\t\t\t100\tgld\t15\tAct 1 (N) Junk\t15\tAct 1 (N) Equip B\t10\tAct 1 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Chest C\t6\t42\t4\t\t\t\t\t100\tgld\t15\tAct 1 (N) Junk\t15\tAct 1 (N) Equip B\t10\tAct 1 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Chest A\t6\t44\t4\t\t\t\t\t100\tgld\t15\tAct 2 (N) Junk\t15\tAct 2 (N) Equip A\t10\tAct 2 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Chest B\t6\t46\t4\t\t\t\t\t100\tgld\t15\tAct 2 (N) Junk\t15\tAct 2 (N) Equip B\t10\tAct 2 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Chest C\t6\t48\t4\t\t\t\t\t100\tgld\t15\tAct 2 (N) Junk\t15\tAct 2 (N) Equip B\t10\tAct 2 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Chest A\t6\t50\t4\t\t\t\t\t100\tgld\t15\tAct 3 (N) Junk\t15\tAct 3 (N) Equip A\t10\tAct 3 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Chest B\t6\t52\t4\t\t\t\t\t100\tgld\t15\tAct 3 (N) Junk\t15\tAct 3 (N) Equip B\t10\tAct 3 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Chest C\t6\t54\t4\t\t\t\t\t100\tgld\t15\tAct 3 (N) Junk\t15\tAct 3 (N) Equip B\t10\tAct 3 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Chest A\t6\t56\t4\t\t\t\t\t100\tgld\t15\tAct 4 (N) Junk\t15\tAct 4 (N) Equip A\t10\tAct 4 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Chest B\t6\t58\t4\t\t\t\t\t100\tgld\t15\tAct 4 (N) Junk\t15\tAct 4 (N) Equip B\t10\tAct 4 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Chest C\t6\t60\t4\t\t\t\t\t100\tgld\t15\tAct 4 (N) Junk\t15\tAct 4 (N) Equip C\t10\tAct 4 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Chest A\t6\t62\t4\t\t\t\t\t100\tgld\t15\tAct 5 (N) Junk\t15\tAct 5 (N) Equip A\t10\tAct 5 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Chest B\t6\t64\t4\t\t\t\t\t100\tgld\t15\tAct 5 (N) Junk\t15\tAct 5 (N) Equip B\t10\tAct 5 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Chest C\t6\t66\t4\t\t\t\t\t100\tgld\t15\tAct 5 (N) Junk\t15\tAct 5 (N) Equip B\t10\tAct 5 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Chest A\t6\t68\t4\t\t\t\t\t100\tgld\t15\tAct 1 (H) Junk\t15\tAct 1 (H) Equip A\t10\tAct 1 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Chest B\t6\t70\t4\t\t\t\t\t100\tgld\t15\tAct 1 (H) Junk\t15\tAct 1 (H) Equip B\t10\tAct 1 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Chest C\t6\t71\t4\t\t\t\t\t100\tgld\t15\tAct 1 (H) Junk\t15\tAct 1 (H) Equip B\t10\tAct 1 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Chest A\t6\t72\t4\t\t\t\t\t100\tgld\t15\tAct 2 (H) Junk\t15\tAct 2 (H) Equip A\t10\tAct 2 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Chest B\t6\t74\t4\t\t\t\t\t100\tgld\t15\tAct 2 (H) Junk\t15\tAct 2 (H) Equip B\t10\tAct 2 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Chest C\t6\t75\t4\t\t\t\t\t100\tgld\t15\tAct 2 (H) Junk\t15\tAct 2 (H) Equip B\t10\tAct 2 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Chest A\t6\t76\t4\t\t\t\t\t100\tgld\t15\tAct 3 (H) Junk\t15\tAct 3 (H) Equip A\t10\tAct 3 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Chest B\t6\t78\t4\t\t\t\t\t100\tgld\t15\tAct 3 (H) Junk\t15\tAct 3 (H) Equip B\t10\tAct 3 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Chest C\t6\t79\t4\t\t\t\t\t100\tgld\t15\tAct 3 (H) Junk\t15\tAct 3 (H) Equip B\t10\tAct 3 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Chest A\t6\t80\t4\t\t\t\t\t100\tgld\t15\tAct 4 (H) Junk\t15\tAct 4 (H) Equip A\t10\tAct 4 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Chest B\t6\t81\t4\t\t\t\t\t100\tgld\t15\tAct 4 (H) Junk\t15\tAct 4 (H) Equip B\t10\tAct 4 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Chest C\t6\t82\t4\t\t\t\t\t100\tgld\t15\tAct 4 (H) Junk\t15\tAct 4 (H) Equip B\t10\tAct 4 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Chest A\t6\t83\t4\t\t\t\t\t100\tgld\t15\tAct 5 (H) Junk\t15\tAct 5 (H) Equip A\t10\tAct 5 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Chest B\t6\t84\t4\t\t\t\t\t100\tgld\t15\tAct 5 (H) Junk\t15\tAct 5 (H) Equip B\t10\tAct 5 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Chest C\t6\t85\t4\t\t\t\t\t100\tgld\t15\tAct 5 (H) Junk\t15\tAct 5 (H) Equip B\t10\tAct 5 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 H2H A\t7\t0\t1\t\t\t\t\t100\tgld\t21\tAct 1 Equip A\t16\tAct 1 Junk\t21\tAct 1 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 H2H B\t7\t5\t1\t\t\t\t\t100\tgld\t21\tAct 1 Equip B\t16\tAct 1 Junk\t21\tAct 1 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 H2H C\t7\t9\t1\t\t\t\t\t100\tgld\t21\tAct 1 Equip C\t16\tAct 1 Junk\t21\tAct 1 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 H2H A\t7\t12\t1\t\t\t\t\t100\tgld\t21\tAct 2 Equip A\t16\tAct 2 Junk\t21\tAct 2 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 H2H B\t7\t15\t1\t\t\t\t\t100\tgld\t21\tAct 2 Equip B\t16\tAct 2 Junk\t21\tAct 2 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 H2H C\t7\t18\t1\t\t\t\t\t100\tgld\t21\tAct 2 Equip C\t16\tAct 2 Junk\t21\tAct 3 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 H2H A\t7\t20\t1\t\t\t\t\t100\tgld\t21\tAct 3 Equip A\t16\tAct 3 Junk\t21\tAct 3 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 H2H B\t7\t22\t1\t\t\t\t\t100\tgld\t21\tAct 3 Equip B\t16\tAct 3 Junk\t21\tAct 3 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 H2H C\t7\t24\t1\t\t\t\t\t100\tgld\t21\tAct 3 Equip C\t16\tAct 3 Junk\t21\tAct 3 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 H2H A\t7\t26\t1\t\t\t\t\t100\tgld\t21\tAct 4 Equip A\t16\tAct 4 Junk\t21\tAct 4 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 H2H B\t7\t29\t1\t\t\t\t\t100\tgld\t21\tAct 4 Equip B\t16\tAct 4 Junk\t21\tAct 4 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 H2H A\t7\t32\t1\t\t\t\t\t100\tgld\t21\tAct 5 Equip A\t16\tAct 5 Junk\t21\tAct 5 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 H2H B\t7\t34\t1\t\t\t\t\t100\tgld\t21\tAct 5 Equip B\t16\tAct 5 Junk\t21\tAct 5 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 H2H C\t7\t36\t1\t\t\t\t\t100\tgld\t21\tAct 5 Equip C\t16\tAct 5 Junk\t21\tAct 5 Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) H2H A\t7\t38\t1\t\t\t\t\t100\tgld\t21\tAct 1 (N) Equip A\t16\tAct 1 (N) Junk\t21\tAct 1 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) H2H B\t7\t40\t1\t\t\t\t\t100\tgld\t21\tAct 1 (N) Equip B\t16\tAct 1 (N) Junk\t21\tAct 1 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) H2H C\t7\t41\t1\t\t\t\t\t100\tgld\t21\tAct 1 (N) Equip B\t16\tAct 1 (N) Junk\t21\tAct 1 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) H2H A\t7\t42\t1\t\t\t\t\t100\tgld\t21\tAct 2 (N) Equip A\t16\tAct 2 (N) Junk\t21\tAct 2 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) H2H B\t7\t44\t1\t\t\t\t\t100\tgld\t21\tAct 2 (N) Equip B\t16\tAct 2 (N) Junk\t21\tAct 2 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) H2H C\t7\t45\t1\t\t\t\t\t100\tgld\t21\tAct 2 (N) Equip B\t16\tAct 2 (N) Junk\t21\tAct 2 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) H2H A\t7\t46\t1\t\t\t\t\t100\tgld\t21\tAct 3 (N) Equip A\t16\tAct 3 (N) Junk\t21\tAct 3 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) H2H B\t7\t48\t1\t\t\t\t\t100\tgld\t21\tAct 3 (N) Equip B\t16\tAct 3 (N) Junk\t21\tAct 3 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) H2H C\t7\t50\t1\t\t\t\t\t100\tgld\t21\tAct 3 (N) Equip B\t16\tAct 3 (N) Junk\t21\tAct 3 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) H2H A\t7\t51\t1\t\t\t\t\t100\tgld\t21\tAct 4 (N) Equip A\t16\tAct 4 (N) Junk\t21\tAct 4 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) H2H B\t7\t54\t1\t\t\t\t\t100\tgld\t21\tAct 4 (N) Equip B\t16\tAct 4 (N) Junk\t21\tAct 4 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) H2H A\t7\t57\t1\t\t\t\t\t100\tgld\t21\tAct 5 (N) Equip A\t16\tAct 5 (N) Junk\t21\tAct 5 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) H2H B\t7\t60\t1\t\t\t\t\t100\tgld\t21\tAct 5 (N) Equip B\t16\tAct 5 (N) Junk\t21\tAct 5 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) H2H C\t7\t62\t1\t\t\t\t\t100\tgld\t21\tAct 5 (N) Equip C\t16\tAct 5 (N) Junk\t21\tAct 5 (N) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) H2H A\t7\t63\t1\t\t\t\t\t100\tgld\t21\tAct 1 (H) Equip A\t16\tAct 1 (H) Junk\t21\tAct 1 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) H2H B\t7\t66\t1\t\t\t\t\t100\tgld\t21\tAct 1 (H) Equip B\t16\tAct 1 (H) Junk\t21\tAct 1 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) H2H C\t7\t68\t1\t\t\t\t\t100\tgld\t21\tAct 1 (H) Equip B\t16\tAct 1 (H) Junk\t21\tAct 1 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) H2H A\t7\t69\t1\t\t\t\t\t100\tgld\t21\tAct 2 (H) Equip A\t16\tAct 1 (H) Junk\t21\tAct 2 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) H2H B\t7\t71\t1\t\t\t\t\t100\tgld\t21\tAct 2 (H) Equip B\t16\tAct 1 (H) Junk\t21\tAct 2 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) H2H C\t7\t73\t1\t\t\t\t\t100\tgld\t21\tAct 2 (H) Equip B\t16\tAct 1 (H) Junk\t21\tAct 2 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) H2H A\t7\t74\t1\t\t\t\t\t100\tgld\t21\tAct 3 (H) Equip A\t16\tAct 1 (H) Junk\t21\tAct 3 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) H2H B\t7\t76\t1\t\t\t\t\t100\tgld\t21\tAct 3 (H) Equip B\t16\tAct 1 (H) Junk\t21\tAct 3 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) H2H C\t7\t77\t1\t\t\t\t\t100\tgld\t21\tAct 3 (H) Equip B\t16\tAct 1 (H) Junk\t21\tAct 3 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) H2H A\t7\t79\t1\t\t\t\t\t100\tgld\t21\tAct 4 (H) Equip A\t16\tAct 1 (H) Junk\t21\tAct 4 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) H2H B\t7\t80\t1\t\t\t\t\t100\tgld\t21\tAct 4 (H) Equip B\t16\tAct 1 (H) Junk\t21\tAct 4 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) H2H A\t7\t82\t1\t\t\t\t\t100\tgld\t21\tAct 5 (H) Equip A\t16\tAct 1 (H) Junk\t21\tAct 5 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) H2H B\t7\t83\t1\t\t\t\t\t100\tgld\t21\tAct 5 (H) Equip B\t16\tAct 1 (H) Junk\t21\tAct 5 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) H2H C\t7\t85\t1\t\t\t\t\t100\tgld\t21\tAct 5 (H) Equip C\t16\tAct 1 (H) Junk\t21\tAct 5 (H) Good\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Cast A\t8\t0\t1\t\t\t\t\t100\tgld\t18\tAct 1 Equip A\t15\tAct 1 Junk\t18\tAct 1 Good\t2\tAct 1 Magic A\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Cast B\t8\t5\t1\t\t\t\t\t100\tgld\t18\tAct 1 Equip B\t15\tAct 1 Junk\t18\tAct 1 Good\t2\tAct 1 Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Cast C\t8\t9\t1\t\t\t\t\t100\tgld\t18\tAct 1 Equip C\t15\tAct 1 Junk\t18\tAct 1 Good\t2\tAct 1 Magic C\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Cast A\t8\t12\t1\t\t\t\t\t100\tgld\t18\tAct 2 Equip A\t15\tAct 2 Junk\t18\tAct 2 Good\t2\tAct 2 Magic A\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Cast B\t8\t15\t1\t\t\t\t\t100\tgld\t18\tAct 2 Equip B\t15\tAct 2 Junk\t18\tAct 2 Good\t2\tAct 2 Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Cast C\t8\t18\t1\t\t\t\t\t100\tgld\t18\tAct 2 Equip C\t15\tAct 2 Junk\t18\tAct 3 Good\t2\tAct 2 Magic C\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Cast A\t8\t20\t1\t\t\t\t\t100\tgld\t18\tAct 3 Equip A\t15\tAct 3 Junk\t18\tAct 3 Good\t2\tAct 3 Magic A\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Cast B\t8\t22\t1\t\t\t\t\t100\tgld\t18\tAct 3 Equip B\t15\tAct 3 Junk\t18\tAct 3 Good\t2\tAct 3 Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Cast C\t8\t24\t1\t\t\t\t\t100\tgld\t18\tAct 3 Equip C\t15\tAct 3 Junk\t18\tAct 3 Good\t2\tAct 3 Magic C\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Cast A\t8\t26\t1\t\t\t\t\t100\tgld\t18\tAct 4 Equip A\t15\tAct 4 Junk\t18\tAct 4 Good\t2\tAct 4 Magic A\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Cast B\t8\t29\t1\t\t\t\t\t100\tgld\t18\tAct 4 Equip B\t15\tAct 4 Junk\t18\tAct 4 Good\t2\tAct 4 Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Cast A\t8\t32\t1\t\t\t\t\t100\tgld\t18\tAct 5 Equip A\t15\tAct 5 Junk\t18\tAct 5 Good\t2\tAct 5 Magic A\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Cast B\t8\t34\t1\t\t\t\t\t100\tgld\t18\tAct 5 Equip B\t15\tAct 5 Junk\t18\tAct 5 Good\t2\tAct 5 Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Cast C\t8\t36\t1\t\t\t\t\t100\tgld\t18\tAct 5 Equip C\t15\tAct 5 Junk\t18\tAct 5 Good\t2\tAct 5 Magic C\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Cast A\t8\t38\t1\t\t\t\t\t100\tgld\t18\tAct 1 (N) Equip A\t15\tAct 1 (N) Junk\t18\tAct 1 (N) Good\t2\tAct 1 (N) Magic A\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Cast B\t8\t40\t1\t\t\t\t\t100\tgld\t18\tAct 1 (N) Equip B\t15\tAct 1 (N) Junk\t18\tAct 1 (N) Good\t2\tAct 1 (N) Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Cast C\t8\t41\t1\t\t\t\t\t100\tgld\t18\tAct 1 (N) Equip B\t15\tAct 1 (N) Junk\t18\tAct 1 (N) Good\t2\tAct 1 (N) Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Cast A\t8\t42\t1\t\t\t\t\t100\tgld\t18\tAct 2 (N) Equip A\t15\tAct 2 (N) Junk\t18\tAct 2 (N) Good\t2\tAct 2 (N) Magic A\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Cast B\t8\t44\t1\t\t\t\t\t100\tgld\t18\tAct 2 (N) Equip B\t15\tAct 2 (N) Junk\t18\tAct 2 (N) Good\t2\tAct 2 (N) Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Cast C\t8\t45\t1\t\t\t\t\t100\tgld\t18\tAct 2 (N) Equip B\t15\tAct 2 (N) Junk\t18\tAct 2 (N) Good\t2\tAct 2 (N) Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Cast A\t8\t46\t1\t\t\t\t\t100\tgld\t18\tAct 3 (N) Equip A\t15\tAct 3 (N) Junk\t18\tAct 3 (N) Good\t2\tAct 3 (N) Magic A\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Cast B\t8\t48\t1\t\t\t\t\t100\tgld\t18\tAct 3 (N) Equip B\t15\tAct 3 (N) Junk\t18\tAct 3 (N) Good\t2\tAct 3 (N) Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Cast C\t8\t50\t1\t\t\t\t\t100\tgld\t18\tAct 3 (N) Equip B\t15\tAct 3 (N) Junk\t18\tAct 3 (N) Good\t2\tAct 3 (N) Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Cast A\t8\t51\t1\t\t\t\t\t100\tgld\t18\tAct 4 (N) Equip A\t15\tAct 4 (N) Junk\t18\tAct 4 (N) Good\t2\tAct 4 (N) Magic A\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Cast B\t8\t54\t1\t\t\t\t\t100\tgld\t18\tAct 4 (N) Equip B\t15\tAct 4 (N) Junk\t18\tAct 4 (N) Good\t2\tAct 4 (N) Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Cast A\t8\t57\t1\t\t\t\t\t100\tgld\t18\tAct 5 (N) Equip A\t15\tAct 5 (N) Junk\t18\tAct 5 (N) Good\t2\tAct 5 (N) Magic A\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Cast B\t8\t60\t1\t\t\t\t\t100\tgld\t18\tAct 5 (N) Equip B\t15\tAct 5 (N) Junk\t18\tAct 5 (N) Good\t2\tAct 5 (N) Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Cast C\t8\t62\t1\t\t\t\t\t100\tgld\t18\tAct 5 (N) Equip C\t15\tAct 5 (N) Junk\t18\tAct 5 (N) Good\t2\tAct 5 (N) Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Cast A\t8\t63\t1\t\t\t\t\t100\tgld\t18\tAct 1 (H) Equip A\t15\tAct 1 (H) Junk\t18\tAct 1 (H) Good\t2\tAct 1 (H) Magic A\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Cast B\t8\t66\t1\t\t\t\t\t100\tgld\t18\tAct 1 (H) Equip B\t15\tAct 1 (H) Junk\t18\tAct 1 (H) Good\t2\tAct 1 (H) Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Cast C\t8\t68\t1\t\t\t\t\t100\tgld\t18\tAct 1 (H) Equip B\t15\tAct 1 (H) Junk\t18\tAct 1 (H) Good\t2\tAct 1 (H) Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Cast A\t8\t69\t1\t\t\t\t\t100\tgld\t18\tAct 2 (H) Equip A\t15\tAct 1 (H) Junk\t18\tAct 2 (H) Good\t2\tAct 2 (H) Magic A\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Cast B\t8\t71\t1\t\t\t\t\t100\tgld\t18\tAct 2 (H) Equip B\t15\tAct 1 (H) Junk\t18\tAct 2 (H) Good\t2\tAct 2 (H) Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Cast C\t8\t72\t1\t\t\t\t\t100\tgld\t18\tAct 2 (H) Equip B\t15\tAct 1 (H) Junk\t18\tAct 2 (H) Good\t2\tAct 2 (H) Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Cast A\t8\t74\t1\t\t\t\t\t100\tgld\t18\tAct 3 (H) Equip A\t15\tAct 1 (H) Junk\t18\tAct 3 (H) Good\t2\tAct 3 (H) Magic A\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Cast B\t8\t75\t1\t\t\t\t\t100\tgld\t18\tAct 3 (H) Equip B\t15\tAct 1 (H) Junk\t18\tAct 3 (H) Good\t2\tAct 3 (H) Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Cast C\t8\t77\t1\t\t\t\t\t100\tgld\t18\tAct 3 (H) Equip B\t15\tAct 1 (H) Junk\t18\tAct 3 (H) Good\t2\tAct 3 (H) Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Cast A\t8\t78\t1\t\t\t\t\t100\tgld\t18\tAct 4 (H) Equip A\t15\tAct 1 (H) Junk\t18\tAct 4 (H) Good\t2\tAct 4 (H) Magic A\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Cast B\t8\t80\t1\t\t\t\t\t100\tgld\t18\tAct 4 (H) Equip B\t15\tAct 1 (H) Junk\t18\tAct 4 (H) Good\t2\tAct 4 (H) Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Cast A\t8\t81\t1\t\t\t\t\t100\tgld\t18\tAct 5 (H) Equip A\t15\tAct 1 (H) Junk\t18\tAct 5 (H) Good\t2\tAct 5 (H) Magic A\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Cast B\t8\t83\t1\t\t\t\t\t100\tgld\t18\tAct 5 (H) Equip B\t15\tAct 1 (H) Junk\t18\tAct 5 (H) Good\t2\tAct 5 (H) Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Cast C\t8\t85\t1\t\t\t\t\t100\tgld\t18\tAct 5 (H) Equip C\t15\tAct 1 (H) Junk\t18\tAct 5 (H) Good\t2\tAct 5 (H) Magic B\t7\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Miss A\t9\t0\t1\t\t\t\t\t100\tgld\t18\tAct 1 Equip A\t13\tAct 1 Junk\t18\tAct 1 Good\t2\tAct 1 Bow A\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Miss B\t9\t5\t1\t\t\t\t\t100\tgld\t18\tAct 1 Equip B\t13\tAct 1 Junk\t18\tAct 1 Good\t2\tAct 1 Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Miss C\t9\t9\t1\t\t\t\t\t100\tgld\t18\tAct 1 Equip C\t13\tAct 1 Junk\t18\tAct 1 Good\t2\tAct 1 Bow C\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Miss A\t9\t12\t1\t\t\t\t\t100\tgld\t18\tAct 2 Equip A\t13\tAct 2 Junk\t18\tAct 2 Good\t2\tAct 2 Bow A\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Miss B\t9\t15\t1\t\t\t\t\t100\tgld\t18\tAct 2 Equip B\t13\tAct 2 Junk\t18\tAct 2 Good\t2\tAct 2 Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Miss C\t9\t18\t1\t\t\t\t\t100\tgld\t18\tAct 2 Equip C\t13\tAct 2 Junk\t18\tAct 3 Good\t2\tAct 2 Bow C\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Miss A\t9\t20\t1\t\t\t\t\t100\tgld\t18\tAct 3 Equip A\t13\tAct 3 Junk\t18\tAct 3 Good\t2\tAct 3 Bow A\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Miss B\t9\t22\t1\t\t\t\t\t100\tgld\t18\tAct 3 Equip B\t13\tAct 3 Junk\t18\tAct 3 Good\t2\tAct 3 Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Miss C\t9\t24\t1\t\t\t\t\t100\tgld\t18\tAct 3 Equip C\t13\tAct 3 Junk\t18\tAct 3 Good\t2\tAct 3 Bow C\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Miss A\t9\t26\t1\t\t\t\t\t100\tgld\t18\tAct 4 Equip A\t13\tAct 4 Junk\t18\tAct 4 Good\t2\tAct 4 Bow A\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Miss B\t9\t29\t1\t\t\t\t\t100\tgld\t18\tAct 4 Equip B\t13\tAct 4 Junk\t18\tAct 4 Good\t2\tAct 4 Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Miss A\t9\t32\t1\t\t\t\t\t100\tgld\t18\tAct 5 Equip A\t13\tAct 5 Junk\t18\tAct 5 Good\t2\tAct 5 Bow A\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Miss B\t9\t34\t1\t\t\t\t\t100\tgld\t18\tAct 5 Equip B\t13\tAct 5 Junk\t18\tAct 5 Good\t2\tAct 5 Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Miss C\t9\t36\t1\t\t\t\t\t100\tgld\t18\tAct 5 Equip C\t13\tAct 5 Junk\t18\tAct 5 Good\t2\tAct 5 Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Miss A\t9\t38\t1\t\t\t\t\t100\tgld\t18\tAct 1 (N) Equip A\t13\tAct 1 (N) Junk\t18\tAct 1 (N) Good\t2\tAct 1 (N) Bow A\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Miss B\t9\t40\t1\t\t\t\t\t100\tgld\t18\tAct 1 (N) Equip B\t13\tAct 1 (N) Junk\t18\tAct 1 (N) Good\t2\tAct 1 (N) Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Miss C\t9\t41\t1\t\t\t\t\t100\tgld\t18\tAct 1 (N) Equip B\t13\tAct 1 (N) Junk\t18\tAct 1 (N) Good\t2\tAct 1 (N) Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Miss A\t9\t42\t1\t\t\t\t\t100\tgld\t18\tAct 2 (N) Equip A\t13\tAct 2 (N) Junk\t18\tAct 2 (N) Good\t2\tAct 2 (N) Bow A\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Miss B\t9\t44\t1\t\t\t\t\t100\tgld\t18\tAct 2 (N) Equip B\t13\tAct 2 (N) Junk\t18\tAct 2 (N) Good\t2\tAct 2 (N) Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Miss C\t9\t45\t1\t\t\t\t\t100\tgld\t18\tAct 2 (N) Equip B\t13\tAct 2 (N) Junk\t18\tAct 2 (N) Good\t2\tAct 2 (N) Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Miss A\t9\t46\t1\t\t\t\t\t100\tgld\t18\tAct 3 (N) Equip A\t13\tAct 3 (N) Junk\t18\tAct 3 (N) Good\t2\tAct 3 (N) Bow A\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Miss B\t9\t48\t1\t\t\t\t\t100\tgld\t18\tAct 3 (N) Equip B\t13\tAct 3 (N) Junk\t18\tAct 3 (N) Good\t2\tAct 3 (N) Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Miss C\t9\t50\t1\t\t\t\t\t100\tgld\t18\tAct 3 (N) Equip B\t13\tAct 3 (N) Junk\t18\tAct 3 (N) Good\t2\tAct 3 (N) Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Miss A\t9\t51\t1\t\t\t\t\t100\tgld\t18\tAct 4 (N) Equip A\t13\tAct 4 (N) Junk\t18\tAct 4 (N) Good\t2\tAct 4 (N) Bow A\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Miss B\t9\t54\t1\t\t\t\t\t100\tgld\t18\tAct 4 (N) Equip B\t13\tAct 4 (N) Junk\t18\tAct 4 (N) Good\t2\tAct 4 (N) Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Miss A\t9\t57\t1\t\t\t\t\t100\tgld\t18\tAct 5 (N) Equip A\t13\tAct 5 (N) Junk\t18\tAct 5 (N) Good\t2\tAct 5 (N) Bow A\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Miss B\t9\t60\t1\t\t\t\t\t100\tgld\t18\tAct 5 (N) Equip B\t13\tAct 5 (N) Junk\t18\tAct 5 (N) Good\t2\tAct 5 (N) Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Miss C\t9\t62\t1\t\t\t\t\t100\tgld\t18\tAct 5 (N) Equip C\t13\tAct 5 (N) Junk\t18\tAct 5 (N) Good\t2\tAct 5 (N) Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Miss A\t9\t63\t1\t\t\t\t\t100\tgld\t18\tAct 1 (H) Equip A\t13\tAct 1 (H) Junk\t18\tAct 1 (H) Good\t2\tAct 1 (H) Bow A\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Miss B\t9\t66\t1\t\t\t\t\t100\tgld\t18\tAct 1 (H) Equip B\t13\tAct 1 (H) Junk\t18\tAct 1 (H) Good\t2\tAct 1 (H) Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Miss C\t9\t68\t1\t\t\t\t\t100\tgld\t18\tAct 1 (H) Equip B\t13\tAct 1 (H) Junk\t18\tAct 1 (H) Good\t2\tAct 1 (H) Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Miss A\t9\t69\t1\t\t\t\t\t100\tgld\t18\tAct 2 (H) Equip A\t13\tAct 1 (H) Junk\t18\tAct 2 (H) Good\t2\tAct 2 (H) Bow A\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Miss B\t9\t71\t1\t\t\t\t\t100\tgld\t18\tAct 2 (H) Equip B\t13\tAct 1 (H) Junk\t18\tAct 2 (H) Good\t2\tAct 2 (H) Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Miss C\t9\t72\t1\t\t\t\t\t100\tgld\t18\tAct 2 (H) Equip B\t13\tAct 1 (H) Junk\t18\tAct 2 (H) Good\t2\tAct 2 (H) Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Miss A\t9\t74\t1\t\t\t\t\t100\tgld\t18\tAct 3 (H) Equip A\t13\tAct 1 (H) Junk\t18\tAct 3 (H) Good\t2\tAct 3 (H) Bow A\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Miss B\t9\t75\t1\t\t\t\t\t100\tgld\t18\tAct 3 (H) Equip B\t13\tAct 1 (H) Junk\t18\tAct 3 (H) Good\t2\tAct 3 (H) Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Miss C\t9\t77\t1\t\t\t\t\t100\tgld\t18\tAct 3 (H) Equip B\t13\tAct 1 (H) Junk\t18\tAct 3 (H) Good\t2\tAct 3 (H) Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Miss A\t9\t78\t1\t\t\t\t\t100\tgld\t18\tAct 4 (H) Equip A\t13\tAct 1 (H) Junk\t18\tAct 4 (H) Good\t2\tAct 4 (H) Bow A\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Miss B\t9\t80\t1\t\t\t\t\t100\tgld\t18\tAct 4 (H) Equip B\t13\tAct 1 (H) Junk\t18\tAct 4 (H) Good\t2\tAct 4 (H) Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Miss A\t9\t81\t1\t\t\t\t\t100\tgld\t18\tAct 5 (H) Equip A\t13\tAct 1 (H) Junk\t18\tAct 5 (H) Good\t2\tAct 5 (H) Bow A\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Miss B\t9\t83\t1\t\t\t\t\t100\tgld\t18\tAct 5 (H) Equip B\t13\tAct 1 (H) Junk\t18\tAct 5 (H) Good\t2\tAct 5 (H) Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Miss C\t9\t85\t1\t\t\t\t\t100\tgld\t18\tAct 5 (H) Equip C\t13\tAct 1 (H) Junk\t18\tAct 5 (H) Good\t2\tAct 5 (H) Bow B\t6\tAmmo\t3\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Wraith A\t10\t0\t1\t\t\t\t\t75\tAct 1 Good\t4\tAct 1 Magic A\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Wraith B\t10\t5\t1\t\t\t\t\t75\tAct 1 Good\t4\tAct 1 Magic B\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Wraith C\t10\t9\t1\t\t\t\t\t75\tAct 1 Good\t4\tAct 1 Magic C\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Wraith A\t10\t12\t1\t\t\t\t\t75\tAct 2 Good\t4\tAct 2 Magic A\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Wraith B\t10\t15\t1\t\t\t\t\t75\tAct 2 Good\t4\tAct 2 Magic B\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Wraith C\t10\t18\t1\t\t\t\t\t75\tAct 3 Good\t4\tAct 2 Magic C\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Wraith A\t10\t20\t1\t\t\t\t\t75\tAct 3 Good\t4\tAct 3 Magic A\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Wraith B\t10\t22\t1\t\t\t\t\t75\tAct 3 Good\t4\tAct 3 Magic B\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Wraith C\t10\t24\t1\t\t\t\t\t75\tAct 3 Good\t4\tAct 3 Magic C\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Wraith A\t10\t26\t1\t\t\t\t\t75\tAct 4 Good\t4\tAct 4 Magic A\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Wraith B\t10\t29\t1\t\t\t\t\t75\tAct 4 Good\t4\tAct 4 Magic B\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Wraith A\t10\t32\t1\t\t\t\t\t75\tAct 5 Good\t4\tAct 5 Magic A\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Wraith B\t10\t34\t1\t\t\t\t\t75\tAct 5 Good\t4\tAct 5 Magic B\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Wraith C\t10\t36\t1\t\t\t\t\t75\tAct 5 Good\t4\tAct 5 Magic C\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Wraith A\t10\t38\t1\t\t\t\t\t75\tAct 1 (N) Good\t4\tAct 1 (N) Magic A\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Wraith B\t10\t40\t1\t\t\t\t\t75\tAct 1 (N) Good\t4\tAct 1 (N) Magic B\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Wraith C\t10\t41\t1\t\t\t\t\t75\tAct 1 (N) Good\t4\tAct 1 (N) Magic B\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Wraith A\t10\t42\t1\t\t\t\t\t75\tAct 2 (N) Good\t4\tAct 2 (N) Magic A\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Wraith B\t10\t44\t1\t\t\t\t\t75\tAct 2 (N) Good\t4\tAct 2 (N) Magic B\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Wraith C\t10\t45\t1\t\t\t\t\t75\tAct 2 (N) Good\t4\tAct 2 (N) Magic B\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Wraith A\t10\t46\t1\t\t\t\t\t75\tAct 3 (N) Good\t4\tAct 3 (N) Magic A\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Wraith B\t10\t48\t1\t\t\t\t\t75\tAct 3 (N) Good\t4\tAct 3 (N) Magic B\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Wraith C\t10\t50\t1\t\t\t\t\t75\tAct 3 (N) Good\t4\tAct 3 (N) Magic B\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Wraith A\t10\t51\t1\t\t\t\t\t75\tAct 4 (N) Good\t4\tAct 4 (N) Magic A\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Wraith B\t10\t54\t1\t\t\t\t\t75\tAct 4 (N) Good\t4\tAct 4 (N) Magic B\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Wraith A\t10\t57\t1\t\t\t\t\t75\tAct 5 (N) Good\t4\tAct 5 (N) Magic A\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Wraith B\t10\t60\t1\t\t\t\t\t75\tAct 5 (N) Good\t4\tAct 5 (N) Magic B\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Wraith C\t10\t62\t1\t\t\t\t\t75\tAct 5 (N) Good\t4\tAct 5 (N) Magic C\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Wraith A\t10\t63\t1\t\t\t\t\t75\tAct 1 (H) Good\t4\tAct 1 (H) Magic A\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Wraith B\t10\t66\t1\t\t\t\t\t75\tAct 1 (H) Good\t4\tAct 1 (H) Magic B\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Wraith C\t10\t68\t1\t\t\t\t\t75\tAct 1 (H) Good\t4\tAct 1 (H) Magic B\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Wraith A\t10\t69\t1\t\t\t\t\t75\tAct 2 (H) Good\t4\tAct 2 (H) Magic A\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Wraith B\t10\t71\t1\t\t\t\t\t75\tAct 2 (H) Good\t4\tAct 2 (H) Magic B\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Wraith C\t10\t72\t1\t\t\t\t\t75\tAct 2 (H) Good\t4\tAct 2 (H) Magic B\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Wraith A\t10\t74\t1\t\t\t\t\t75\tAct 3 (H) Good\t4\tAct 3 (H) Magic A\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Wraith B\t10\t75\t1\t\t\t\t\t75\tAct 3 (H) Good\t4\tAct 3 (H) Magic B\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Wraith C\t10\t77\t1\t\t\t\t\t75\tAct 3 (H) Good\t4\tAct 3 (H) Magic B\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Wraith A\t10\t78\t1\t\t\t\t\t75\tAct 4 (H) Good\t4\tAct 4 (H) Magic A\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Wraith B\t10\t80\t1\t\t\t\t\t75\tAct 4 (H) Good\t4\tAct 4 (H) Magic B\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Wraith A\t10\t81\t1\t\t\t\t\t75\tAct 5 (H) Good\t4\tAct 5 (H) Magic A\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Wraith B\t10\t83\t1\t\t\t\t\t75\tAct 5 (H) Good\t4\tAct 5 (H) Magic B\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Wraith C\t10\t85\t1\t\t\t\t\t75\tAct 5 (H) Good\t4\tAct 5 (H) Magic C\t25\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Citem A\t11\t0\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 1 Equip A\t37\tAct 1 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Citem B\t11\t5\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 1 Equip B\t37\tAct 1 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Citem C\t11\t9\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 1 Equip C\t37\tAct 1 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Citem A\t11\t12\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 2 Equip A\t37\tAct 2 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Citem B\t11\t15\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 2 Equip B\t37\tAct 2 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Citem C\t11\t18\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 2 Equip C\t37\tAct 3 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Citem A\t11\t20\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 3 Equip A\t37\tAct 3 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Citem B\t11\t22\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 3 Equip B\t37\tAct 3 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Citem C\t11\t24\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 3 Equip C\t37\tAct 3 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Citem A\t11\t26\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 4 Equip A\t37\tAct 4 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Citem B\t11\t29\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 4 Equip B\t37\tAct 4 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Citem A\t11\t32\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 5 Equip A\t37\tAct 5 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Citem B\t11\t34\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 5 Equip B\t37\tAct 5 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Citem C\t11\t36\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 5 Equip C\t37\tAct 5 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Citem A\t11\t38\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 1 (N) Equip A\t37\tAct 1 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Citem B\t11\t40\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 1 (N) Equip B\t37\tAct 1 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Citem C\t11\t41\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 1 (N) Equip B\t37\tAct 1 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Citem A\t11\t42\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 2 (N) Equip A\t37\tAct 2 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Citem B\t11\t44\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 2 (N) Equip B\t37\tAct 2 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Citem C\t11\t45\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 2 (N) Equip B\t37\tAct 2 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Citem A\t11\t46\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 3 (N) Equip A\t37\tAct 3 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Citem B\t11\t48\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 3 (N) Equip B\t37\tAct 3 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Citem C\t11\t50\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 3 (N) Equip B\t37\tAct 3 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Citem A\t11\t51\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 4 (N) Equip A\t37\tAct 4 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Citem B\t11\t54\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 4 (N) Equip B\t37\tAct 4 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Citem A\t11\t57\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 5 (N) Equip A\t37\tAct 5 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Citem B\t11\t60\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 5 (N) Equip B\t37\tAct 5 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Citem C\t11\t62\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 5 (N) Equip C\t37\tAct 5 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Citem A\t11\t63\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 1 (H) Equip A\t37\tAct 1 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Citem B\t11\t66\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 1 (H) Equip B\t37\tAct 1 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Citem C\t11\t68\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 1 (H) Equip B\t37\tAct 1 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Citem A\t11\t69\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 2 (H) Equip A\t37\tAct 2 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Citem B\t11\t71\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 2 (H) Equip B\t37\tAct 2 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Citem C\t11\t72\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 2 (H) Equip B\t37\tAct 2 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Citem A\t11\t74\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 3 (H) Equip A\t37\tAct 3 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Citem B\t11\t75\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 3 (H) Equip B\t37\tAct 3 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Citem C\t11\t77\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 3 (H) Equip B\t37\tAct 3 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Citem A\t11\t78\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 4 (H) Equip A\t37\tAct 4 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Citem B\t11\t80\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 4 (H) Equip B\t37\tAct 4 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Citem A\t11\t81\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 5 (H) Equip A\t37\tAct 5 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Citem B\t11\t83\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 5 (H) Equip B\t37\tAct 5 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Citem C\t11\t85\t1\t800\t800\t800\t1024\t0\t\"gld,mul=1280\"\t60\tAct 5 (H) Equip C\t37\tAct 5 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Cpot A\t12\t0\t2\t\t\t\t\t0\tHpotion 1\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Cpot B\t12\t5\t2\t\t\t\t\t0\tHpotion 1\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Cpot C\t12\t9\t2\t\t\t\t\t0\tHpotion 1\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Cpot A\t12\t12\t2\t\t\t\t\t0\tHpotion 2\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Cpot B\t12\t15\t2\t\t\t\t\t0\tHpotion 2\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Cpot C\t12\t18\t2\t\t\t\t\t0\tHpotion 2\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Cpot A\t12\t20\t2\t\t\t\t\t0\tHpotion 3\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Cpot B\t12\t22\t2\t\t\t\t\t0\tHpotion 3\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Cpot C\t12\t24\t2\t\t\t\t\t0\tHpotion 3\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Cpot A\t12\t26\t2\t\t\t\t\t0\tHpotion 3\t1\tHpotion 4\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Cpot B\t12\t29\t2\t\t\t\t\t0\tHpotion 3\t1\tHpotion 4\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Cpot A\t12\t32\t2\t\t\t\t\t0\tHpotion 4\t1\tHpotion 5\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Cpot B\t12\t34\t2\t\t\t\t\t0\tHpotion 4\t1\tHpotion 5\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Cpot C\t12\t36\t2\t\t\t\t\t0\tHpotion 4\t1\tHpotion 5\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Cpot A\t12\t38\t2\t\t\t\t\t0\tHpotion 4\t1\tHpotion 5\t2\tHpotion 6\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Cpot B\t12\t40\t2\t\t\t\t\t0\tHpotion 4\t1\tHpotion 5\t2\tHpotion 6\t4\t\t\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Cpot C\t12\t41\t2\t\t\t\t\t0\tHpotion 4\t1\tHpotion 5\t2\tHpotion 6\t4\t\t\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Cpot A\t12\t42\t2\t\t\t\t\t0\tHpotion 4\t1\tHpotion 5\t2\tHpotion 6\t4\t\t\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Cpot B\t12\t44\t2\t\t\t\t\t0\tHpotion 4\t1\tHpotion 5\t2\tHpotion 6\t4\t\t\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Cpot C\t12\t45\t2\t\t\t\t\t0\tHpotion 4\t1\tHpotion 5\t2\tHpotion 6\t4\t\t\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Cpot A\t12\t46\t2\t\t\t\t\t0\trvs\t1\tHpotion 4\t2\tHpotion 5\t4\tHpotion 6\t8\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Cpot B\t12\t48\t2\t\t\t\t\t0\trvs\t1\tHpotion 4\t2\tHpotion 5\t4\tHpotion 6\t8\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Cpot C\t12\t50\t2\t\t\t\t\t0\trvs\t1\tHpotion 4\t2\tHpotion 5\t4\tHpotion 6\t8\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Cpot A\t12\t51\t2\t\t\t\t\t0\trvs\t1\tHpotion 4\t2\tHpotion 5\t4\tHpotion 6\t8\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Cpot B\t12\t54\t2\t\t\t\t\t0\trvs\t1\tHpotion 4\t2\tHpotion 5\t4\tHpotion 6\t8\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Cpot A\t12\t57\t2\t\t\t\t\t0\trvs\t1\tHpotion 4\t2\tHpotion 5\t4\tHpotion 6\t8\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Cpot B\t12\t60\t2\t\t\t\t\t0\trvs\t1\tHpotion 4\t2\tHpotion 5\t4\tHpotion 6\t8\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Cpot C\t12\t62\t2\t\t\t\t\t0\trvs\t1\tHpotion 4\t2\tHpotion 5\t4\tHpotion 6\t8\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Cpot A\t12\t63\t2\t\t\t\t\t0\trvl\t1\tHpotion 4\t2\tHpotion 5\t4\tHpotion 6\t8\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Cpot B\t12\t66\t2\t\t\t\t\t0\trvl\t1\tHpotion 4\t2\tHpotion 5\t4\tHpotion 6\t8\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Cpot C\t12\t68\t2\t\t\t\t\t0\trvl\t1\tHpotion 4\t2\tHpotion 5\t4\tHpotion 6\t8\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Cpot A\t12\t69\t2\t\t\t\t\t0\trvl\t1\tHpotion 4\t2\tHpotion 5\t4\tHpotion 6\t8\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Cpot B\t12\t71\t2\t\t\t\t\t0\trvl\t1\tHpotion 4\t2\tHpotion 5\t4\tHpotion 6\t8\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Cpot C\t12\t72\t2\t\t\t\t\t0\trvl\t1\tHpotion 4\t2\tHpotion 5\t4\tHpotion 6\t8\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Cpot A\t12\t74\t2\t\t\t\t\t0\trvl\t1\tHpotion 4\t2\tHpotion 5\t4\tHpotion 6\t8\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Cpot B\t12\t75\t2\t\t\t\t\t0\trvl\t1\tHpotion 4\t2\tHpotion 5\t4\tHpotion 6\t8\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Cpot C\t12\t77\t2\t\t\t\t\t0\trvl\t1\tHpotion 4\t2\tHpotion 5\t4\tHpotion 6\t8\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Cpot A\t12\t78\t2\t\t\t\t\t0\trvl\t1\tHpotion 4\t2\tHpotion 5\t4\tHpotion 6\t8\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Cpot B\t12\t80\t2\t\t\t\t\t0\trvl\t1\tHpotion 4\t2\tHpotion 5\t4\tHpotion 6\t8\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Cpot A\t12\t81\t2\t\t\t\t\t0\trvl\t1\tHpotion 4\t2\tHpotion 5\t4\tHpotion 6\t8\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Cpot B\t12\t83\t2\t\t\t\t\t0\trvl\t1\tHpotion 4\t2\tHpotion 5\t4\tHpotion 6\t8\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Cpot C\t12\t85\t2\t\t\t\t\t0\trvl\t1\tHpotion 4\t2\tHpotion 5\t4\tHpotion 6\t8\t\t\t\t7\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Champ A\t13\t0\t-2\t\t\t\t\t0\tAct 1 Citem A\t1\tAct 1 Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Champ B\t13\t5\t-2\t\t\t\t\t0\tAct 1 Citem B\t1\tAct 1 Cpot B\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Champ C\t13\t9\t-2\t\t\t\t\t0\tAct 1 Citem C\t1\tAct 1 Cpot C\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Champ A\t13\t12\t-2\t\t\t\t\t0\tAct 2 Citem A\t1\tAct 2 Cpot A\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Champ B\t13\t15\t-2\t\t\t\t\t0\tAct 2 Citem B\t1\tAct 2 Cpot B\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Champ C\t13\t18\t-2\t\t\t\t\t0\tAct 2 Citem C\t1\tAct 2 Cpot C\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Champ A\t13\t20\t-2\t\t\t\t\t0\tAct 3 Citem A\t1\tAct 3 Cpot A\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Champ B\t13\t22\t-2\t\t\t\t\t0\tAct 3 Citem B\t1\tAct 3 Cpot B\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Champ C\t13\t24\t-2\t\t\t\t\t0\tAct 3 Citem C\t1\tAct 3 Cpot C\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Champ A\t13\t26\t-2\t\t\t\t\t0\tAct 4 Citem A\t1\tAct 4 Cpot A\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Champ B\t13\t29\t-2\t\t\t\t\t0\tAct 4 Citem B\t1\tAct 4 Cpot B\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Champ A\t13\t32\t-2\t\t\t\t\t0\tAct 5 Citem A\t1\tAct 5 Cpot A\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Champ B\t13\t34\t-2\t\t\t\t\t0\tAct 5 Citem B\t1\tAct 5 Cpot B\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Champ C\t13\t36\t-2\t\t\t\t\t0\tAct 5 Citem C\t1\tAct 5 Cpot C\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Champ A\t13\t38\t-2\t\t\t\t\t0\tAct 1 (N) Citem A\t1\tAct 1 (N) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Champ B\t13\t40\t-2\t\t\t\t\t0\tAct 1 (N) Citem B\t1\tAct 1 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Champ C\t13\t41\t-2\t\t\t\t\t0\tAct 1 (N) Citem C\t1\tAct 1 (N) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Champ A\t13\t42\t-2\t\t\t\t\t0\tAct 2 (N) Citem A\t1\tAct 2 (N) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Champ B\t13\t44\t-2\t\t\t\t\t0\tAct 2 (N) Citem B\t1\tAct 2 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Champ C\t13\t45\t-2\t\t\t\t\t0\tAct 2 (N) Citem C\t1\tAct 2 (N) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Champ A\t13\t46\t-2\t\t\t\t\t0\tAct 3 (N) Citem A\t1\tAct 3 (N) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Champ B\t13\t48\t-2\t\t\t\t\t0\tAct 3 (N) Citem B\t1\tAct 3 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Champ C\t13\t50\t-2\t\t\t\t\t0\tAct 3 (N) Citem C\t1\tAct 3 (N) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Champ A\t13\t51\t-2\t\t\t\t\t0\tAct 4 (N) Citem A\t1\tAct 4 (N) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Champ B\t13\t54\t-2\t\t\t\t\t0\tAct 4 (N) Citem B\t1\tAct 4 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Champ C\t13\t56\t-2\t\t\t\t\t0\tAct 4 (N) Citem B\t1\tAct 4 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\nAct 5 (N) Champ A\t13\t57\t-2\t\t\t\t\t0\tAct 5 (N) Citem A\t1\tAct 5 (N) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Champ B\t13\t60\t-2\t\t\t\t\t0\tAct 5 (N) Citem B\t1\tAct 5 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Champ C\t13\t62\t-2\t\t\t\t\t0\tAct 5 (N) Citem C\t1\tAct 5 (N) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\nAct 1 (H) Champ A\t13\t63\t-2\t\t\t\t\t0\tAct 1 (H) Citem A\t1\tAct 1 (H) Cpot A\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Champ B\t13\t66\t-2\t\t\t\t\t0\tAct 1 (H) Citem B\t1\tAct 1 (H) Cpot B\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Champ C\t13\t68\t-2\t\t\t\t\t0\tAct 1 (H) Citem C\t1\tAct 1 (H) Cpot C\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Champ A\t13\t69\t-2\t\t\t\t\t0\tAct 2 (H) Citem A\t1\tAct 2 (H) Cpot A\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Champ B\t13\t71\t-2\t\t\t\t\t0\tAct 2 (H) Citem B\t1\tAct 2 (H) Cpot B\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Champ C\t13\t72\t-2\t\t\t\t\t0\tAct 2 (H) Citem C\t1\tAct 2 (H) Cpot C\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Champ A\t13\t74\t-2\t\t\t\t\t0\tAct 3 (H) Citem A\t1\tAct 3 (H) Cpot A\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Champ B\t13\t75\t-2\t\t\t\t\t0\tAct 3 (H) Citem B\t1\tAct 3 (H) Cpot B\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Champ C\t13\t77\t-2\t\t\t\t\t0\tAct 3 (H) Citem C\t1\tAct 3 (H) Cpot C\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Champ A\t13\t78\t-2\t\t\t\t\t0\tAct 4 (H) Citem A\t1\tAct 4 (H) Cpot A\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Champ B\t13\t80\t-2\t\t\t\t\t0\tAct 4 (H) Citem B\t1\tAct 4 (H) Cpot B\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Champ A\t13\t81\t-2\t\t\t\t\t0\tAct 5 (H) Citem A\t1\tAct 5 (H) Cpot A\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Champ B\t13\t83\t-2\t\t\t\t\t0\tAct 5 (H) Citem B\t1\tAct 5 (H) Cpot B\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Champ C\t13\t85\t-2\t\t\t\t\t0\tAct 5 (H) Citem C\t1\tAct 5 (H) Cpot C\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Uitem A\t14\t0\t1\t800\t800\t800\t1024\t0\tAct 1 Equip A\t58\tAct 1 Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Uitem B\t14\t5\t1\t800\t800\t800\t1024\t0\tAct 1 Equip B\t58\tAct 1 Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Uitem C\t14\t9\t1\t800\t800\t800\t1024\t0\tAct 1 Equip C\t58\tAct 1 Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Uitem A\t14\t12\t1\t800\t800\t800\t1024\t0\tAct 2 Equip A\t58\tAct 2 Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Uitem B\t14\t15\t1\t800\t800\t800\t1024\t0\tAct 2 Equip B\t58\tAct 2 Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Uitem C\t14\t18\t1\t800\t800\t800\t1024\t0\tAct 2 Equip C\t58\tAct 3 Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Uitem A\t14\t20\t1\t800\t800\t800\t1024\t0\tAct 3 Equip A\t58\tAct 3 Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Uitem B\t14\t22\t1\t800\t800\t800\t1024\t0\tAct 3 Equip B\t58\tAct 3 Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Uitem C\t14\t24\t1\t800\t800\t800\t1024\t0\tAct 3 Equip C\t58\tAct 3 Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Uitem A\t14\t26\t1\t800\t800\t800\t1024\t0\tAct 4 Equip A\t58\tAct 4 Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Uitem B\t14\t29\t1\t800\t800\t800\t1024\t0\tAct 4 Equip B\t58\tAct 4 Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Uitem A\t14\t32\t1\t800\t800\t800\t1024\t0\tAct 5 Equip A\t58\tAct 5 Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Uitem B\t14\t34\t1\t800\t800\t800\t1024\t0\tAct 5 Equip B\t58\tAct 5 Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Uitem C\t14\t36\t1\t800\t800\t800\t1024\t0\tAct 5 Equip C\t58\tAct 5 Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Uitem A\t14\t38\t1\t800\t800\t800\t1024\t0\tAct 1 (N) Equip A\t58\tAct 1 (N) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Uitem B\t14\t40\t1\t800\t800\t800\t1024\t0\tAct 1 (N) Equip B\t58\tAct 1 (N) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Uitem C\t14\t41\t1\t800\t800\t800\t1024\t0\tAct 1 (N) Equip B\t58\tAct 1 (N) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Uitem A\t14\t42\t1\t800\t800\t800\t1024\t0\tAct 2 (N) Equip A\t58\tAct 2 (N) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Uitem B\t14\t44\t1\t800\t800\t800\t1024\t0\tAct 2 (N) Equip B\t58\tAct 2 (N) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Uitem C\t14\t45\t1\t800\t800\t800\t1024\t0\tAct 2 (N) Equip B\t58\tAct 2 (N) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Uitem A\t14\t46\t1\t800\t800\t800\t1024\t0\tAct 3 (N) Equip A\t58\tAct 3 (N) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Uitem B\t14\t48\t1\t800\t800\t800\t1024\t0\tAct 3 (N) Equip B\t58\tAct 3 (N) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Uitem C\t14\t50\t1\t800\t800\t800\t1024\t0\tAct 3 (N) Equip B\t58\tAct 3 (N) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Uitem A\t14\t51\t1\t800\t800\t800\t1024\t0\tAct 4 (N) Equip A\t58\tAct 4 (N) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Uitem B\t14\t54\t1\t800\t800\t800\t1024\t0\tAct 4 (N) Equip B\t58\tAct 4 (N) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Uitem A\t14\t57\t1\t800\t800\t800\t1024\t0\tAct 5 (N) Equip A\t58\tAct 5 (N) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Uitem B\t14\t60\t1\t800\t800\t800\t1024\t0\tAct 5 (N) Equip B\t58\tAct 5 (N) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Uitem C\t14\t62\t1\t800\t800\t800\t1024\t0\tAct 5 (N) Equip C\t58\tAct 5 (N) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Uitem A\t14\t63\t1\t800\t800\t800\t1024\t0\tAct 1 (H) Equip A\t58\tAct 1 (H) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Uitem B\t14\t66\t1\t800\t800\t800\t1024\t0\tAct 1 (H) Equip B\t58\tAct 1 (H) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Uitem C\t14\t68\t1\t800\t800\t800\t1024\t0\tAct 1 (H) Equip B\t58\tAct 1 (H) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Uitem A\t14\t69\t1\t800\t800\t800\t1024\t0\tAct 2 (H) Equip A\t58\tAct 2 (H) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Uitem B\t14\t71\t1\t800\t800\t800\t1024\t0\tAct 2 (H) Equip B\t58\tAct 2 (H) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Uitem C\t14\t72\t1\t800\t800\t800\t1024\t0\tAct 2 (H) Equip B\t58\tAct 2 (H) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Uitem A\t14\t74\t1\t800\t800\t800\t1024\t0\tAct 3 (H) Equip A\t58\tAct 3 (H) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Uitem B\t14\t75\t1\t800\t800\t800\t1024\t0\tAct 3 (H) Equip B\t58\tAct 3 (H) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Uitem C\t14\t77\t1\t800\t800\t800\t1024\t0\tAct 3 (H) Equip B\t58\tAct 3 (H) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Uitem A\t14\t78\t1\t800\t800\t800\t1024\t0\tAct 4 (H) Equip A\t58\tAct 4 (H) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Uitem B\t14\t80\t1\t800\t800\t800\t1024\t0\tAct 4 (H) Equip B\t58\tAct 4 (H) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Uitem A\t14\t81\t1\t800\t800\t800\t1024\t0\tAct 5 (H) Equip A\t58\tAct 5 (H) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Uitem B\t14\t83\t1\t800\t800\t800\t1024\t0\tAct 5 (H) Equip B\t58\tAct 5 (H) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Uitem C\t14\t85\t1\t800\t800\t800\t1024\t0\tAct 5 (H) Equip C\t58\tAct 5 (H) Good\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Unique A\t15\t0\t-3\t\t\t\t\t0\tAct 1 Uitem A\t1\tAct 1 Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Unique B\t15\t5\t-3\t\t\t\t\t0\tAct 1 Uitem B\t1\tAct 1 Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Unique C\t15\t9\t-3\t\t\t\t\t0\tAct 1 Uitem C\t1\tAct 1 Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Unique A\t15\t12\t-3\t\t\t\t\t0\tAct 2 Uitem A\t1\tAct 2 Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Unique B\t15\t15\t-3\t\t\t\t\t0\tAct 2 Uitem B\t1\tAct 2 Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Unique C\t15\t18\t-3\t\t\t\t\t0\tAct 2 Uitem C\t1\tAct 2 Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Unique A\t15\t20\t-3\t\t\t\t\t0\tAct 3 Uitem A\t1\tAct 3 Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Unique B\t15\t22\t-3\t\t\t\t\t0\tAct 3 Uitem B\t1\tAct 3 Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Unique C\t15\t24\t-3\t\t\t\t\t0\tAct 3 Uitem C\t1\tAct 3 Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Unique A\t15\t26\t-3\t\t\t\t\t0\tAct 4 Uitem A\t1\tAct 4 Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Unique B\t15\t29\t-3\t\t\t\t\t0\tAct 4 Uitem B\t1\tAct 4 Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Unique A\t15\t32\t-3\t\t\t\t\t0\tAct 5 Uitem A\t1\tAct 5 Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Unique B\t15\t34\t-3\t\t\t\t\t0\tAct 5 Uitem B\t1\tAct 5 Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Unique C\t15\t36\t-3\t\t\t\t\t0\tAct 5 Uitem C\t1\tAct 5 Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Unique A\t15\t38\t-3\t\t\t\t\t0\tAct 1 (N) Uitem A\t1\tAct 1 (N) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Unique B\t15\t40\t-3\t\t\t\t\t0\tAct 1 (N) Uitem B\t1\tAct 1 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Unique C\t15\t41\t-3\t\t\t\t\t0\tAct 1 (N) Uitem C\t1\tAct 1 (N) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Unique A\t15\t42\t-3\t\t\t\t\t0\tAct 2 (N) Uitem A\t1\tAct 2 (N) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Unique B\t15\t44\t-3\t\t\t\t\t0\tAct 2 (N) Uitem B\t1\tAct 2 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Unique C\t15\t45\t-3\t\t\t\t\t0\tAct 2 (N) Uitem C\t1\tAct 2 (N) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Unique A\t15\t46\t-3\t\t\t\t\t0\tAct 3 (N) Uitem A\t1\tAct 3 (N) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Unique B\t15\t48\t-3\t\t\t\t\t0\tAct 3 (N) Uitem B\t1\tAct 3 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Unique C\t15\t50\t-3\t\t\t\t\t0\tAct 3 (N) Uitem C\t1\tAct 3 (N) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Unique A\t15\t51\t-3\t\t\t\t\t0\tAct 4 (N) Uitem A\t1\tAct 4 (N) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Unique B\t15\t54\t-3\t\t\t\t\t0\tAct 4 (N) Uitem B\t1\tAct 4 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Unique A\t15\t57\t-3\t\t\t\t\t0\tAct 5 (N) Uitem A\t1\tAct 5 (N) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Unique B\t15\t60\t-3\t\t\t\t\t0\tAct 5 (N) Uitem B\t1\tAct 5 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Unique C\t15\t62\t-3\t\t\t\t\t0\tAct 5 (N) Uitem C\t1\tAct 5 (N) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Unique A\t15\t63\t-3\t\t\t\t\t0\tAct 1 (H) Uitem A\t1\tAct 1 (H) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Unique B\t15\t66\t-3\t\t\t\t\t0\tAct 1 (H) Uitem B\t1\tAct 1 (H) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Unique C\t15\t68\t-3\t\t\t\t\t0\tAct 1 (H) Uitem C\t1\tAct 1 (H) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Unique A\t15\t69\t-3\t\t\t\t\t0\tAct 2 (H) Uitem A\t1\tAct 2 (H) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Unique B\t15\t71\t-3\t\t\t\t\t0\tAct 2 (H) Uitem B\t1\tAct 2 (H) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Unique C\t15\t72\t-3\t\t\t\t\t0\tAct 2 (H) Uitem C\t1\tAct 2 (H) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Unique A\t15\t74\t-3\t\t\t\t\t0\tAct 3 (H) Uitem A\t1\tAct 3 (H) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Unique B\t15\t75\t-3\t\t\t\t\t0\tAct 3 (H) Uitem B\t1\tAct 3 (H) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Unique C\t15\t77\t-3\t\t\t\t\t0\tAct 3 (H) Uitem C\t1\tAct 3 (H) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Unique A\t15\t78\t-3\t\t\t\t\t0\tAct 4 (H) Uitem A\t1\tAct 4 (H) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Unique B\t15\t80\t-3\t\t\t\t\t0\tAct 4 (H) Uitem B\t1\tAct 4 (H) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Unique A\t15\t81\t-3\t\t\t\t\t0\tAct 5 (H) Uitem A\t1\tAct 5 (H) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Unique B\t15\t83\t-3\t\t\t\t\t0\tAct 5 (H) Uitem B\t1\tAct 5 (H) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Unique C\t15\t85\t-3\t\t\t\t\t0\tAct 5 (H) Uitem C\t1\tAct 5 (H) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nQuill 1\t16\t0\t1\t\t\t\t\t125\tgld\t19\tAct 1 Junk\t19\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nQuill 2\t16\t4\t1\t\t\t\t\t125\tgld\t19\tAct 1 Junk\t19\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nQuill 3\t16\t8\t1\t\t\t\t\t125\tgld\t19\tAct 1 Junk\t19\tAct 1 Good\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nQuill 4\t16\t12\t1\t\t\t\t\t125\tgld\t19\tAct 1 Junk\t19\tAct 1 Good\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nQuill 5\t16\t20\t1\t\t\t\t\t125\tgld\t19\tAct 3 Junk\t19\tAct 3 Good\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nQuill 1 (N)\t16\t32\t1\t\t\t\t\t125\tgld\t19\tAct 1 (N) Junk\t19\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nQuill 2 (N)\t16\t35\t1\t\t\t\t\t125\tgld\t19\tAct 1 (N) Junk\t19\tAct 1 (N) Good\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nQuill 3 (N)\t16\t38\t1\t\t\t\t\t125\tgld\t19\tAct 1 (N) Junk\t19\tAct 1 (N) Good\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nQuill 4 (N)\t16\t40\t1\t\t\t\t\t125\tgld\t19\tAct 1 (N) Junk\t19\tAct 1 (N) Good\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nQuill 5 (N)\t16\t46\t1\t\t\t\t\t125\tgld\t19\tAct 3 (N) Junk\t19\tAct 3 (N) Good\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nQuill 1 (H)\t16\t60\t1\t\t\t\t\t125\tgld\t19\tAct 1 (H) Junk\t19\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nQuill 2 (H)\t16\t62\t1\t\t\t\t\t125\tgld\t19\tAct 1 (H) Junk\t19\tAct 1 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nQuill 3 (H)\t16\t65\t1\t\t\t\t\t125\tgld\t19\tAct 1 (H) Junk\t19\tAct 1 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nQuill 4 (H)\t16\t68\t1\t\t\t\t\t125\tgld\t19\tAct 1 (H) Junk\t19\tAct 1 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nQuill 5 (H)\t16\t85\t1\t\t\t\t\t125\tgld\t19\tAct 3 (H) Junk\t19\tAct 3 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nSwarm 1\t17\t0\t1\t\t\t\t\t125\tPotion 2\t12\tMisc 1\t6\tAct 2 Good\t1\tAct 2 Magic A\t6\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nSwarm 2\t17\t20\t1\t\t\t\t\t125\tPotion 3\t12\tMisc 2\t6\tAct 3 Good\t1\tAct 3 Magic A\t6\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nSwarm 1 (N)\t17\t40\t1\t\t\t\t\t125\tPotion 6\t12\tMisc 2\t6\tAct 2 (N) Good\t1\tAct 2 (N) Magic A\t6\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nSwarm 2 (N)\t17\t46\t1\t\t\t\t\t125\tPotion 6\t12\tMisc 2\t6\tAct 3 (N) Good\t1\tAct 3 (N) Magic A\t6\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nSwarm 1 (H)\t17\t68\t1\t\t\t\t\t125\tPotion 6\t12\tMisc 2\t6\tAct 2 (H) Good\t1\tAct 2 (H) Magic A\t6\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nSwarm 2 (H)\t17\t74\t1\t\t\t\t\t125\tPotion 6\t12\tMisc 2\t6\tAct 3 (H) Good\t1\tAct 3 (H) Magic A\t6\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAndariel\t\t\t7\t983\t983\t983\t1024\t19\t\"gld,mul=1280\"\t11\tAct 2 Equip A\t19\tAct 2 Junk\t15\tAct 2 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAndariel (N)\t\t\t7\t983\t983\t983\t1024\t19\t\"gld,mul=1536\"\t11\tAct 2 (N) Equip A\t19\tAct 2 (N) Junk\t15\tAct 2 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAndariel (H)\t\t\t7\t983\t983\t983\t1024\t19\t\"gld,mul=2048\"\t11\tAct 2 (H) Equip A\t19\tAct 2 (H) Junk\t15\tAct 2 (H) Good\t3\ttes\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAndarielq\t\t\t7\t995\t995\t1024\t1024\t19\tAct 2 Equip A\t19\tAct 2 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAndarielq (N)\t\t\t7\t995\t995\t1024\t1024\t19\tAct 2 (N) Equip A\t19\tAct 2 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAndarielq (H)\t\t\t7\t995\t995\t1024\t1024\t19\tAct 2 (H) Equip A\t19\tAct 2 (H) Good\t3\ttes\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nDuriel - Base\t\t\t7\t983\t983\t983\t1024\t19\t\"gld,mul=1280\"\t11\tAct 3 Equip A\t19\tAct 3 Junk\t15\tAct 3 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nDuriel (N) - Base\t\t\t7\t983\t983\t983\t1024\t19\t\"gld,mul=1536\"\t11\tAct 3 (N) Equip A\t19\tAct 3 (N) Junk\t15\tAct 3 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nDuriel (H) - Base\t\t\t7\t983\t983\t983\t1024\t19\t\"gld,mul=2048\"\t11\tAct 3 (H) Equip A\t19\tAct 3 (H) Junk\t15\tAct 3 (H) Good\t3\ttes\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nDurielq - Base\t\t\t7\t993\t993\t1024\t1024\t19\tAct 3 Equip A\t19\tAct 3 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nDurielq (N) - Base\t\t\t7\t993\t993\t1024\t1024\t19\tAct 3 (N) Equip A\t19\tAct 3 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nDurielq (H) - Base\t\t\t7\t993\t993\t1024\t1024\t19\tAct 3 (H) Equip A\t19\tAct 3 (H) Good\t3\ttes\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nRadament\t\t\t5\t900\t900\t900\t1024\t19\t\"gld,mul=1280\"\t11\tAct 3 Equip B\t19\tAct 3 Junk\t15\tAct 3 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nRadament (N)\t\t\t5\t900\t900\t900\t1024\t19\t\"gld,mul=1536\"\t11\tAct 3 (N) Equip B\t19\tAct 3 (N) Junk\t15\tAct 3 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nRadament (H)\t\t\t5\t900\t900\t900\t1024\t19\t\"gld,mul=2048\"\t11\tAct 3 (H) Equip C\t19\tAct 3 (H) Junk\t15\tAct 3 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nFlying Scimitar\t\t\t1\t\t\t\t\t100\tAct 2 Good\t3\tscm\t57\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nFlying Scimitar (N)\t\t\t1\t\t\t\t\t100\tAct 2 (N) Good\t3\tscm\t25\t9sm\t32\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nFlying Scimitar (H)\t\t\t1\t\t\t\t\t100\tAct 2 (H) Good\t3\tscm\t7\t9sm\t18\t7sm\t32\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nMephisto\t\t\t7\t983\t983\t983\t1024\t15\t\"gld,mul=1280\"\t5\tAct 4 Equip A\t52\tAct 4 Junk\t5\tAct 4 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nMephisto (N)\t\t\t7\t983\t983\t983\t1024\t15\t\"gld,mul=1536\"\t5\tAct 4 (N) Equip A\t52\tAct 4 (N) Junk\t5\tAct 4 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nMephisto (H)\t\t\t7\t983\t983\t983\t1024\t15\t\"gld,mul=2048\"\t5\tAct 4 (H) Equip A\t52\tAct 4 (H) Junk\t5\tAct 4 (H) Good\t3\tceh\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nMephistoq\t\t\t7\t993\t993\t1024\t1024\t15\tAct 4 Equip A\t52\tAct 4 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nMephistoq (N)\t\t\t7\t993\t993\t1024\t1024\t15\tAct 4 (N) Equip A\t52\tAct 4 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nMephistoq (H)\t\t\t7\t993\t993\t1024\t1024\t15\tAct 4 (H) Equip A\t52\tAct 4 (H) Good\t3\tceh\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nDiablo\t\t\t7\t983\t983\t983\t1024\t15\t\"gld,mul=1280\"\t5\tAct 5 Equip A\t52\tAct 5 Junk\t5\tAct 5 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nDiablo (N)\t\t\t7\t983\t983\t983\t1024\t15\t\"gld,mul=1536\"\t5\tAct 5 (N) Equip A\t52\tAct 5 (N) Junk\t5\tAct 5 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nDiablo (H)\t\t\t7\t983\t983\t983\t1024\t15\t\"gld,mul=2048\"\t5\tAct 5 (H) Equip A\t52\tAct 5 (H) Junk\t5\tAct 5 (H) Good\t3\tbet\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nDiabloq\t\t\t7\t993\t993\t1024\t1024\t15\tAct 5 Equip A\t52\tAct 5 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nDiabloq (N)\t\t\t7\t993\t993\t1024\t1024\t15\tAct 5 (N) Equip A\t52\tAct 5 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nDiabloq (H)\t\t\t7\t993\t993\t1024\t1024\t15\tAct 5 (H) Equip A\t52\tAct 5 (H) Good\t3\tbet\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nSummoner\t\t\t5\t900\t900\t972\t1024\t19\t\"gld,mul=1280\"\t9\tAct 2 Equip C\t15\tAct 3 Junk\t5\tAct 2 Good\t3\tAct 3 Magic A\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nSummoner (N)\t\t\t5\t900\t900\t972\t1024\t19\t\"gld,mul=1536\"\t9\tAct 2 (N) Equip C\t15\tAct 3 (N) Junk\t5\tAct 2 (N) Good\t3\tAct 3 (N) Magic A\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nSummoner (H)\t\t\t5\t900\t900\t972\t1024\t19\t\"gld,mul=2048\"\t9\tAct 2 (H) Equip C\t15\tAct 3 (H) Junk\t5\tAct 2 (H) Good\t3\tAct 3 (H) Magic A\t4\tpk2\t1\t\t\t\t\t\t\t\t\t\t\t\t0\r\nCouncil\t\t\t3\t650\t800\t800\t1024\t19\t\"gld,mul=1280\"\t9\tAct 4 Equip A\t15\tAct 4 Junk\t5\tAct 4 Good\t3\tAct 4 Magic A\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nCouncil (N)\t\t\t3\t650\t800\t800\t1024\t19\t\"gld,mul=1536\"\t9\tAct 4 (N) Equip A\t15\tAct 4 (N) Junk\t5\tAct 4 (N) Good\t3\tAct 4 (N) Magic A\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nCouncil (H)\t\t\t3\t650\t800\t800\t1024\t19\t\"gld,mul=2048\"\t9\tAct 4 (H) Equip A\t15\tAct 4 (H) Junk\t5\tAct 4 (H) Good\t3\tAct 4 (H) Magic A\t4\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nGriswold\t\t\t-3\t\t\t\t\t0\tAct 1 Uitem C\t1\tAct 1 Melee B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nGriswold (N)\t\t\t-3\t\t\t\t\t0\tAct 1 (H) Uitem C\t1\tAct 1 (N) Melee B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nGriswold (H)\t\t\t-3\t\t\t\t\t0\tAct 1 (H) Uitem C\t1\tAct 1 (H) Melee B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nCow\t\t\t1\t\t\t\t\t100\tgld\t19\tAct 5 Equip A\t19\tAct 5 Junk\t19\tAct 5 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nCow (N)\t\t\t1\t\t\t\t\t100\tgld\t19\tAct 5 (N) Equip A\t19\tAct 5 (N) Junk\t19\tAct 5 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nCow (H)\t\t\t1\t\t\t\t\t100\tgld\t19\tAct 5 (H) Equip B\t19\tAct 5 (H) Junk\t19\tAct 5 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nTrapped Soul\t\t\t1\t\t\t\t\t100\tgld\t19\tAct 4 Equip A\t8\tAct 4 Junk\t33\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nTrapped Soul (N)\t\t\t1\t\t\t\t\t100\tgld\t19\tAct 4 (N) Equip A\t8\tAct 4 (N) Junk\t33\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nTrapped Soul (H)\t\t\t1\t\t\t\t\t100\tgld\t19\tAct 4 (H) Equip A\t8\tAct 4 (H) Junk\t33\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nHaphesto\t\t\t5\t800\t800\t900\t1024\t19\t\"gld,mul=1280\"\t14\tAct 5 Equip A\t19\tAct 5 Junk\t5\tAct 5 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nHaphesto (N)\t\t\t5\t800\t800\t900\t1024\t19\t\"gld,mul=1536\"\t14\tAct 5 (N) Equip A\t19\tAct 5 (N) Junk\t5\tAct 5 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nHaphesto (H)\t\t\t5\t800\t800\t900\t1024\t19\t\"gld,mul=2048\"\t14\tAct 5 (H) Equip A\t19\tAct 5 (H) Junk\t5\tAct 5 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nNihlathak\t\t\t5\t900\t900\t900\t1024\t19\t\"gld,mul=1280\"\t14\tAct 5 Equip B\t19\tAct 5 Junk\t5\tAct 5 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nNihlathak (N)\t\t\t5\t900\t900\t900\t1024\t19\t\"gld,mul=1536\"\t14\tAct 5 (N) Equip B\t19\tAct 5 (N) Junk\t5\tAct 5 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nNihlathak (H)\t\t\t5\t900\t900\t900\t1024\t19\t\"gld,mul=2048\"\t14\tAct 5 (H) Equip B\t19\tAct 5 (H) Junk\t5\tAct 5 (H) Good\t3\tpk3\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nBaal\t\t\t7\t983\t983\t983\t1024\t15\t\"gld,mul=1280\"\t5\tAct 1 (N) Equip A\t52\tAct 1 (N) Junk\t5\tAct 1 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nBaal (N)\t\t\t7\t983\t983\t983\t1024\t15\t\"gld,mul=1536\"\t5\tAct 1 (H) Equip A\t52\tAct 1 (H) Junk\t5\tAct 1 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nBaal (H)\t\t\t7\t983\t983\t983\t1024\t15\t\"gld,mul=2048\"\t5\tAct 5 (H) Equip B\t52\tAct 5 (H) Junk\t5\tAct 5 (H) Good\t3\tfed\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nBaalq\t\t\t7\t993\t993\t1024\t1024\t15\tAct 1 (N) Equip A\t52\tAct 1 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nBaalq (N)\t\t\t7\t993\t993\t1024\t1024\t15\tAct 1 (H) Equip A\t52\tAct 1 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nBaalq (H)\t\t\t7\t993\t993\t1024\t1024\t15\tAct 5 (H) Equip B\t52\tAct 5 (H) Good\t3\tfed\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nBlood Raven\t\t\t5\t800\t800\t972\t1024\t19\t\"gld,mul=1280\"\t14\tAct 1 Equip B\t19\tAct 1 Junk\t5\tAct 1 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nBlood Raven (N)\t\t\t5\t800\t800\t972\t1024\t19\t\"gld,mul=1536\"\t14\tAct 1 (N) Equip B\t19\tAct 1 (N) Junk\t5\tAct 1 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nBlood Raven (H)\t\t\t5\t800\t800\t972\t1024\t19\t\"gld,mul=2048\"\t14\tAct 1 (H) Equip B\t19\tAct 1 (H) Junk\t5\tAct 1 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nSmith\t\t\t-3\t\t\t\t\t0\tAct 1 Uitem C\t1\tAct 1 Melee B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nSmith (N)\t\t\t-3\t\t\t\t\t0\tAct 1 (H) Uitem C\t1\tAct 1 (N) Melee B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nSmith (H)\t\t\t-3\t\t\t\t\t0\tAct 1 (H) Uitem C\t1\tAct 1 (H) Melee B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nIzual\t\t\t5\t800\t800\t972\t1024\t19\t\"gld,mul=1280\"\t14\tAct 4 Equip B\t19\tAct 4 Junk\t5\tAct 4 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nIzual (N)\t\t\t5\t800\t800\t972\t1024\t19\t\"gld,mul=1536\"\t14\tAct 4 (N) Equip B\t19\tAct 4 (N) Junk\t5\tAct 4 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nIzual (H)\t\t\t5\t800\t800\t972\t1024\t19\t\"gld,mul=2048\"\t14\tAct 4 (H) Equip B\t19\tAct 4 (H) Junk\t5\tAct 4 (H) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Super A\t18\t0\t-4\t800\t800\t972\t1024\t0\tAct 1 Uitem A\t2\tAct 1 Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Super B\t18\t5\t-4\t800\t800\t972\t1024\t0\tAct 1 Uitem B\t2\tAct 1 Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Super C\t18\t9\t-4\t800\t800\t972\t1024\t0\tAct 1 Uitem C\t2\tAct 1 Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Super A\t18\t12\t-4\t800\t800\t972\t1024\t0\tAct 2 Uitem A\t2\tAct 2 Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Super B\t18\t15\t-4\t800\t800\t972\t1024\t0\tAct 2 Uitem B\t2\tAct 2 Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Super C\t18\t18\t-4\t800\t800\t972\t1024\t0\tAct 2 Uitem C\t2\tAct 2 Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Super A\t18\t20\t-4\t800\t800\t972\t1024\t0\tAct 3 Uitem A\t2\tAct 3 Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Super B\t18\t22\t-4\t800\t800\t972\t1024\t0\tAct 3 Uitem B\t2\tAct 3 Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Super C\t18\t24\t-4\t800\t800\t972\t1024\t0\tAct 3 Uitem C\t2\tAct 3 Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Super A\t18\t26\t-4\t800\t800\t972\t1024\t0\tAct 4 Uitem A\t2\tAct 4 Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Super B\t18\t29\t-4\t800\t800\t972\t1024\t0\tAct 4 Uitem B\t2\tAct 4 Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Super C\t18\t32\t-4\t800\t800\t972\t1024\t0\tAct 4 Uitem B\t2\tAct 4 Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Super A\t18\t34\t-4\t800\t800\t972\t1024\t0\tAct 5 Uitem A\t2\tAct 5 Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Super B\t18\t36\t-4\t800\t800\t972\t1024\t0\tAct 5 Uitem B\t2\tAct 5 Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Super C\t18\t38\t-4\t800\t800\t972\t1024\t0\tAct 5 Uitem C\t2\tAct 5 Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Super A\t18\t40\t-4\t800\t800\t972\t1024\t0\tAct 1 (N) Uitem A\t2\tAct 1 (N) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Super B\t18\t41\t-4\t800\t800\t972\t1024\t0\tAct 1 (N) Uitem B\t2\tAct 1 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Super C\t18\t42\t-4\t800\t800\t972\t1024\t0\tAct 1 (N) Uitem C\t2\tAct 1 (N) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Super A\t18\t44\t-4\t800\t800\t972\t1024\t0\tAct 2 (N) Uitem A\t2\tAct 2 (N) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Super B\t18\t45\t-4\t800\t800\t972\t1024\t0\tAct 2 (N) Uitem B\t2\tAct 2 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Super C\t18\t46\t-4\t800\t800\t972\t1024\t0\tAct 2 (N) Uitem C\t2\tAct 2 (N) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Super A\t18\t48\t-4\t800\t800\t972\t1024\t0\tAct 3 (N) Uitem A\t2\tAct 3 (N) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Super B\t18\t50\t-4\t800\t800\t972\t1024\t0\tAct 3 (N) Uitem B\t2\tAct 3 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Super C\t18\t51\t-4\t800\t800\t972\t1024\t0\tAct 3 (N) Uitem C\t2\tAct 3 (N) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Super A\t18\t54\t-4\t800\t800\t972\t1024\t0\tAct 4 (N) Uitem A\t2\tAct 4 (N) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Super B\t18\t57\t-4\t800\t800\t972\t1024\t0\tAct 4 (N) Uitem B\t2\tAct 4 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Super C\t18\t60\t-4\t800\t800\t972\t1024\t0\tAct 4 (N) Uitem B\t2\tAct 4 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Super A\t18\t62\t-4\t800\t800\t972\t1024\t0\tAct 5 (N) Uitem A\t2\tAct 5 (N) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Super B\t18\t63\t-4\t800\t800\t972\t1024\t0\tAct 5 (N) Uitem B\t2\tAct 5 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Super C\t18\t66\t-4\t800\t800\t972\t1024\t0\tAct 5 (N) Uitem C\t2\tAct 5 (N) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Super A\t18\t67\t-4\t800\t800\t972\t1024\t0\tAct 1 (H) Uitem A\t2\tAct 1 (H) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Super B\t18\t67\t-4\t800\t800\t972\t1024\t0\tAct 1 (H) Uitem B\t2\tAct 1 (H) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Super C\t18\t68\t-4\t800\t800\t972\t1024\t0\tAct 1 (H) Uitem C\t2\tAct 1 (H) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Super A\t18\t68\t-4\t800\t800\t972\t1024\t0\tAct 2 (H) Uitem A\t2\tAct 2 (H) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Super B\t18\t69\t-4\t800\t800\t972\t1024\t0\tAct 2 (H) Uitem B\t2\tAct 2 (H) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Super C\t18\t72\t-4\t800\t800\t972\t1024\t0\tAct 2 (H) Uitem C\t2\tAct 2 (H) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Super A\t18\t73\t-4\t800\t800\t972\t1024\t0\tAct 3 (H) Uitem A\t2\tAct 3 (H) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Super B\t18\t75\t-4\t800\t800\t972\t1024\t0\tAct 3 (H) Uitem B\t2\tAct 3 (H) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Super C\t18\t78\t-4\t800\t800\t972\t1024\t0\tAct 3 (H) Uitem C\t2\tAct 3 (H) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Super A\t18\t81\t-4\t800\t800\t972\t1024\t0\tAct 4 (H) Uitem A\t2\tAct 4 (H) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Super B\t18\t84\t-4\t800\t800\t972\t1024\t0\tAct 4 (H) Uitem B\t2\tAct 4 (H) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Super C\t18\t87\t-4\t800\t800\t972\t1024\t0\tAct 4 (H) Uitem B\t2\tAct 4 (H) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Super A\t18\t90\t-4\t800\t800\t972\t1024\t0\tAct 5 (H) Uitem A\t2\tAct 5 (H) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Super B\t18\t93\t-4\t800\t800\t972\t1024\t0\tAct 5 (H) Uitem B\t2\tAct 5 (H) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Super C\t18\t96\t-4\t800\t800\t972\t1024\t0\tAct 5 (H) Uitem C\t2\tAct 5 (H) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Super Ax\t18\t0\t-4\t512\t654\t972\t1024\t0\tAct 1 Uitem A\t2\tAct 1 Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Super Bx\t18\t5\t-4\t512\t654\t972\t1024\t0\tAct 1 Uitem B\t2\tAct 1 Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 Super Cx\t18\t9\t-4\t512\t654\t972\t1024\t0\tAct 1 Uitem C\t2\tAct 1 Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Super Ax\t18\t12\t-4\t512\t654\t972\t1024\t0\tAct 2 Uitem A\t2\tAct 2 Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Super Bx\t18\t15\t-4\t512\t654\t972\t1024\t0\tAct 2 Uitem B\t2\tAct 2 Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 Super Cx\t18\t18\t-4\t512\t654\t972\t1024\t0\tAct 2 Uitem C\t2\tAct 2 Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Super Ax\t18\t20\t-4\t512\t654\t972\t1024\t0\tAct 3 Uitem A\t2\tAct 3 Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Super Bx\t18\t22\t-4\t512\t654\t972\t1024\t0\tAct 3 Uitem B\t2\tAct 3 Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 Super Cx\t18\t24\t-4\t512\t654\t972\t1024\t0\tAct 3 Uitem C\t2\tAct 3 Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Super Ax\t18\t26\t-4\t512\t654\t972\t1024\t0\tAct 4 Uitem A\t2\tAct 4 Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Super Bx\t18\t29\t-4\t512\t654\t972\t1024\t0\tAct 4 Uitem B\t2\tAct 4 Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 Super Cx\t18\t32\t-4\t512\t654\t972\t1024\t0\tAct 4 Uitem B\t2\tAct 4 Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Super Ax\t18\t34\t-4\t512\t654\t972\t1024\t0\tAct 5 Uitem A\t2\tAct 5 Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Super Bx\t18\t36\t-4\t512\t654\t972\t1024\t0\tAct 5 Uitem B\t2\tAct 5 Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 Super Cx\t18\t38\t-4\t512\t654\t972\t1024\t0\tAct 5 Uitem C\t2\tAct 5 Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Super Ax\t18\t40\t-4\t512\t654\t972\t1024\t0\tAct 1 (N) Uitem A\t2\tAct 1 (N) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Super Bx\t18\t41\t-4\t512\t654\t972\t1024\t0\tAct 1 (N) Uitem B\t2\tAct 1 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (N) Super Cx\t18\t42\t-4\t512\t654\t972\t1024\t0\tAct 1 (N) Uitem C\t2\tAct 1 (N) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Super Ax\t18\t44\t-4\t512\t654\t972\t1024\t0\tAct 2 (N) Uitem A\t2\tAct 2 (N) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Super Bx\t18\t45\t-4\t512\t654\t972\t1024\t0\tAct 2 (N) Uitem B\t2\tAct 2 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (N) Super Cx\t18\t46\t-4\t512\t654\t972\t1024\t0\tAct 2 (N) Uitem C\t2\tAct 2 (N) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Super Ax\t18\t48\t-4\t512\t654\t972\t1024\t0\tAct 3 (N) Uitem A\t2\tAct 3 (N) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Super Bx\t18\t50\t-4\t512\t654\t972\t1024\t0\tAct 3 (N) Uitem B\t2\tAct 3 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (N) Super Cx\t18\t51\t-4\t512\t654\t972\t1024\t0\tAct 3 (N) Uitem C\t2\tAct 3 (N) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Super Ax\t18\t54\t-4\t512\t654\t972\t1024\t0\tAct 4 (N) Uitem A\t2\tAct 4 (N) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Super Bx\t18\t57\t-4\t512\t654\t972\t1024\t0\tAct 4 (N) Uitem B\t2\tAct 4 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (N) Super Cx\t18\t60\t-4\t512\t654\t972\t1024\t0\tAct 4 (N) Uitem B\t2\tAct 4 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Super Ax\t18\t62\t-4\t512\t654\t972\t1024\t0\tAct 5 (N) Uitem A\t2\tAct 5 (N) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Super Bx\t18\t63\t-4\t512\t654\t972\t1024\t0\tAct 5 (N) Uitem B\t2\tAct 5 (N) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (N) Super Cx\t18\t66\t-4\t512\t654\t972\t1024\t0\tAct 5 (N) Uitem C\t2\tAct 5 (N) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Super Ax\t18\t67\t-4\t512\t654\t972\t1024\t0\tAct 1 (H) Uitem A\t2\tAct 1 (H) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Super Bx\t18\t67\t-4\t512\t654\t972\t1024\t0\tAct 1 (H) Uitem B\t2\tAct 1 (H) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 1 (H) Super Cx\t18\t68\t-4\t512\t654\t972\t1024\t0\tAct 1 (H) Uitem C\t2\tAct 1 (H) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Super Ax\t18\t68\t-4\t512\t654\t972\t1024\t0\tAct 2 (H) Uitem A\t2\tAct 2 (H) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Super Bx\t18\t69\t-4\t512\t654\t972\t1024\t0\tAct 2 (H) Uitem B\t2\tAct 2 (H) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 2 (H) Super Cx\t18\t72\t-4\t512\t654\t972\t1024\t0\tAct 2 (H) Uitem C\t2\tAct 2 (H) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Super Ax\t18\t73\t-4\t512\t654\t972\t1024\t0\tAct 3 (H) Uitem A\t2\tAct 3 (H) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Super Bx\t18\t75\t-4\t512\t654\t972\t1024\t0\tAct 3 (H) Uitem B\t2\tAct 3 (H) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 3 (H) Super Cx\t18\t78\t-4\t512\t654\t972\t1024\t0\tAct 3 (H) Uitem C\t2\tAct 3 (H) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Super Ax\t18\t81\t-4\t512\t654\t972\t1024\t0\tAct 4 (H) Uitem A\t2\tAct 4 (H) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Super Bx\t18\t84\t-4\t512\t654\t972\t1024\t0\tAct 4 (H) Uitem B\t2\tAct 4 (H) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 4 (H) Super Cx\t18\t87\t-4\t512\t654\t972\t1024\t0\tAct 4 (H) Uitem B\t2\tAct 4 (H) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Super Ax\t18\t90\t-4\t512\t654\t972\t1024\t0\tAct 5 (H) Uitem A\t2\tAct 5 (H) Cpot A\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Super Bx\t18\t93\t-4\t512\t654\t972\t1024\t0\tAct 5 (H) Uitem B\t2\tAct 5 (H) Cpot B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nAct 5 (H) Super Cx\t18\t96\t-4\t512\t654\t972\t1024\t0\tAct 5 (H) Uitem C\t2\tAct 5 (H) Cpot C\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nCow King\t\t\t5\t850\t983\t983\t1024\t0\tAct 1 Uitem C\t1\tAct 1 Melee B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nCow King (N)\t\t\t5\t850\t983\t983\t1024\t0\tAct 1 (H) Uitem C\t1\tAct 1 (N) Melee B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nCow King (H)\t\t\t5\t850\t983\t983\t1024\t0\tAct 1 (H) Uitem C\t1\tAct 1 (H) Melee B\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nDuriel\t\t\t5\t\t\t\t\t0\ttsc\t1\tDuriel - Base\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nDuriel (N)\t\t\t5\t\t\t\t\t0\ttsc\t1\tDuriel (N) - Base\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nDuriel (H)\t\t\t5\t\t\t\t\t0\ttsc\t1\tDuriel (H) - Base\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nDurielq\t\t\t5\t\t\t\t\t0\ttsc\t1\tDurielq - Base\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nDurielq (N)\t\t\t5\t\t\t\t\t0\ttsc\t1\tDurielq (N) - Base\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nDurielq (H)\t\t\t5\t\t\t\t\t0\ttsc\t1\tDurielq (H) - Base\t2\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nROP\t\t\t1\t1024\t\t\t\t0\tDiablo\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nROP (N)\t\t\t1\t1024\t\t\t\t0\tDiablo (N)\t4\tAnnihilus\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nROP (H)\t\t\t1\t1024\t\t\t\t0\tAnnihilus\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nCountess Rune\t\t\t3\t\t\t\t\t5\tRunes 4\t15\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nCountess Rune (N)\t\t\t3\t\t\t\t\t5\tRunes 8\t15\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nCountess Rune (H)\t\t\t3\t\t\t\t\t5\tRunes 12\t15\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nCountess Item\t\t\t5\t\t\t\t\t19\t\"gld,mul=1280\"\t11\tAct 1 Equip C\t19\tAct 2 Junk\t15\tAct 2 Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nCountess Item (N)\t\t\t5\t\t\t\t\t19\t\"gld,mul=1536\"\t11\tAct 1 (N) Equip C\t19\tAct 2 (N) Junk\t15\tAct 2 (N) Good\t3\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nCountess Item (H)\t\t\t5\t\t\t\t\t19\t\"gld,mul=2048\"\t11\tAct 1 (H) Equip C\t19\tAct 2 (H) Junk\t15\tAct 2 (H) Good\t3\tpk1\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nCountess\t\t\t-2\t883\t883\t883\t1024\t0\tCountess Item\t1\tCountess Rune\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nCountess (N)\t\t\t-2\t883\t883\t983\t1024\t0\tCountess Item (N)\t1\tCountess Rune (N)\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nCountess (H)\t\t\t-2\t883\t883\t983\t1024\t0\tCountess Item (H)\t1\tCountess Rune (H)\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nUber Andariel\t\t\t1\t1024\t\t\t\t0\tdhn\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nUber Duriel\t\t\t1\t1024\t\t\t\t0\tbey\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nUber Izual\t\t\t1\t1024\t\t\t\t0\tmbr\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nPandemonium Key A\t\t\t1\t1024\t\t\t\t0\tpk1\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nPandemonium Key B\t\t\t1\t1024\t\t\t\t0\tpk2\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\nPandemonium Key C\t\t\t1\t1024\t\t\t\t0\tpk3\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\r\n"; - -export const CANONICAL_AUTO_TCS_DATA: readonly { - readonly name: string; - readonly level: number; - readonly picks: number; - readonly totalProb: number; - readonly entries: readonly { readonly itemCode: string; readonly prob: number }[]; -}[] = [{"name":"weap3","level":3,"picks":1,"totalProb":26,"entries":[{"itemCode":"hax","prob":3},{"itemCode":"wnd","prob":1},{"itemCode":"clb","prob":1},{"itemCode":"scp","prob":2},{"itemCode":"ssd","prob":3},{"itemCode":"dgr","prob":2},{"itemCode":"tkf","prob":2},{"itemCode":"jav","prob":4},{"itemCode":"sst","prob":2},{"itemCode":"sbw","prob":2},{"itemCode":"ktr","prob":2},{"itemCode":"ob1","prob":2}]},{"name":"weap6","level":6,"picks":1,"totalProb":22,"entries":[{"itemCode":"lax","prob":4},{"itemCode":"spc","prob":3},{"itemCode":"scm","prob":3},{"itemCode":"spr","prob":4},{"itemCode":"bar","prob":4},{"itemCode":"hbw","prob":2},{"itemCode":"lxb","prob":2}]},{"name":"weap9","level":9,"picks":1,"totalProb":28,"entries":[{"itemCode":"axe","prob":4},{"itemCode":"mac","prob":4},{"itemCode":"sbr","prob":3},{"itemCode":"dir","prob":2},{"itemCode":"tax","prob":2},{"itemCode":"tri","prob":4},{"itemCode":"lst","prob":2},{"itemCode":"lbw","prob":2},{"itemCode":"wrb","prob":2},{"itemCode":"ob2","prob":3}]},{"name":"weap12","level":12,"picks":1,"totalProb":32,"entries":[{"itemCode":"bax","prob":4},{"itemCode":"ywn","prob":1},{"itemCode":"flc","prob":4},{"itemCode":"crs","prob":2},{"itemCode":"2hs","prob":4},{"itemCode":"pil","prob":4},{"itemCode":"vou","prob":4},{"itemCode":"cst","prob":2},{"itemCode":"cbw","prob":2},{"itemCode":"axf","prob":2},{"itemCode":"ob3","prob":3}]},{"name":"weap15","level":15,"picks":1,"totalProb":30,"entries":[{"itemCode":"2ax","prob":4},{"itemCode":"gsc","prob":4},{"itemCode":"mst","prob":4},{"itemCode":"bsd","prob":4},{"itemCode":"bkf","prob":2},{"itemCode":"ssp","prob":4},{"itemCode":"scy","prob":4},{"itemCode":"mxb","prob":2},{"itemCode":"ces","prob":2}]},{"name":"weap18","level":18,"picks":1,"totalProb":33,"entries":[{"itemCode":"btx","prob":4},{"itemCode":"bwn","prob":1},{"itemCode":"clm","prob":4},{"itemCode":"kri","prob":2},{"itemCode":"bal","prob":2},{"itemCode":"brn","prob":4},{"itemCode":"bst","prob":2},{"itemCode":"sbb","prob":2},{"itemCode":"clw","prob":2},{"itemCode":"ob4","prob":4},{"itemCode":"am1","prob":3},{"itemCode":"am3","prob":3}]},{"name":"weap21","level":21,"picks":1,"totalProb":34,"entries":[{"itemCode":"mpi","prob":4},{"itemCode":"wsp","prob":4},{"itemCode":"fla","prob":4},{"itemCode":"mau","prob":4},{"itemCode":"lsd","prob":4},{"itemCode":"gis","prob":4},{"itemCode":"spt","prob":4},{"itemCode":"pax","prob":4},{"itemCode":"btl","prob":2}]},{"name":"weap24","level":24,"picks":1,"totalProb":35,"entries":[{"itemCode":"gax","prob":4},{"itemCode":"bsw","prob":4},{"itemCode":"bld","prob":3},{"itemCode":"glv","prob":4},{"itemCode":"pik","prob":4},{"itemCode":"wst","prob":2},{"itemCode":"lbb","prob":2},{"itemCode":"hxb","prob":2},{"itemCode":"skr","prob":2},{"itemCode":"ob5","prob":5},{"itemCode":"am5","prob":3}]},{"name":"weap27","level":27,"picks":1,"totalProb":29,"entries":[{"itemCode":"wax","prob":4},{"itemCode":"gix","prob":4},{"itemCode":"gwn","prob":1},{"itemCode":"whm","prob":4},{"itemCode":"wsd","prob":4},{"itemCode":"flb","prob":4},{"itemCode":"swb","prob":2},{"itemCode":"am2","prob":3},{"itemCode":"am4","prob":3}]},{"name":"weap30","level":30,"picks":1,"totalProb":22,"entries":[{"itemCode":"tsp","prob":4},{"itemCode":"hal","prob":4},{"itemCode":"9cl","prob":1},{"itemCode":"9ss","prob":3},{"itemCode":"9ja","prob":4},{"itemCode":"8ss","prob":2},{"itemCode":"8sb","prob":2},{"itemCode":"9ar","prob":2}]},{"name":"weap33","level":33,"picks":1,"totalProb":38,"entries":[{"itemCode":"gma","prob":3},{"itemCode":"gsd","prob":4},{"itemCode":"lwb","prob":2},{"itemCode":"rxb","prob":2},{"itemCode":"9ha","prob":3},{"itemCode":"9wn","prob":1},{"itemCode":"9sc","prob":2},{"itemCode":"9sp","prob":3},{"itemCode":"9dg","prob":2},{"itemCode":"9tk","prob":2},{"itemCode":"9sr","prob":4},{"itemCode":"9b7","prob":4},{"itemCode":"8hb","prob":2},{"itemCode":"9wb","prob":2},{"itemCode":"ob6","prob":2}]},{"name":"weap36","level":36,"picks":1,"totalProb":41,"entries":[{"itemCode":"wsc","prob":3},{"itemCode":"9ax","prob":4},{"itemCode":"9la","prob":4},{"itemCode":"9ma","prob":4},{"itemCode":"9sb","prob":3},{"itemCode":"9di","prob":2},{"itemCode":"9ta","prob":2},{"itemCode":"9tr","prob":4},{"itemCode":"9pa","prob":4},{"itemCode":"8ls","prob":2},{"itemCode":"8lb","prob":2},{"itemCode":"8lx","prob":2},{"itemCode":"9xf","prob":2},{"itemCode":"ama","prob":3}]},{"name":"weap39","level":39,"picks":1,"totalProb":43,"entries":[{"itemCode":"92a","prob":4},{"itemCode":"9ba","prob":4},{"itemCode":"9yw","prob":1},{"itemCode":"9mt","prob":4},{"itemCode":"9fc","prob":4},{"itemCode":"9cr","prob":2},{"itemCode":"92h","prob":4},{"itemCode":"9bk","prob":2},{"itemCode":"9pi","prob":4},{"itemCode":"9vo","prob":4},{"itemCode":"8cs","prob":2},{"itemCode":"8cb","prob":2},{"itemCode":"ob7","prob":3},{"itemCode":"am6","prob":3}]},{"name":"weap42","level":42,"picks":1,"totalProb":41,"entries":[{"itemCode":"9bt","prob":4},{"itemCode":"9qs","prob":4},{"itemCode":"9bs","prob":4},{"itemCode":"9cm","prob":4},{"itemCode":"9kr","prob":2},{"itemCode":"9b8","prob":2},{"itemCode":"9s9","prob":4},{"itemCode":"9br","prob":4},{"itemCode":"9s8","prob":4},{"itemCode":"8bs","prob":2},{"itemCode":"8mx","prob":2},{"itemCode":"9cs","prob":2},{"itemCode":"ob8","prob":3}]},{"name":"weap45","level":45,"picks":1,"totalProb":39,"entries":[{"itemCode":"9mp","prob":4},{"itemCode":"9bw","prob":1},{"itemCode":"9ws","prob":4},{"itemCode":"9fl","prob":4},{"itemCode":"9m9","prob":4},{"itemCode":"9sm","prob":3},{"itemCode":"9ls","prob":4},{"itemCode":"9gs","prob":4},{"itemCode":"9st","prob":4},{"itemCode":"8s8","prob":2},{"itemCode":"9lw","prob":2},{"itemCode":"am8","prob":3}]},{"name":"weap48","level":48,"picks":1,"totalProb":40,"entries":[{"itemCode":"9wa","prob":4},{"itemCode":"9ga","prob":4},{"itemCode":"9wh","prob":4},{"itemCode":"9b9","prob":4},{"itemCode":"9bl","prob":3},{"itemCode":"9gl","prob":4},{"itemCode":"9p9","prob":4},{"itemCode":"8ws","prob":2},{"itemCode":"8l8","prob":2},{"itemCode":"8hx","prob":2},{"itemCode":"ob9","prob":4},{"itemCode":"am7","prob":3}]},{"name":"weap51","level":51,"picks":1,"totalProb":33,"entries":[{"itemCode":"9gi","prob":4},{"itemCode":"9gw","prob":1},{"itemCode":"9wd","prob":4},{"itemCode":"9fb","prob":4},{"itemCode":"9ts","prob":4},{"itemCode":"9h9","prob":4},{"itemCode":"8sw","prob":2},{"itemCode":"9tw","prob":2},{"itemCode":"oba","prob":5},{"itemCode":"am9","prob":3}]},{"name":"weap54","level":54,"picks":1,"totalProb":24,"entries":[{"itemCode":"9gm","prob":3},{"itemCode":"9gd","prob":4},{"itemCode":"8lw","prob":2},{"itemCode":"8rx","prob":2},{"itemCode":"9qr","prob":2},{"itemCode":"7ha","prob":3},{"itemCode":"7cl","prob":1},{"itemCode":"7ja","prob":4},{"itemCode":"amb","prob":3}]},{"name":"weap57","level":57,"picks":1,"totalProb":20,"entries":[{"itemCode":"9wc","prob":3},{"itemCode":"7la","prob":4},{"itemCode":"7wn","prob":1},{"itemCode":"7sp","prob":3},{"itemCode":"7ss","prob":3},{"itemCode":"7ta","prob":2},{"itemCode":"6sb","prob":2},{"itemCode":"6lx","prob":2}]},{"name":"weap60","level":60,"picks":1,"totalProb":22,"entries":[{"itemCode":"7ar","prob":2},{"itemCode":"72h","prob":4},{"itemCode":"7dg","prob":2},{"itemCode":"7sr","prob":4},{"itemCode":"7o7","prob":4},{"itemCode":"6ss","prob":2},{"itemCode":"6hb","prob":2},{"itemCode":"obb","prob":2}]},{"name":"weap63","level":63,"picks":1,"totalProb":27,"entries":[{"itemCode":"7wb","prob":2},{"itemCode":"7ax","prob":4},{"itemCode":"7sc","prob":2},{"itemCode":"7ma","prob":4},{"itemCode":"7sm","prob":3},{"itemCode":"7sb","prob":3},{"itemCode":"7pi","prob":4},{"itemCode":"6lb","prob":2},{"itemCode":"amd","prob":3}]},{"name":"weap66","level":66,"picks":1,"totalProb":24,"entries":[{"itemCode":"7ba","prob":4},{"itemCode":"7yw","prob":1},{"itemCode":"7cm","prob":4},{"itemCode":"7tk","prob":2},{"itemCode":"7tr","prob":4},{"itemCode":"7vo","prob":4},{"itemCode":"6ls","prob":2},{"itemCode":"amf","prob":3}]},{"name":"weap69","level":69,"picks":1,"totalProb":17,"entries":[{"itemCode":"7xf","prob":2},{"itemCode":"7m7","prob":4},{"itemCode":"7fc","prob":4},{"itemCode":"6cb","prob":2},{"itemCode":"6mx","prob":2},{"itemCode":"obc","prob":3}]},{"name":"weap72","level":72,"picks":1,"totalProb":24,"entries":[{"itemCode":"72a","prob":4},{"itemCode":"7mt","prob":4},{"itemCode":"7gs","prob":4},{"itemCode":"7di","prob":2},{"itemCode":"7s7","prob":4},{"itemCode":"7s8","prob":4},{"itemCode":"6s7","prob":2}]},{"name":"weap75","level":75,"picks":1,"totalProb":20,"entries":[{"itemCode":"7cs","prob":2},{"itemCode":"7bt","prob":4},{"itemCode":"7bw","prob":1},{"itemCode":"7cr","prob":2},{"itemCode":"7br","prob":4},{"itemCode":"6cs","prob":2},{"itemCode":"6hx","prob":2},{"itemCode":"obd","prob":3}]},{"name":"weap78","level":78,"picks":1,"totalProb":25,"entries":[{"itemCode":"7lw","prob":2},{"itemCode":"7qs","prob":4},{"itemCode":"7fl","prob":4},{"itemCode":"7bs","prob":4},{"itemCode":"7b7","prob":4},{"itemCode":"7bk","prob":2},{"itemCode":"6l7","prob":2},{"itemCode":"amc","prob":3}]},{"name":"weap81","level":81,"picks":1,"totalProb":25,"entries":[{"itemCode":"7tw","prob":2},{"itemCode":"7mp","prob":4},{"itemCode":"7fb","prob":4},{"itemCode":"7b8","prob":2},{"itemCode":"7gl","prob":4},{"itemCode":"7pa","prob":4},{"itemCode":"6sw","prob":2},{"itemCode":"ame","prob":3}]},{"name":"weap84","level":84,"picks":1,"totalProb":30,"entries":[{"itemCode":"7ga","prob":4},{"itemCode":"7wh","prob":4},{"itemCode":"7ls","prob":4},{"itemCode":"7kr","prob":2},{"itemCode":"7st","prob":4},{"itemCode":"7h7","prob":4},{"itemCode":"6bs","prob":2},{"itemCode":"6rx","prob":2},{"itemCode":"obe","prob":4}]},{"name":"weap87","level":87,"picks":1,"totalProb":49,"entries":[{"itemCode":"7qr","prob":2},{"itemCode":"7wa","prob":4},{"itemCode":"7gi","prob":4},{"itemCode":"7gw","prob":1},{"itemCode":"7ws","prob":4},{"itemCode":"7gm","prob":3},{"itemCode":"7wd","prob":4},{"itemCode":"7gd","prob":4},{"itemCode":"7bl","prob":3},{"itemCode":"7ts","prob":4},{"itemCode":"7p7","prob":4},{"itemCode":"7wc","prob":3},{"itemCode":"6ws","prob":2},{"itemCode":"6lw","prob":2},{"itemCode":"obf","prob":5}]},{"name":"armo3","level":3,"picks":1,"totalProb":9,"entries":[{"itemCode":"cap","prob":1},{"itemCode":"qui","prob":1},{"itemCode":"lea","prob":2},{"itemCode":"buc","prob":2},{"itemCode":"lgl","prob":1},{"itemCode":"lbt","prob":1},{"itemCode":"lbl","prob":1}]},{"name":"armo6","level":6,"picks":1,"totalProb":16,"entries":[{"itemCode":"skp","prob":4},{"itemCode":"hla","prob":3},{"itemCode":"sml","prob":3},{"itemCode":"dr1","prob":1},{"itemCode":"ba1","prob":2},{"itemCode":"pa1","prob":2},{"itemCode":"ne1","prob":1}]},{"name":"armo9","level":9,"picks":1,"totalProb":13,"entries":[{"itemCode":"stu","prob":4},{"itemCode":"vgl","prob":1},{"itemCode":"vbt","prob":1},{"itemCode":"vbl","prob":1},{"itemCode":"dr2","prob":1},{"itemCode":"ba2","prob":2},{"itemCode":"pa2","prob":2},{"itemCode":"ne2","prob":1}]},{"name":"armo12","level":12,"picks":1,"totalProb":21,"entries":[{"itemCode":"hlm","prob":4},{"itemCode":"rng","prob":4},{"itemCode":"lrg","prob":4},{"itemCode":"mgl","prob":2},{"itemCode":"mbt","prob":2},{"itemCode":"mbl","prob":2},{"itemCode":"spk","prob":3}]},{"name":"armo15","level":15,"picks":1,"totalProb":16,"entries":[{"itemCode":"fhl","prob":4},{"itemCode":"scl","prob":4},{"itemCode":"chn","prob":4},{"itemCode":"kit","prob":4}]},{"name":"armo18","level":18,"picks":1,"totalProb":10,"entries":[{"itemCode":"brs","prob":4},{"itemCode":"dr3","prob":1},{"itemCode":"ba3","prob":2},{"itemCode":"pa3","prob":2},{"itemCode":"ne3","prob":1}]},{"name":"armo21","level":21,"picks":1,"totalProb":24,"entries":[{"itemCode":"msk","prob":4},{"itemCode":"spl","prob":4},{"itemCode":"tgl","prob":3},{"itemCode":"tbt","prob":3},{"itemCode":"tbl","prob":2},{"itemCode":"bsh","prob":2},{"itemCode":"dr4","prob":1},{"itemCode":"ba4","prob":2},{"itemCode":"pa4","prob":2},{"itemCode":"ne4","prob":1}]},{"name":"armo24","level":24,"picks":1,"totalProb":21,"entries":[{"itemCode":"ghm","prob":4},{"itemCode":"plt","prob":4},{"itemCode":"tow","prob":4},{"itemCode":"bhm","prob":2},{"itemCode":"dr5","prob":1},{"itemCode":"ba5","prob":2},{"itemCode":"pa5","prob":2},{"itemCode":"ne5","prob":1},{"itemCode":"ci0","prob":1}]},{"name":"armo27","level":27,"picks":1,"totalProb":11,"entries":[{"itemCode":"hgl","prob":4},{"itemCode":"hbt","prob":4},{"itemCode":"hbl","prob":3}]},{"name":"armo30","level":30,"picks":1,"totalProb":12,"entries":[{"itemCode":"crn","prob":4},{"itemCode":"fld","prob":4},{"itemCode":"gts","prob":4}]},{"name":"armo33","level":33,"picks":1,"totalProb":8,"entries":[{"itemCode":"gth","prob":4},{"itemCode":"xlg","prob":1},{"itemCode":"ba6","prob":2},{"itemCode":"ne6","prob":1}]},{"name":"armo36","level":36,"picks":1,"totalProb":15,"entries":[{"itemCode":"ltp","prob":4},{"itemCode":"xap","prob":1},{"itemCode":"xui","prob":1},{"itemCode":"xea","prob":2},{"itemCode":"xuc","prob":2},{"itemCode":"xlb","prob":1},{"itemCode":"zlb","prob":1},{"itemCode":"dr6","prob":1},{"itemCode":"pa6","prob":2}]},{"name":"armo39","level":39,"picks":1,"totalProb":20,"entries":[{"itemCode":"ful","prob":4},{"itemCode":"xkp","prob":4},{"itemCode":"xla","prob":3},{"itemCode":"xml","prob":3},{"itemCode":"xvg","prob":1},{"itemCode":"xvb","prob":1},{"itemCode":"zvb","prob":1},{"itemCode":"ba7","prob":2},{"itemCode":"ne7","prob":1}]},{"name":"armo42","level":42,"picks":1,"totalProb":26,"entries":[{"itemCode":"aar","prob":4},{"itemCode":"xlm","prob":4},{"itemCode":"xtu","prob":4},{"itemCode":"xng","prob":4},{"itemCode":"xrg","prob":4},{"itemCode":"xpk","prob":3},{"itemCode":"dr7","prob":1},{"itemCode":"pa7","prob":2}]},{"name":"armo45","level":45,"picks":1,"totalProb":25,"entries":[{"itemCode":"xhl","prob":4},{"itemCode":"xcl","prob":4},{"itemCode":"xhn","prob":4},{"itemCode":"xit","prob":4},{"itemCode":"xmg","prob":2},{"itemCode":"xmb","prob":2},{"itemCode":"zmb","prob":2},{"itemCode":"ba8","prob":2},{"itemCode":"ne8","prob":1}]},{"name":"armo48","level":48,"picks":1,"totalProb":13,"entries":[{"itemCode":"xsk","prob":4},{"itemCode":"xrs","prob":4},{"itemCode":"xsh","prob":2},{"itemCode":"dr8","prob":1},{"itemCode":"pa8","prob":2}]},{"name":"armo51","level":51,"picks":1,"totalProb":28,"entries":[{"itemCode":"xhm","prob":4},{"itemCode":"xpl","prob":4},{"itemCode":"xow","prob":4},{"itemCode":"xtg","prob":3},{"itemCode":"xtb","prob":3},{"itemCode":"ztb","prob":2},{"itemCode":"xh9","prob":2},{"itemCode":"dr9","prob":1},{"itemCode":"ba9","prob":2},{"itemCode":"pa9","prob":2},{"itemCode":"ne9","prob":1}]},{"name":"armo54","level":54,"picks":1,"totalProb":23,"entries":[{"itemCode":"xlt","prob":4},{"itemCode":"xhg","prob":4},{"itemCode":"xhb","prob":4},{"itemCode":"zhb","prob":3},{"itemCode":"ci1","prob":1},{"itemCode":"uhl","prob":4},{"itemCode":"baa","prob":2},{"itemCode":"nea","prob":1}]},{"name":"armo57","level":57,"picks":1,"totalProb":16,"entries":[{"itemCode":"xrn","prob":4},{"itemCode":"xld","prob":4},{"itemCode":"xts","prob":4},{"itemCode":"ulg","prob":1},{"itemCode":"dra","prob":1},{"itemCode":"paa","prob":2}]},{"name":"armo60","level":60,"picks":1,"totalProb":15,"entries":[{"itemCode":"xth","prob":4},{"itemCode":"xtp","prob":4},{"itemCode":"uap","prob":1},{"itemCode":"uuc","prob":2},{"itemCode":"ulb","prob":1},{"itemCode":"bab","prob":2},{"itemCode":"neb","prob":1}]},{"name":"armo63","level":63,"picks":1,"totalProb":20,"entries":[{"itemCode":"xul","prob":4},{"itemCode":"ukp","prob":4},{"itemCode":"uhn","prob":4},{"itemCode":"uml","prob":3},{"itemCode":"uvg","prob":1},{"itemCode":"ulc","prob":1},{"itemCode":"drb","prob":1},{"itemCode":"pab","prob":2}]},{"name":"armo66","level":66,"picks":1,"totalProb":13,"entries":[{"itemCode":"xar","prob":4},{"itemCode":"uui","prob":1},{"itemCode":"urg","prob":4},{"itemCode":"uvb","prob":1},{"itemCode":"bac","prob":2},{"itemCode":"ned","prob":1}]},{"name":"armo69","level":69,"picks":1,"totalProb":17,"entries":[{"itemCode":"ulm","prob":4},{"itemCode":"uea","prob":2},{"itemCode":"ula","prob":3},{"itemCode":"umg","prob":2},{"itemCode":"uvc","prob":1},{"itemCode":"upk","prob":3},{"itemCode":"drc","prob":1},{"itemCode":"neg","prob":1}]},{"name":"armo72","level":72,"picks":1,"totalProb":17,"entries":[{"itemCode":"ci2","prob":1},{"itemCode":"utu","prob":4},{"itemCode":"ung","prob":4},{"itemCode":"uit","prob":4},{"itemCode":"umb","prob":2},{"itemCode":"pac","prob":2}]},{"name":"armo75","level":75,"picks":1,"totalProb":18,"entries":[{"itemCode":"usk","prob":4},{"itemCode":"ucl","prob":4},{"itemCode":"urs","prob":4},{"itemCode":"umc","prob":2},{"itemCode":"bad","prob":2},{"itemCode":"pad","prob":2}]},{"name":"armo78","level":78,"picks":1,"totalProb":14,"entries":[{"itemCode":"upl","prob":4},{"itemCode":"ult","prob":4},{"itemCode":"utg","prob":3},{"itemCode":"ush","prob":2},{"itemCode":"drd","prob":1}]},{"name":"armo81","level":81,"picks":1,"totalProb":18,"entries":[{"itemCode":"uhm","prob":4},{"itemCode":"uld","prob":4},{"itemCode":"uow","prob":4},{"itemCode":"utb","prob":3},{"itemCode":"bae","prob":2},{"itemCode":"nee","prob":1}]},{"name":"armo84","level":84,"picks":1,"totalProb":23,"entries":[{"itemCode":"uth","prob":4},{"itemCode":"uul","prob":4},{"itemCode":"utp","prob":4},{"itemCode":"uts","prob":4},{"itemCode":"utc","prob":2},{"itemCode":"uh9","prob":2},{"itemCode":"dre","prob":1},{"itemCode":"pae","prob":2}]},{"name":"armo87","level":87,"picks":1,"totalProb":26,"entries":[{"itemCode":"ci3","prob":1},{"itemCode":"urn","prob":4},{"itemCode":"uar","prob":4},{"itemCode":"uhg","prob":4},{"itemCode":"uhb","prob":4},{"itemCode":"uhc","prob":3},{"itemCode":"drf","prob":1},{"itemCode":"baf","prob":2},{"itemCode":"paf","prob":2},{"itemCode":"nef","prob":1}]},{"name":"bow3","level":3,"picks":1,"totalProb":2,"entries":[{"itemCode":"sbw","prob":2}]},{"name":"bow6","level":6,"picks":1,"totalProb":2,"entries":[{"itemCode":"hbw","prob":2}]},{"name":"bow9","level":9,"picks":1,"totalProb":2,"entries":[{"itemCode":"lbw","prob":2}]},{"name":"bow12","level":12,"picks":1,"totalProb":2,"entries":[{"itemCode":"cbw","prob":2}]},{"name":"bow15","level":15,"picks":1,"totalProb":0,"entries":[]},{"name":"bow18","level":18,"picks":1,"totalProb":5,"entries":[{"itemCode":"sbb","prob":2},{"itemCode":"am1","prob":3}]},{"name":"bow21","level":21,"picks":1,"totalProb":0,"entries":[]},{"name":"bow24","level":24,"picks":1,"totalProb":2,"entries":[{"itemCode":"lbb","prob":2}]},{"name":"bow27","level":27,"picks":1,"totalProb":5,"entries":[{"itemCode":"swb","prob":2},{"itemCode":"am2","prob":3}]},{"name":"bow30","level":30,"picks":1,"totalProb":2,"entries":[{"itemCode":"8sb","prob":2}]},{"name":"bow33","level":33,"picks":1,"totalProb":4,"entries":[{"itemCode":"lwb","prob":2},{"itemCode":"8hb","prob":2}]},{"name":"bow36","level":36,"picks":1,"totalProb":2,"entries":[{"itemCode":"8lb","prob":2}]},{"name":"bow39","level":39,"picks":1,"totalProb":5,"entries":[{"itemCode":"8cb","prob":2},{"itemCode":"am6","prob":3}]},{"name":"bow42","level":42,"picks":1,"totalProb":0,"entries":[]},{"name":"bow45","level":45,"picks":1,"totalProb":2,"entries":[{"itemCode":"8s8","prob":2}]},{"name":"bow48","level":48,"picks":1,"totalProb":5,"entries":[{"itemCode":"8l8","prob":2},{"itemCode":"am7","prob":3}]},{"name":"bow51","level":51,"picks":1,"totalProb":2,"entries":[{"itemCode":"8sw","prob":2}]},{"name":"bow54","level":54,"picks":1,"totalProb":5,"entries":[{"itemCode":"8lw","prob":2},{"itemCode":"amb","prob":3}]},{"name":"bow57","level":57,"picks":1,"totalProb":2,"entries":[{"itemCode":"6sb","prob":2}]},{"name":"bow60","level":60,"picks":1,"totalProb":2,"entries":[{"itemCode":"6hb","prob":2}]},{"name":"bow63","level":63,"picks":1,"totalProb":2,"entries":[{"itemCode":"6lb","prob":2}]},{"name":"bow66","level":66,"picks":1,"totalProb":0,"entries":[]},{"name":"bow69","level":69,"picks":1,"totalProb":2,"entries":[{"itemCode":"6cb","prob":2}]},{"name":"bow72","level":72,"picks":1,"totalProb":2,"entries":[{"itemCode":"6s7","prob":2}]},{"name":"bow75","level":75,"picks":1,"totalProb":0,"entries":[]},{"name":"bow78","level":78,"picks":1,"totalProb":5,"entries":[{"itemCode":"6l7","prob":2},{"itemCode":"amc","prob":3}]},{"name":"bow81","level":81,"picks":1,"totalProb":2,"entries":[{"itemCode":"6sw","prob":2}]},{"name":"bow84","level":84,"picks":1,"totalProb":0,"entries":[]},{"name":"bow87","level":87,"picks":1,"totalProb":2,"entries":[{"itemCode":"6lw","prob":2}]},{"name":"mele3","level":3,"picks":1,"totalProb":22,"entries":[{"itemCode":"hax","prob":3},{"itemCode":"wnd","prob":1},{"itemCode":"clb","prob":1},{"itemCode":"scp","prob":2},{"itemCode":"ssd","prob":3},{"itemCode":"dgr","prob":2},{"itemCode":"tkf","prob":2},{"itemCode":"jav","prob":4},{"itemCode":"sst","prob":2},{"itemCode":"ktr","prob":2}]},{"name":"mele6","level":6,"picks":1,"totalProb":18,"entries":[{"itemCode":"lax","prob":4},{"itemCode":"spc","prob":3},{"itemCode":"scm","prob":3},{"itemCode":"spr","prob":4},{"itemCode":"bar","prob":4}]},{"name":"mele9","level":9,"picks":1,"totalProb":23,"entries":[{"itemCode":"axe","prob":4},{"itemCode":"mac","prob":4},{"itemCode":"sbr","prob":3},{"itemCode":"dir","prob":2},{"itemCode":"tax","prob":2},{"itemCode":"tri","prob":4},{"itemCode":"lst","prob":2},{"itemCode":"wrb","prob":2}]},{"name":"mele12","level":12,"picks":1,"totalProb":27,"entries":[{"itemCode":"bax","prob":4},{"itemCode":"ywn","prob":1},{"itemCode":"flc","prob":4},{"itemCode":"crs","prob":2},{"itemCode":"2hs","prob":4},{"itemCode":"pil","prob":4},{"itemCode":"vou","prob":4},{"itemCode":"cst","prob":2},{"itemCode":"axf","prob":2}]},{"name":"mele15","level":15,"picks":1,"totalProb":28,"entries":[{"itemCode":"2ax","prob":4},{"itemCode":"gsc","prob":4},{"itemCode":"mst","prob":4},{"itemCode":"bsd","prob":4},{"itemCode":"bkf","prob":2},{"itemCode":"ssp","prob":4},{"itemCode":"scy","prob":4},{"itemCode":"ces","prob":2}]},{"name":"mele18","level":18,"picks":1,"totalProb":24,"entries":[{"itemCode":"btx","prob":4},{"itemCode":"bwn","prob":1},{"itemCode":"clm","prob":4},{"itemCode":"kri","prob":2},{"itemCode":"bal","prob":2},{"itemCode":"brn","prob":4},{"itemCode":"bst","prob":2},{"itemCode":"clw","prob":2},{"itemCode":"am3","prob":3}]},{"name":"mele21","level":21,"picks":1,"totalProb":34,"entries":[{"itemCode":"mpi","prob":4},{"itemCode":"wsp","prob":4},{"itemCode":"fla","prob":4},{"itemCode":"mau","prob":4},{"itemCode":"lsd","prob":4},{"itemCode":"gis","prob":4},{"itemCode":"spt","prob":4},{"itemCode":"pax","prob":4},{"itemCode":"btl","prob":2}]},{"name":"mele24","level":24,"picks":1,"totalProb":26,"entries":[{"itemCode":"gax","prob":4},{"itemCode":"bsw","prob":4},{"itemCode":"bld","prob":3},{"itemCode":"glv","prob":4},{"itemCode":"pik","prob":4},{"itemCode":"wst","prob":2},{"itemCode":"skr","prob":2},{"itemCode":"am5","prob":3}]},{"name":"mele27","level":27,"picks":1,"totalProb":24,"entries":[{"itemCode":"wax","prob":4},{"itemCode":"gix","prob":4},{"itemCode":"gwn","prob":1},{"itemCode":"whm","prob":4},{"itemCode":"wsd","prob":4},{"itemCode":"flb","prob":4},{"itemCode":"am4","prob":3}]},{"name":"mele30","level":30,"picks":1,"totalProb":20,"entries":[{"itemCode":"tsp","prob":4},{"itemCode":"hal","prob":4},{"itemCode":"9cl","prob":1},{"itemCode":"9ss","prob":3},{"itemCode":"9ja","prob":4},{"itemCode":"8ss","prob":2},{"itemCode":"9ar","prob":2}]},{"name":"mele33","level":33,"picks":1,"totalProb":30,"entries":[{"itemCode":"gma","prob":3},{"itemCode":"gsd","prob":4},{"itemCode":"9ha","prob":3},{"itemCode":"9wn","prob":1},{"itemCode":"9sc","prob":2},{"itemCode":"9sp","prob":3},{"itemCode":"9dg","prob":2},{"itemCode":"9tk","prob":2},{"itemCode":"9sr","prob":4},{"itemCode":"9b7","prob":4},{"itemCode":"9wb","prob":2}]},{"name":"mele36","level":36,"picks":1,"totalProb":37,"entries":[{"itemCode":"wsc","prob":3},{"itemCode":"9ax","prob":4},{"itemCode":"9la","prob":4},{"itemCode":"9ma","prob":4},{"itemCode":"9sb","prob":3},{"itemCode":"9di","prob":2},{"itemCode":"9ta","prob":2},{"itemCode":"9tr","prob":4},{"itemCode":"9pa","prob":4},{"itemCode":"8ls","prob":2},{"itemCode":"9xf","prob":2},{"itemCode":"ama","prob":3}]},{"name":"mele39","level":39,"picks":1,"totalProb":35,"entries":[{"itemCode":"92a","prob":4},{"itemCode":"9ba","prob":4},{"itemCode":"9yw","prob":1},{"itemCode":"9mt","prob":4},{"itemCode":"9fc","prob":4},{"itemCode":"9cr","prob":2},{"itemCode":"92h","prob":4},{"itemCode":"9bk","prob":2},{"itemCode":"9pi","prob":4},{"itemCode":"9vo","prob":4},{"itemCode":"8cs","prob":2}]}]; - -export const CANONICAL_ITEM_NAMES: Readonly> = {"cap":"Cap (便帽)","skp":"Skull Cap","hlm":"Helm","fhl":"Full Helm","ghm":"Great Helm","crn":"Crown","msk":"Mask","qui":"Quilted Armor","lea":"Leather Armor","hla":"Hard Leather Armor","stu":"Studded Leather","rng":"Ring Mail","scl":"Scale Mail","chn":"Chain Mail","brs":"Breast Plate","spl":"Splint Mail","plt":"Plate Mail","fld":"Field Plate","gth":"Gothic Plate","ful":"Full Plate Mail","aar":"Ancient Armor","ltp":"Light Plate","buc":"Buckler","sml":"Small Shield","lrg":"Large Shield","kit":"Kite Shield","tow":"Tower Shield","gts":"Gothic Shield","lgl":"Gloves(L)","vgl":"Heavy Gloves","mgl":"Bracers(M)","tgl":"Light Gauntlets","hgl":"Gaunlets(H)","lbt":"Leather Boots","vbt":"Heavy Boots","mbt":"Chain Boots","tbt":"Light Plate Boots","hbt":"Plate Boots","lbl":"Sash(L)","vbl":"Light Belt","mbl":"Belt(M)","tbl":"Heavy Belt","hbl":"Girdle(H)","bhm":"Bone Helm","bsh":"Bone Shield","spk":"Spiked Shield","xap":"War Hat","xkp":"Sallet","xlm":"Casque","xhl":"Basinet","xhm":"Winged Helm","xrn":"Grand Crown","xsk":"Death Mask","xui":"Ghost Armor","xea":"Serpentskin Armor","xla":"Demonhide Armor","xtu":"Trellised Armor","xng":"Linked Mail","xcl":"Tigulated Mail","xhn":"Mesh Armor","xrs":"Cuirass","xpl":"Russet Armor","xlt":"Templar Coat","xld":"Sharktooth Armor","xth":"Embossed Plate","xul":"Chaos Armor","xar":"Ornate Armor","xtp":"Mage Plate","xuc":"Defender","xml":"Round Shield","xrg":"Scutum","xit":"Dragon Shield","xow":"Pavise","xts":"Ancient Shield","xlg":"Demonhide Gloves","xvg":"Sharkskin Gloves","xmg":"Heavy Bracers","xtg":"Battle Gauntlets","xhg":"War Gauntlets","xlb":"Demonhide Boots","xvb":"Sharkskin Boots","xmb":"Mesh Boots","xtb":"Battle Boots","xhb":"War Boots","zlb":"Demonhide Sash","zvb":"Sharkskin Belt","zmb":"Mesh Belt","ztb":"Battle Belt","zhb":"War Belt","xh9":"Grim Helm","xsh":"Grim Shield","xpk":"Barbed Shield","dr1":"Wolf Head","dr2":"Hawk Helm","dr3":"Antlers","dr4":"Falcon Mask","dr5":"Spirit Mask","ba1":"Jawbone Cap","ba2":"Fanged Helm","ba3":"Horned Helm","ba4":"Assault Helmet","ba5":"Avenger Guard","pa1":"Targe","pa2":"Rondache","pa3":"Heraldic Shield","pa4":"Aerin Shield","pa5":"Crown Shield","ne1":"Preserved Head","ne2":"Zombie Head","ne3":"Unraveller Head","ne4":"Gargoyle Head","ne5":"Demon Head","ci0":"Circlet","ci1":"Coronet","ci2":"Tiara","ci3":"Diadem","uap":"Shako (军帽)","ukp":"Hydraskull","ulm":"Armet","uhl":"Giant Conch","uhm":"Spired Helm","urn":"Corona","usk":"Demonhead","uui":"Dusk Shroud","uea":"Wyrmhide","ula":"Scarab Husk","utu":"Wire Fleece","ung":"Diamond Mail","ucl":"Loricated Mail","uhn":"Boneweave","urs":"Great Hauberk","upl":"Balrog Skin","ult":"Hellforged Plate","uld":"Kraken Shell","uth":"Lacquered Plate","uul":"Shadow Plate","uar":"Sacred Armor","utp":"Archon Plate","uuc":"Heater","uml":"Luna","urg":"Hyperion","uit":"Monarch","uow":"Aegis","uts":"Ward","ulg":"Bramble Mitts","uvg":"Vampirebone Gloves","umg":"Vambraces","utg":"Crusader Gauntlets","uhg":"Ogre Gauntlets","ulb":"Wyrmhide Boots","uvb":"Scarabshell Boots","umb":"Boneweave Boots","utb":"Mirrored Boots","uhb":"Myrmidon Greaves","ulc":"Spiderweb Sash","uvc":"Vampirefang Belt","umc":"Mithril Coil","utc":"Troll Belt","uhc":"Colossus Girdle","uh9":"Bone Visage","ush":"Troll Nest","upk":"Blade Barrier","dr6":"Alpha Helm","dr7":"Griffon Headress","dr8":"Hunter's Guise","dr9":"Sacred Feathers","dra":"Totemic Mask","ba6":"Jawbone Visor","ba7":"Lion Helm","ba8":"Rage Mask","ba9":"Savage Helmet","baa":"Slayer Guard","pa6":"Akaran Targe","pa7":"Akaran Rondache","pa8":"Protector Shield","pa9":"Guilded Shield","paa":"Royal Shield","ne6":"Mummified Trophy","ne7":"Fetish Trophy","ne8":"Sexton Trophy","ne9":"Cantor Trophy","nea":"Heirophant Trophy","drb":"Blood Spirt","drc":"Sun Spirit","drd":"Earth Spirit","dre":"Sky Spirit","drf":"Dream Spirit","bab":"Carnage Helm","bac":"Fury Visor","bad":"Destroyer Helm","bae":"Conquerer Crown","baf":"Guardian Crown","pab":"Sacred Targe","pac":"Sacred Rondache","pad":"Ancient Shield","pae":"Zakarum Shield","paf":"Vortex Shield","neb":"Minion Skull","neg":"Hellspawn Skull","ned":"Overseer Skull","nee":"Succubae Skull","nef":"Bloodlord Skull","hax":"Hand Axe","axe":"Axe","2ax":"Double Axe","mpi":"Military Pick","wax":"War Axe","lax":"Large Axe","bax":"Broad Axe","btx":"Battle Axe","gax":"Great Axe","gix":"Giant Axe","wnd":"Wand","ywn":"Yew Wand","bwn":"Bone Wand","gwn":"Grim Wand","clb":"Club","scp":"Scepter","gsc":"Grand Scepter","wsp":"War Scepter","spc":"Spiked Club","mac":"Mace","mst":"Morning Star","fla":"Flail","whm":"War Hammer","mau":"Maul","gma":"Great Maul","ssd":"Short Sword","scm":"Scimitar","sbr":"Saber","flc":"Falchion","crs":"Crystal Sword","bsd":"Broad Sword","lsd":"Long Sword","wsd":"War Sword","2hs":"Two-Handed Sword","clm":"Claymore","gis":"Giant Sword","bsw":"Bastard Sword","flb":"Flamberge","gsd":"Great Sword","dgr":"Dagger","dir":"Dirk","kri":"Kriss","bld":"Blade","tkf":"Throwing Knife","tax":"Throwing Axe","bkf":"Balanced Knife","bal":"Balanced Axe","jav":"Javelin","pil":"Pilum","ssp":"Short Spear","glv":"Glaive","tsp":"Throwing Spear","spr":"Spear","tri":"Trident","brn":"Brandistock","spt":"Spetum","pik":"Pike","bar":"Bardiche","vou":"Voulge","scy":"Scythe","pax":"Poleaxe","hal":"Halberd","wsc":"War Scythe","sst":"Short Staff","lst":"Long Staff","cst":"Gnarled Staff","bst":"Battle Staff","wst":"War Staff","sbw":"Short Bow","hbw":"Hunter's Bow","lbw":"Long Bow","cbw":"Composite Bow","sbb":"Short Battle Bow","lbb":"Long Battle Bow","swb":"Short War Bow","lwb":"Long War Bow","lxb":"Light Crossbow","mxb":"Crossbow","hxb":"Heavy Crossbow","rxb":"Repeating Crossbow","gps":"Rancid Gas Potion","ops":"Oil Potion","gpm":"Choking Gas Potion","opm":"Exploding Potion","gpl":"Strangling Gas Potion","opl":"Fulminating Potion","d33":"decoy dagger","g33":"Gidbinn","leg":"Wirt's Leg","hdm":"Horadric Malus","hfh":"Hellforge Hammer","hst":"Horadric Staff","msf":"Staff of the Kings","9ha":"Hatchet","9ax":"Cleaver","92a":"Twin Axe","9mp":"Crowbill","9wa":"Naga","9la":"Military Axe","9ba":"Bearded Axe","9bt":"Tabar","9ga":"Gothic Axe","9gi":"Ancient Axe","9wn":"Burnt Wand","9yw":"Petrified Wand","9bw":"Tomb Wand","9gw":"Grave Wand","9cl":"Cudgel","9sc":"Rune Scepter","9qs":"Holy Water Sprinkler","9ws":"Divine Scepter","9sp":"Barbed Club","9ma":"Flanged Mace","9mt":"Jagged Star","9fl":"Knout","9wh":"Battle Hammer","9m9":"War Club","9gm":"Martel de Fer","9ss":"Gladius","9sm":"Cutlass","9sb":"Shamshir","9fc":"Tulwar","9cr":"Dimensional Blade","9bs":"Battle Sword","9ls":"Rune Sword","9wd":"Ancient Sword","92h":"Espadon","9cm":"Dacian Falx","9gs":"Tusk Sword","9b9":"Gothic Sword","9fb":"Zweihander","9gd":"Executioner Sword","9dg":"Poignard","9di":"Rondel","9kr":"Cinquedeas","9bl":"Stilleto","9tk":"Battle Dart","9ta":"Francisca","9bk":"War Dart","9b8":"Hurlbat","9ja":"War Javelin","9pi":"Great Pilum","9s9":"Simbilan","9gl":"Spiculum","9ts":"Harpoon","9sr":"War Spear","9tr":"Fuscina","9br":"War Fork","9st":"Yari","9p9":"Lance","9b7":"Lochaber Axe","9vo":"Bill","9s8":"Battle Scythe","9pa":"Partizan","9h9":"Bec-de-Corbin","9wc":"Grim Scythe","8ss":"Jo Staff","8ls":"Quarterstaff","8cs":"Cedar Staff","8bs":"Gothic Staff","8ws":"Rune Staff","8sb":"Edge Bow","8hb":"Razor Bow","8lb":"Cedar Bow","8cb":"Double Bow","8s8":"Short Siege Bow","8l8":"Long Siege Bow","8sw":"Rune Bow","8lw":"Gothic Bow","8lx":"Arbalest","8mx":"Siege Crossbow","8hx":"Balista","8rx":"Chu-Ko-Nu","qf1":"KhalimFlail","qf2":"SuperKhalimFlail","ktr":"Katar","wrb":"Wrist Blade","axf":"Hatchet Hands","ces":"Cestus","clw":"Claws","btl":"Blade Talons","skr":"Scissors Katar","9ar":"Quhab","9wb":"Wrist Spike","9xf":"Fascia","9cs":"Hand Scythe","9lw":"Greater Claws","9tw":"Greater Talons","9qr":"Scissors Quhab","7ar":"Suwayyah","7wb":"Wrist Sword","7xf":"War Fist","7cs":"Battle Cestus","7lw":"Feral Claws","7tw":"Runic Talons","7qr":"Scissors Suwayyah","7ha":"Tomahawk","7ax":"Small Crescent","72a":"Ettin Axe","7mp":"War Spike","7wa":"Berserker Axe","7la":"Feral Axe","7ba":"Silver Edged Axe","7bt":"Decapitator","7ga":"Champion Axe","7gi":"Glorious Axe","7wn":"Polished Wand","7yw":"Ghost Wand","7bw":"Lich Wand","7gw":"Unearthed Wand","7cl":"Truncheon","7sc":"Mighty Scepter","7qs":"Seraph Rod","7ws":"Caduceus","7sp":"Tyrant Club","7ma":"Reinforced Mace","7mt":"Devil Star","7fl":"Scourge","7wh":"Legendary Mallet","7m7":"Ogre Maul","7gm":"Thunder Maul","7ss":"Falcata","7sm":"Ataghan","7sb":"Elegant Blade","7fc":"Hydra Edge","7cr":"Phase Blade","7bs":"Conquest Sword","7ls":"Cryptic Sword","7wd":"Mythical Sword","72h":"Legend Sword","7cm":"Highland Blade","7gs":"Balrog Blade","7b7":"Champion Sword","7fb":"Colossal Sword","7gd":"Colossus Blade","7dg":"Bone Knife","7di":"Mithral Point","7kr":"Fanged Knife","7bl":"Legend Spike","7tk":"Flying Knife","7ta":"Flying Axe","7bk":"Winged Knife","7b8":"Winged Axe","7ja":"Hyperion Javelin","7pi":"Stygian Pilum","7s7":"Balrog Spear","7gl":"Ghost Glaive","7ts":"Winged Harpoon","7sr":"Hyperion Spear","7tr":"Stygian Pike","7br":"Mancatcher","7st":"Ghost Spear","7p7":"War Pike","7o7":"Ogre Axe","7vo":"Colossus Voulge","7s8":"Thresher","7pa":"Cryptic Axe","7h7":"Great Poleaxe","7wc":"Giant Thresher","6ss":"Walking Stick","6ls":"Stalagmite","6cs":"Elder Staff","6bs":"Shillelah","6ws":"Archon Staff","6sb":"Spider Bow","6hb":"Blade Bow","6lb":"Shadow Bow","6cb":"Great Bow","6s7":"Diamond Bow","6l7":"Crusader Bow","6sw":"Ward Bow","6lw":"Hydra Bow","6lx":"Pellet Bow","6mx":"Gorgon Crossbow","6hx":"Colossus Crossbow","6rx":"Demon Crossbow","ob1":"Eagle Orb","ob2":"Sacred Globe","ob3":"Smoked Sphere","ob4":"Clasped Orb","ob5":"Dragon Stone","am1":"Stag Bow","am2":"Reflex Bow","am3":"Maiden Spear","am4":"Maiden Pike","am5":"Maiden Javelin","ob6":"Glowing Orb","ob7":"Crystalline Globe","ob8":"Cloudy Sphere","ob9":"Sparkling Ball","oba":"Swirling Crystal","am6":"Ashwood Bow","am7":"Ceremonial Bow","am8":"Ceremonial Spear","am9":"Ceremonial Pike","ama":"Ceremonial Javelin","obb":"Heavenly Stone","obc":"Eldritch Orb","obd":"Demon Heart","obe":"Vortex Orb","obf":"Dimensional Shard","amb":"Matriarchal Bow","amc":"Grand Matron Bow","amd":"Matriarchal Spear","ame":"Matriarchal Pike","amf":"MatriarchalJavelin","elx":"elixir","hpo":"Healing Potion","mpo":"Mana Potion","hpf":"Full Healing Potion","mpf":"Full Mana Potion","vps":"Stamina Potion","yps":"Antidote Potion","rvs":"Rejuv Potion","rvl":"Full Rejuv Potion","wms":"Thawing Potion","tbk":"Town Portal Book","ibk":"Identify Book","amu":"amulet","vip":"viper amulet","rin":"ring","gld":"Gold (金币)","bks":"Bark Scroll","bkd":"deciphered Bark Scroll","aqv":"Arrows","tch":"Torch","cqv":"Bolts","tsc":"Town Portal Scroll","isc":"Identify Scroll","hrt":"Not used","brz":"Not used","jaw":"Not used","eyz":"Not used","hrn":"Not used","tal":"Not used","flg":"Not used","fng":"Not used","qll":"Not used","sol":"Not used","scz":"Not used","spe":"Not used","key":"Skeleton Key","luv":"Mephisto Key","xyz":"scroll of self resurrect","j34":"jade figurine","g34":"gold bird","bbb":"lam esen's tome","box":"Horadric Cube","tr1":"Scroll of Horadric Quest Info","mss":"Mephisto SoulStone","ass":"Book of Skill","qey":"KhalimEye","qhr":"KhalimHeart","qbr":"KhalimBrain","ear":"Player Ear","gcv":"Chipped Amethyst","gfv":"Flawed Amethyst","gsv":"Amethyst","gzv":"Flawless Amethyst","gpv":"Perfect Amethyst","gcy":"Chipped Topaz","gfy":"Flawed Topaz","gsy":"Topaz","gly":"Flawless Topaz","gpy":"Perfect Topaz","gcb":"Chipped Saphire","gfb":"Flawed Saphire","gsb":"Saphire","glb":"Flawless Saphire","gpb":"Perfect Saphire","gcg":"Chipped Emerald","gfg":"Flawed Emerald","gsg":"Emerald","glg":"Flawless Emerald","gpg":"Perfect Emerald","gcr":"Chipped Ruby","gfr":"Flawed Ruby","gsr":"Ruby","glr":"Flawless Ruby","gpr":"Perfect Ruby","gcw":"Chipped Diamond","gfw":"Flawed Diamond","gsw":"Diamond","glw":"Flawless Diamond","gpw":"Perfect Diamond","hp1":"Lesser Healing Potion","hp2":"Light Healing Potion","hp3":"Healing Potion","hp4":"Strong Healing Potion","hp5":"Greater Healing Potion","mp1":"Lesser Mana Potion","mp2":"Light Mana Potion","mp3":"Mana Potion","mp4":"Strong Mana Potion","mp5":"Greater Mana Potion","skc":"Chipped Skull","skf":"Flawed Skull","sku":"Skull","skl":"Flawless Skull","skz":"Perfect Skull","hrb":"herb","cm1":"Charm Small","cm2":"Charm Medium","cm3":"Charm Large","rps":"Small Red Potion","rpl":"Large Red Potion","bps":"Small Blue Potion","bpl":"Large Blue Potion","r01":"El Rune","r02":"Eld Rune","r03":"Tir Rune","r04":"Nef Rune","r05":"Eth Rune","r06":"Ith Rune","r07":"Tal Rune","r08":"Ral Rune","r09":"Ort Rune","r10":"Thul Rune","r11":"Amn Rune","r12":"Sol Rune","r13":"Shael Rune","r14":"Dol Rune","r15":"Hel Rune","r16":"Io Rune","r17":"Lum Rune","r18":"Ko Rune","r19":"Fal Rune","r20":"Lem Rune (20# 蓝姆)","r21":"Pul Rune (21# 普尔)","r22":"Um Rune (22# 乌姆)","r23":"Mal Rune (23# 马尔)","r24":"Ist Rune (24# 伊司特)","r25":"Gul Rune (25# 古尔)","r26":"Vex Rune (26# 伐克斯)","r27":"Ohm Rune (27# 欧姆)","r28":"Lo Rune (28# 罗)","r29":"Sur Rune (29# 瑟)","r30":"Ber Rune (30# 贝)","r31":"Jah Rune (31# 乔)","r32":"Cham Rune (32# 查姆)","r33":"Zod Rune (33# 萨德)","jew":"Jewel","ice":"Maguffin","0sc":"Scroll","tr2":"Scroll of Malah - Boost Resistances","pk1":"Key of Terror (恐惧之钥)","pk2":"Key of Hate (憎恨之钥)","pk3":"Key of Destruction (毁灭之钥)","dhn":"Charged Essence of Hatred (憎恨之精华)","bey":"Baal's Eye","mbr":"Twisted Essence of Suffering (痛苦之精华)","toa":"Token of Absolution","tes":"Token of Absolution (赦免徽章)","ceh":"Standard of Heroes (英雄旗帜)","bet":"Burning Essence of Terror (恐惧之精华)","fed":"Festering Essence of Destruction (毁灭之精华)","std":"Standard","Annihilus":"Annihilus (毁灭小护身符)"}; - -export const CANONICAL_ITEM_RATIO_ROWS: readonly ItemRatioRow[] = [ - { function: 'Ratio', version: 0, uber: 0, classSpecific: 0, unique: 400, uniqueDiv: 2, uniqueMin: 6400, rare: 160, rareDiv: 3, rareMin: 3200, set: 125, setDiv: 6, setMin: 5600, magic: 30, magicDiv: 16, magicMin: 192, hiQual: 12, hiQualDiv: 16, normal: 4, normalDiv: 8 }, - { function: 'Uber', version: 0, uber: 1, classSpecific: 0, unique: 240, uniqueDiv: 2, uniqueMin: 6400, rare: 96, rareDiv: 3, rareMin: 3200, set: 96, setDiv: 6, setMin: 5600, magic: 3, magicDiv: 100, magicMin: 192, hiQual: 4, hiQualDiv: 16, normal: 1, normalDiv: 8 }, - { function: 'Ratio Exp', version: 1, uber: 0, classSpecific: 0, unique: 400, uniqueDiv: 1, uniqueMin: 6400, rare: 100, rareDiv: 2, rareMin: 3200, set: 160, setDiv: 2, setMin: 5600, magic: 34, magicDiv: 3, magicMin: 192, hiQual: 12, hiQualDiv: 8, normal: 2, normalDiv: 2 }, - { function: 'Uber Exp', version: 1, uber: 1, classSpecific: 0, unique: 400, uniqueDiv: 1, uniqueMin: 6400, rare: 100, rareDiv: 2, rareMin: 3200, set: 160, setDiv: 2, setMin: 5600, magic: 34, magicDiv: 3, magicMin: 192, hiQual: 12, hiQualDiv: 8, normal: 1, normalDiv: 1 }, - { function: 'Class Specific', version: 1, uber: 0, classSpecific: 1, unique: 240, uniqueDiv: 3, uniqueMin: 6400, rare: 80, rareDiv: 3, rareMin: 3200, set: 120, setDiv: 3, setMin: 5600, magic: 17, magicDiv: 6, magicMin: 192, hiQual: 9, hiQualDiv: 8, normal: 2, normalDiv: 2 }, - { function: 'Class Specific Uber', version: 1, uber: 1, classSpecific: 1, unique: 240, uniqueDiv: 3, uniqueMin: 6400, rare: 80, rareDiv: 3, rareMin: 3200, set: 120, setDiv: 3, setMin: 5600, magic: 17, magicDiv: 6, magicMin: 192, hiQual: 9, hiQualDiv: 8, normal: 1, normalDiv: 1 }, -]; - -let _cachedTcTable: TreasureClassTable | undefined; -let _cachedAutoTcs: Map | undefined; - -/** - * Loads the default canonical 1.13c TreasureClassTable and merged auto-TCs. - */ -export function loadCanonicalTreasureClasses(): { - tcTable: TreasureClassTable; - autoTcs: Map; -} { - if (_cachedTcTable && _cachedAutoTcs) { - return { tcTable: _cachedTcTable, autoTcs: _cachedAutoTcs }; - } - const bytes = new TextEncoder().encode(CANONICAL_TC_EX_TXT); - const tcTable = parseTreasureClassTable(parseTable(bytes)); - const autoTcs = new Map(); - for (const a of CANONICAL_AUTO_TCS_DATA) { - autoTcs.set(a.name, { - name: a.name, - group: 0, - level: a.level, - picks: a.picks as 1, - totalProb: a.totalProb, - entries: a.entries, - }); - } - mergeAutoTreasureClasses(tcTable, autoTcs); - _cachedTcTable = tcTable; - _cachedAutoTcs = autoTcs; - return { tcTable, autoTcs }; -} - -// --- Interfaces & Calculation Helpers ---------------------------------------- - -export interface DirectProbabilities { - readonly tcName: string; - readonly picks: number; - readonly noDrop: number; - readonly totalWeight: number; - readonly probabilities: ReadonlyMap; -} - -export interface AnalyticItemDrop { - readonly item: string; - readonly name: string; - readonly singleProbability: number; - readonly compoundProbability: number; - readonly expectedDrops: number; - readonly oneInOdds: number; - readonly isGold: boolean; - readonly isRune: boolean; -} - -export interface AnalyticTcResult { - readonly tcName: string; - readonly picks: number; - readonly effectivePlayers: number; - readonly noDropSingleProb: number; - readonly noDropCompoundProb: number; - readonly direct: ReadonlyMap; - readonly leaves: readonly AnalyticItemDrop[]; - readonly leafMap: ReadonlyMap; -} - -export interface ReverseLookupEntry { - readonly tcName: string; - readonly picks: number; - readonly singleProbability: number; - readonly compoundProbability: number; - readonly oneInOdds: number; -} - -export interface ReverseLookupResult { - readonly query: string; - readonly matchedCode: string; - readonly matchedName: string; - readonly entries: readonly ReverseLookupEntry[]; -} - -export interface QualityCounts { - normal: number; - superior: number; - magic: number; - rare: number; - set: number; - unique: number; - gold: number; - rune: number; - noDrop: number; -} - -export interface DroppedItemSummary { - readonly code: string; - readonly name: string; - readonly quality: string; - readonly count: number; - readonly percentage: number; -} - -export interface SimulationOptions { - readonly tcName: string; - readonly runs: number; - readonly gamePlayers?: number | undefined; - readonly partyPlayers?: number | undefined; - readonly magicFind?: number | undefined; - readonly difficulty?: 'normal' | 'nightmare' | 'hell' | undefined; - readonly isQuestDrop?: boolean | undefined; - readonly seed?: number | undefined; - readonly chunkSize?: number | undefined; - readonly tcTable?: TreasureClassTable | undefined; - readonly autoTcs?: Map | undefined; - readonly itemRatioTable?: ItemRatioTable | undefined; -} - -export interface SimulationProgress { - readonly completedRuns: number; - readonly totalRuns: number; - readonly itemsDropped: number; - readonly qualityCounts: QualityCounts; - readonly isFinished: boolean; - readonly isPaused: boolean; -} - -export interface SimulationResult { - readonly totalRuns: number; - readonly itemsDropped: number; - readonly emptyRuns: number; - readonly averageDropsPerRun: number; - readonly qualityCounts: QualityCounts; - readonly itemSummaries: readonly DroppedItemSummary[]; -} - -/** - * Calculates compound probability 1 - (1 - p)^picks for picks >= 1. - * Guaranteed to be within [0, 1]. - */ -export function calculateCompoundProbability(singleProbability: number, picks: number): number { - if (singleProbability <= 0) return 0; - if (singleProbability >= 1) return 1; - if (picks <= 0) return 1; // deterministic enumeration - if (picks === 1) return singleProbability; - const compound = 1 - Math.pow(1 - singleProbability, picks); - return Math.min(1, Math.max(0, compound)); -} - -/** - * Calculates direct 1-step probabilities of immediate children in a TreasureClass, - * including NoDrop and direct sub-TCs. - */ -export function calculateDirectProbabilities( - tcName: string, - options?: { - tcTable?: TreasureClassTable | undefined; - autoTcs?: Map | undefined; - gamePlayers?: number | undefined; - partyPlayers?: number | undefined; - } | undefined, -): DirectProbabilities { - const table = options?.tcTable ?? loadCanonicalTreasureClasses().tcTable; - const autoMap = options?.autoTcs ?? loadCanonicalTreasureClasses().autoTcs; - let node: TreasureClassNode | undefined = table.get(tcName); - if (!node && autoMap.has(tcName)) { - node = autoTcToTreasureClassNode(autoMap.get(tcName)!); - } - - if (!node) { - return { - tcName, - picks: 0, - noDrop: 0, - totalWeight: 0, - probabilities: new Map(), - }; - } - - const picks = node.picks ?? 1; - const probs = new Map(); - - if (picks < 0) { - let sumWeights = 0; - for (const it of node.items) { - const prob = (it as any).prob ?? 0; - if (prob <= 0) continue; - sumWeights += prob; - const key = typeof it === 'string' ? it : it.item; - probs.set(key, (probs.get(key) ?? 0) + 1.0); - } - return { - tcName, - picks, - noDrop: 0, - totalWeight: sumWeights, - probabilities: probs, - }; - } - - const effectivePlayers = computeEffectivePlayers(options?.gamePlayers, options?.partyPlayers); - const baseNoDrop = node.noDrop ?? 0; - const totalProb = (node as any).totalProbExpansion ?? (node as any).totalProb ?? 0; - const scaledNoDrop = computeScaledNoDrop(baseNoDrop, totalProb, effectivePlayers); - const totalWeight = scaledNoDrop + totalProb; - - if (totalWeight <= 0) { - return { - tcName, - picks, - noDrop: baseNoDrop, - totalWeight: 0, - probabilities: probs, - }; - } - - if (scaledNoDrop > 0) { - probs.set('NoDrop', scaledNoDrop / totalWeight); - } - - for (const it of node.items) { - const prob = (it as any).prob ?? 0; - if (prob <= 0) continue; - const key = typeof it === 'string' ? it : it.item; - probs.set(key, (probs.get(key) ?? 0) + prob / totalWeight); - } - - return { - tcName, - picks, - noDrop: scaledNoDrop, - totalWeight, - probabilities: probs, - }; -} - -/** - * Recursively expands a TreasureClass tree to calculate exact leaf item probabilities. - */ -export function calculateAnalyticProbabilities( - tcName: string, - options?: { - tcTable?: TreasureClassTable | undefined; - autoTcs?: Map | undefined; - gamePlayers?: number | undefined; - partyPlayers?: number | undefined; - isExpansion?: boolean | undefined; - } | undefined, -): AnalyticTcResult { - const table = options?.tcTable ?? loadCanonicalTreasureClasses().tcTable; - const autoMap = options?.autoTcs ?? loadCanonicalTreasureClasses().autoTcs; - let rootNode: TreasureClassNode | undefined = table.get(tcName); - if (!rootNode && autoMap.has(tcName)) { - rootNode = autoTcToTreasureClassNode(autoMap.get(tcName)!); - } - - const directResult = calculateDirectProbabilities(tcName, options); - const effectivePlayers = computeEffectivePlayers(options?.gamePlayers, options?.partyPlayers); - - if (!rootNode) { - return { - tcName, - picks: 0, - effectivePlayers, - noDropSingleProb: 1.0, - noDropCompoundProb: 1.0, - direct: directResult.probabilities, - leaves: [], - leafMap: new Map(), - }; - } - - const rootPicks = rootNode.picks ?? 1; - const leafSingleProb = new Map(); - let noDropSingleProb = 0; - - function expandNode(name: string, probWeight: number, depth: number) { - if (probWeight <= 0 || depth > 32) return; - - const tc = table.get(name); - if (tc) { - if (tc.picks < 0) { - for (const it of tc.items) { - const p = (it as any).prob ?? 0; - if (p <= 0) continue; - for (let k = 0; k < p; k++) { - expandNode(it.item, probWeight, depth + 1); - } - } - return; - } - - const baseNoDrop = tc.noDrop ?? 0; - const totalProb = tc.totalProbExpansion ?? (tc as any).totalProb ?? 0; - const scaledNoDrop = computeScaledNoDrop(baseNoDrop, totalProb, effectivePlayers); - const totalWeight = scaledNoDrop + totalProb; - - if (totalWeight <= 0) return; - - if (scaledNoDrop > 0) { - const pNoDrop = (scaledNoDrop / totalWeight) * probWeight; - noDropSingleProb += pNoDrop; - } - - for (const it of tc.items) { - const p = (it as any).prob ?? 0; - if (p <= 0) continue; - const branchProb = (p / totalWeight) * probWeight; - expandNode(it.item, branchProb, depth + 1); - } - return; - } - - const autoNode = autoMap.get(name); - if (autoNode) { - const totalProb = autoNode.totalProb ?? 0; - if (totalProb <= 0) return; - for (const entry of autoNode.entries) { - const branchProb = (entry.prob / totalProb) * probWeight; - expandNode(entry.itemCode, branchProb, depth + 1); - } - return; - } - - let leafCode = name.trim(); - if (leafCode.startsWith('gld,mul=')) { - leafCode = 'gld'; - } - leafSingleProb.set(leafCode, (leafSingleProb.get(leafCode) ?? 0) + probWeight); - } - - expandNode(tcName, 1.0, 0); - - const leaves: AnalyticItemDrop[] = []; - const leafMap = new Map(); - - for (const [code, pSingle] of leafSingleProb) { - const compound = rootPicks < 0 ? Math.min(1, pSingle) : calculateCompoundProbability(pSingle, rootPicks); - const expected = (rootPicks < 0 ? 1 : rootPicks) * pSingle; - const odds = compound > 0 ? 1 / compound : Infinity; - const isGold = code === 'gld'; - const isRune = /^r(?:0[1-9]|[12]\d|3[0-3])$/.test(code); - const name = CANONICAL_ITEM_NAMES[code] ?? code; - - const itemDrop: AnalyticItemDrop = { - item: code, - name, - singleProbability: pSingle, - compoundProbability: compound, - expectedDrops: expected, - oneInOdds: odds, - isGold, - isRune, - }; - leaves.push(itemDrop); - leafMap.set(code, itemDrop); - } - - leaves.sort((a, b) => b.compoundProbability - a.compoundProbability); - - const noDropCompoundProb = rootPicks < 0 ? 0 : Math.pow(Math.min(1, Math.max(0, noDropSingleProb)), rootPicks); - - return { - tcName, - picks: rootPicks, - effectivePlayers, - noDropSingleProb, - noDropCompoundProb, - direct: directResult.probabilities, - leaves, - leafMap, - }; -} - -/** - * Builds reverse lookup index mapping item code to all dropping TreasureClasses. - */ -export function buildReverseLookupIndex( - tcTable: TreasureClassTable, - autoTcs?: Map | undefined, - options?: { gamePlayers?: number | undefined; partyPlayers?: number | undefined } | undefined, -): Map { - const autoMap = autoTcs ?? loadCanonicalTreasureClasses().autoTcs; - const index = new Map(); - - for (const tc of tcTable.all) { - const result = calculateAnalyticProbabilities(tc.name, { - tcTable, - autoTcs: autoMap, - ...(options?.gamePlayers !== undefined ? { gamePlayers: options.gamePlayers } : {}), - ...(options?.partyPlayers !== undefined ? { partyPlayers: options.partyPlayers } : {}), - }); - - for (const leaf of result.leaves) { - if (leaf.item === 'NoDrop') continue; - let list = index.get(leaf.item); - if (!list) { - list = []; - index.set(leaf.item, list); - } - list.push({ - tcName: tc.name, - picks: tc.picks, - singleProbability: leaf.singleProbability, - compoundProbability: leaf.compoundProbability, - oneInOdds: leaf.oneInOdds, - }); - } - } - - for (const list of index.values()) { - list.sort((a, b) => b.compoundProbability - a.compoundProbability); - } - - return index; -} - -/** - * Performs reverse lookup query for an item code or name. - */ -export function reverseLookup( - index: Map, - query: string, - itemNames?: Record, -): ReverseLookupResult[] { - const names = itemNames ?? CANONICAL_ITEM_NAMES; - const q = query.trim().toLowerCase(); - if (!q) return []; - - const matchedCodes = new Set(); - - if (index.has(q)) { - matchedCodes.add(q); - } - - for (const [code, name] of Object.entries(names)) { - const lowerName = name.toLowerCase(); - if (code.toLowerCase() === q || lowerName.includes(q)) { - if (index.has(code)) { - matchedCodes.add(code); - } - } - } - - if (q === 'shako' && index.has('uap')) matchedCodes.add('uap'); - if (q === 'zod' && index.has('r33')) matchedCodes.add('r33'); - if (q === 'ber' && index.has('r30')) matchedCodes.add('r30'); - if (q === 'jah' && index.has('r31')) matchedCodes.add('r31'); - if (q === 'soj' && index.has('rin')) matchedCodes.add('rin'); - - const results: ReverseLookupResult[] = []; - for (const code of matchedCodes) { - const entries = index.get(code) ?? []; - results.push({ - query, - matchedCode: code, - matchedName: names[code] ?? code, - entries, - }); - } - - return results; -} - -const QUALITY_RANK: Record = { - unique: 6, - set: 5, - rare: 4, - magic: 3, - superior: 2, - normal: 1, - gold: 0, - rune: 7, -}; - -export async function runMonteCarloSimulation( - options: SimulationOptions, - onProgress?: (progress: SimulationProgress) => void | Promise, -): Promise { - const tcTable = options.tcTable ?? loadCanonicalTreasureClasses().tcTable; - const autoTcs = options.autoTcs ?? loadCanonicalTreasureClasses().autoTcs; - if (options.tcTable && options.autoTcs) { - mergeAutoTreasureClasses(tcTable, autoTcs); - } - const itemRatio = options.itemRatioTable ?? createItemRatioTable(CANONICAL_ITEM_RATIO_ROWS); - const ratioRow = itemRatio.getRow(1, 0, 0) ?? CANONICAL_ITEM_RATIO_ROWS[2]!; - - let effectiveTcName = options.tcName; - if (options.isQuestDrop) { - const questTc = effectiveTcName.replace(/^(Andariel|Duriel|Mephisto|Diablo|Baal)(.*)$/, '$1q$2'); - if (tcTable.get(questTc)) { - effectiveTcName = questTc; - } - } - - // Critical fix: When seed is undefined or 0, initialize with non-zero pseudorandom 32-bit integer - // to avoid D2 LCG (0 * multiplier + 0 = 0) permanent zero lock. - const initialSeed = (options.seed !== undefined && options.seed !== 0) - ? options.seed - : ((Math.floor(Math.random() * 0x7fffffff) + 1) >>> 0); - const rng = new D2Rng(initialSeed); - const totalRuns = options.runs; - const chunkSize = options.chunkSize ?? Math.max(100, Math.min(5000, Math.floor(totalRuns / 20))); - - const qualityCounts: QualityCounts = { - normal: 0, - superior: 0, - magic: 0, - rare: 0, - set: 0, - unique: 0, - gold: 0, - rune: 0, - noDrop: 0, - }; - - const itemCounts = new Map(); - let totalDropped = 0; - let emptyRuns = 0; - - let mlvl = 85; - if (options.difficulty === 'normal') mlvl = 20; - else if (options.difficulty === 'nightmare') mlvl = 55; - - let completedRuns = 0; - - while (completedRuns < totalRuns) { - const currentChunk = Math.min(chunkSize, totalRuns - completedRuns); - - for (let i = 0; i < currentChunk; i++) { - const drops = rollTreasureClass(effectiveTcName, { - rng, - tcTable, - gamePlayers: options.gamePlayers ?? 1, - partyPlayers: options.partyPlayers ?? 1, - isExpansion: true, - }); - - if (drops.length === 0) { - emptyRuns++; - qualityCounts.noDrop++; - continue; - } - - for (const drop of drops) { - totalDropped++; - const code = drop.code; - let quality = 'normal'; - - if (drop.isGold) { - quality = 'gold'; - qualityCounts.gold++; - } else if (/^r(?:0[1-9]|[12]\d|3[0-3])$/.test(code)) { - quality = 'rune'; - qualityCounts.rune++; - } else if (drop.forcedUnique) { - quality = 'unique'; - qualityCounts.unique++; - } else if (drop.forcedSet) { - quality = 'set'; - qualityCounts.set++; - } else { - const tier = rollItemQuality({ - ilvl: mlvl, - qlvl: 10, - magicFind: options.magicFind ?? 0, - row: ratioRow, - tcFactors: { - unique: drop.qualityFactors[0], - set: drop.qualityFactors[1], - rare: drop.qualityFactors[2], - magic: drop.qualityFactors[3], - }, - rng, - }); - quality = tier === 'low' ? 'normal' : tier; - if (quality === 'unique') qualityCounts.unique++; - else if (quality === 'set') qualityCounts.set++; - else if (quality === 'rare') qualityCounts.rare++; - else if (quality === 'magic') qualityCounts.magic++; - else if (quality === 'superior') qualityCounts.superior++; - else qualityCounts.normal++; - } - - const existing = itemCounts.get(code); - if (existing) { - existing.count++; - if ((QUALITY_RANK[quality] ?? 0) > (QUALITY_RANK[existing.quality] ?? 0)) { - existing.quality = quality; - } - } else { - itemCounts.set(code, { count: 1, quality }); - } - } - } - - completedRuns += currentChunk; - - if (onProgress) { - await onProgress({ - completedRuns, - totalRuns, - itemsDropped: totalDropped, - qualityCounts: { ...qualityCounts }, - isFinished: completedRuns >= totalRuns, - isPaused: false, - }); - await new Promise(r => setTimeout(r, 0)); - } - } - - const itemSummaries: DroppedItemSummary[] = []; - for (const [code, data] of itemCounts) { - itemSummaries.push({ - code, - name: CANONICAL_ITEM_NAMES[code] ?? code, - quality: data.quality, - count: data.count, - percentage: (data.count / totalRuns) * 100, - }); - } - itemSummaries.sort((a, b) => b.count - a.count); - - return { - totalRuns, - itemsDropped: totalDropped, - emptyRuns, - averageDropsPerRun: totalRuns > 0 ? totalDropped / totalRuns : 0, - qualityCounts, - itemSummaries, - }; -} - -// --- Maxroll-Inspired Monster Selector & Rich Item Presentation -------------- - -export type MonsterCategory = 'boss' | 'superunique' | 'farm' | 'chest' | 'normal'; - -export interface MonsterPreset { - readonly id: string; - readonly nameCn: string; - readonly nameEn: string; - readonly category: MonsterCategory; - readonly act: 1 | 2 | 3 | 4 | 5; - readonly areaCn: string; - readonly areaEn: string; - readonly mlvl: { normal: number; nightmare: number; hell: number }; - readonly tc: { normal: string; nightmare: string; hell: string }; - readonly questTc?: { normal: string; nightmare: string; hell: string }; - readonly picks: number; - readonly baseNoDrop: number; - readonly icon: string; - readonly descCn: string; -} - -export const MONSTER_PRESETS: readonly MonsterPreset[] = [ - // --- Act Bosses --- - { - id: 'andariel', - nameCn: '安达利尔 (Andariel)', - nameEn: 'Maiden of Anguish', - category: 'boss', - act: 1, - areaCn: '地下墓穴第四层 (Catacombs L4)', - areaEn: 'Catacombs Level 4', - mlvl: { normal: 12, nightmare: 49, hell: 75 }, - tc: { normal: 'Andariel', nightmare: 'Andariel (N)', hell: 'Andariel (H)' }, - questTc: { normal: 'Andarielq', nightmare: 'Andarielq (N)', hell: 'Andarielq (H)' }, - picks: 7, - baseNoDrop: 19, - icon: '🕷️', - descCn: '第一幕关底首领,弱火(-50%火抗),Bug首杀热门目标,乔丹之石 (SOJ) 与暗金首饰核心掉落源。', - }, - { - id: 'duriel', - nameCn: '督瑞尔 (Duriel)', - nameEn: 'Lord of Pain', - category: 'boss', - act: 2, - areaCn: '塔·拉夏的古墓 (Tal Rasha Chamber)', - areaEn: "Tal Rasha's Chamber", - mlvl: { normal: 22, nightmare: 55, hell: 88 }, - tc: { normal: 'Duriel', nightmare: 'Duriel (N)', hell: 'Duriel (H)' }, - questTc: { normal: 'Durielq', nightmare: 'Durielq (N)', hell: 'Durielq (H)' }, - picks: 7, - baseNoDrop: 19, - icon: '🐛', - descCn: '第二幕关底首领,神圣冰冻光环,回城卷轴与日用品暗金出处。', - }, - { - id: 'mephisto', - nameCn: '墨菲斯托 (Mephisto)', - nameEn: 'Lord of Hatred', - category: 'boss', - act: 3, - areaCn: '憎恨囚牢第三层 (Durance of Hate L3)', - areaEn: 'Durance of Hate Level 3', - mlvl: { normal: 26, nightmare: 59, hell: 87 }, - tc: { normal: 'Mephisto', nightmare: 'Mephisto (N)', hell: 'Mephisto (H)' }, - questTc: { normal: 'Mephistoq', nightmare: 'Mephistoq (N)', hell: 'Mephistoq (H)' }, - picks: 7, - baseNoDrop: 15, - icon: '💀', - descCn: '第三幕关底首领,全游戏刷军帽、暴风之盾、眼球、海蛇皮等中高级暗金最高效目标。', - }, - { - id: 'diablo', - nameCn: '暗黑破坏神 (Diablo)', - nameEn: 'Lord of Terror', - category: 'boss', - act: 4, - areaCn: '混沌避难所 (Chaos Sanctuary)', - areaEn: 'Chaos Sanctuary', - mlvl: { normal: 40, nightmare: 62, hell: 94 }, - tc: { normal: 'Diablo', nightmare: 'Diablo (N)', hell: 'Diablo (H)' }, - questTc: { normal: 'Diabloq', nightmare: 'Diabloq (N)', hell: 'Diabloq (H)' }, - picks: 7, - baseNoDrop: 15, - icon: '👹', - descCn: '第四幕关底首领,红色闪电地狱火,mlvl高达94,掉落大君之怒、马拉等顶级首饰。', - }, - { - id: 'baal', - nameCn: '巴尔 (Baal)', - nameEn: 'Lord of Destruction', - category: 'boss', - act: 5, - areaCn: '世界之石大厅 (Worldstone Chamber)', - areaEn: 'Worldstone Chamber', - mlvl: { normal: 60, nightmare: 75, hell: 99 }, - tc: { normal: 'Baal', nightmare: 'Baal (N)', hell: 'Baal (H)' }, - questTc: { normal: 'Baalq', nightmare: 'Baalq (N)', hell: 'Baalq (H)' }, - picks: 7, - baseNoDrop: 15, - icon: '👿', - descCn: '第五幕关底终极首领,全游戏最高等级(mlvl 99),TC 87全解锁,可掉落全游戏所有暗金底模。', - }, - - // --- Superuniques --- - { - id: 'countess', - nameCn: '女伯爵 (The Countess)', - nameEn: 'The Countess', - category: 'superunique', - act: 1, - areaCn: '遗忘之塔第五层 (Tower Cellar L5)', - areaEn: 'Tower Cellar Level 5', - mlvl: { normal: 12, nightmare: 45, hell: 82 }, - tc: { normal: 'Countess', nightmare: 'Countess (N)', hell: 'Countess (H)' }, - picks: 5, - baseNoDrop: 19, - icon: '🏰', - descCn: '符文之王,拥有独立符文特殊掉落池(最高掉落28#罗),掉落1#恐惧之钥 (Key of Terror)。', - }, - { - id: 'summoner', - nameCn: '召唤者 (The Summoner)', - nameEn: 'The Summoner', - category: 'superunique', - act: 2, - areaCn: '神秘避难所 (Arcane Sanctuary)', - areaEn: 'Arcane Sanctuary', - mlvl: { normal: 18, nightmare: 55, hell: 80 }, - tc: { normal: 'Summoner', nightmare: 'Summoner (N)', hell: 'Summoner (H)' }, - picks: 5, - baseNoDrop: 19, - icon: '🔮', - descCn: '神秘避难所尽头守护者,赫拉迪克真理追寻者,掉落2#憎恨之钥 (Key of Hate)。', - }, - { - id: 'nihlathak', - nameCn: '尼拉塞克 (Nihlathak)', - nameEn: 'Nihlathak', - category: 'superunique', - act: 5, - areaCn: '瓦特之厅 (Halls of Vaught)', - areaEn: 'Halls of Vaught', - mlvl: { normal: 65, nightmare: 70, hell: 95 }, - tc: { normal: 'Nihlathak', nightmare: 'Nihlathak (N)', hell: 'Nihlathak (H)' }, - picks: 5, - baseNoDrop: 19, - icon: '☠️', - descCn: '哈洛加斯背叛者,极速尸体爆炸致命,mlvl高达95,掉落3#毁灭之钥 (Key of Destruction)。', - }, - { - id: 'pindleskin', - nameCn: '暴躁外皮 / 皮叔 (Pindleskin)', - nameEn: 'Pindleskin', - category: 'superunique', - act: 5, - areaCn: '尼拉塞克神殿红门 (Nihlathak Temple)', - areaEn: "Nihlathak's Temple", - mlvl: { normal: 68, nightmare: 73, hell: 86 }, - tc: { normal: 'Act 5 Super Ax', nightmare: 'Act 5 (N) Super Ax', hell: 'Act 5 (H) Super Cx' }, - picks: 2, - baseNoDrop: 0, - icon: '🧟', - descCn: '红门十五秒速刷首选,除了泰瑞尔之力、蜘蛛之网等少数外可出几乎一切暗金。', - }, - { - id: 'council', - nameCn: '崔凡克最高议会 (Travincal Council)', - nameEn: 'High Council', - category: 'superunique', - act: 3, - areaCn: '崔凡克神殿 (Travincal)', - areaEn: 'Travincal', - mlvl: { normal: 28, nightmare: 57, hell: 85 }, - tc: { normal: 'Council', nightmare: 'Council (N)', hell: 'Council (H)' }, - picks: 3, - baseNoDrop: 19, - icon: '🪙', - descCn: '瘸子群,打钱野蛮人圣地,极高的高级符文(最高至33#萨德)产出率。', - }, - { - id: 'shenk_eldritch', - nameCn: '督军山克与亚瑞克 (Shenk & Eldritch)', - nameEn: 'Shenk & Eldritch', - category: 'superunique', - act: 5, - areaCn: '冰冻高地小站 (Frigid Highlands)', - areaEn: 'Frigid Highlands', - mlvl: { normal: 65, nightmare: 71, hell: 84 }, - tc: { normal: 'Act 5 Super A', nightmare: 'Act 5 (N) Super A', hell: 'Act 5 (H) Super Cx' }, - picks: 2, - baseNoDrop: 0, - icon: '⚔️', - descCn: '冰冻高地小站上下两侧,出门即打,起步速刷固定暗金怪双雄。', - }, - { - id: 'cow_king', - nameCn: '母牛之王 / 牛王 (Cow King)', - nameEn: 'The Cow King', - category: 'superunique', - act: 1, - areaCn: '秘密母牛关 (Secret Cow Level)', - areaEn: 'The Secret Cow Level', - mlvl: { normal: 31, nightmare: 67, hell: 84 }, - tc: { normal: 'Cow King', nightmare: 'Cow King (N)', hell: 'Cow King (H)' }, - picks: 5, - baseNoDrop: 19, - icon: '🐮', - descCn: '母牛之王,掉落牛王皮靴、牛王生皮、牛王之角专属套装。', - }, - { - id: 'radament', - nameCn: '罗达门特 (Radament)', - nameEn: 'Radament', - category: 'superunique', - act: 2, - areaCn: '下水道第三层 (Sewers L3)', - areaEn: 'Sewers Level 3', - mlvl: { normal: 16, nightmare: 49, hell: 83 }, - tc: { normal: 'Radament', nightmare: 'Radament (N)', hell: 'Radament (H)' }, - picks: 5, - baseNoDrop: 19, - icon: '📜', - descCn: '复活木乃伊群首领,技能之书任务目标。', - }, - { - id: 'izual', - nameCn: '衣卒尔 (Izual)', - nameEn: 'Izual', - category: 'superunique', - act: 4, - areaCn: '绝望平原 (Plains of Despair)', - areaEn: 'Plains of Despair', - mlvl: { normal: 29, nightmare: 60, hell: 86 }, - tc: { normal: 'Izual', nightmare: 'Izual (N)', hell: 'Izual (H)' }, - picks: 5, - baseNoDrop: 19, - icon: '👼', - descCn: '被诅咒的堕落天使,击杀奖励2点珍贵技能点。', - }, - { - id: 'hephasto', - nameCn: '海法斯特盔甲制作者 (Hephasto)', - nameEn: 'Hephasto the Armorer', - category: 'superunique', - act: 4, - areaCn: '地狱熔炉 (River of Flame)', - areaEn: 'River of Flame', - mlvl: { normal: 28, nightmare: 59, hell: 88 }, - tc: { normal: 'Act 4 Super C', nightmare: 'Act 4 (N) Super C', hell: 'Act 4 (H) Super C' }, - picks: 2, - baseNoDrop: 0, - icon: '⚒️', - descCn: '地狱熔炉铁锤守护者,极高物理攻击与狂热灵气。', - }, - - // --- Farming Areas --- - { - id: 'cows', - nameCn: '秘密母牛关 母牛群 (Hell Bovine)', - nameEn: 'Hell Bovine', - category: 'farm', - act: 1, - areaCn: '秘密母牛关 (Secret Cow Level)', - areaEn: 'The Secret Cow Level', - mlvl: { normal: 28, nightmare: 64, hell: 81 }, - tc: { normal: 'Cow', nightmare: 'Cow (N)', hell: 'Cow (H)' }, - picks: 1, - baseNoDrop: 100, - icon: '🐄', - descCn: '超大密度怪物群,标马与尸爆死灵最爱,极佳的符文之语白色底模与高级符文出产地。', - }, - { - id: 'wraith_farm', - nameCn: '幽灵怪 / 恶灵 / 鬼火 (Ghosts & Wraiths)', - nameEn: 'Ghosts & Wraiths', - category: 'farm', - act: 2, - areaCn: '神秘避难所 / 世界之石要塞', - areaEn: 'Arcane Sanctuary & Worldstone', - mlvl: { normal: 15, nightmare: 45, hell: 83 }, - tc: { normal: 'Act 1 Wraith A', nightmare: 'Act 3 (N) Wraith C', hell: 'Act 5 (H) Wraith C' }, - picks: 1, - baseNoDrop: 100, - icon: '👻', - descCn: '物理免疫浮空怪,剔除了绝大部分普通装备杂质,高级符文产出期望比常规小怪高约3.5倍!', - }, - { - id: 'pit_unique', - nameCn: '85级场景 随机暗金首领 (Level 85 Area Unique)', - nameEn: 'Level 85 Area Boss', - category: 'farm', - act: 1, - areaCn: '地穴 / 古代通道 / 混沌避难所', - areaEn: 'The Pit, Ancient Tunnels, Chaos', - mlvl: { normal: 20, nightmare: 60, hell: 88 }, - tc: { normal: 'Act 5 Unique A', nightmare: 'Act 5 (N) Unique A', hell: 'Act 5 (H) Unique C' }, - picks: 2, - baseNoDrop: 0, - icon: '👑', - descCn: 'TC 87 全解锁,mlvl 88,可以掉落全游戏包括风之力、格利风之眼在内的所有暗金神器。', - }, - { - id: 'pit_champion', - nameCn: '85级场景 冠军蓝怪 (Level 85 Area Champion)', - nameEn: 'Level 85 Area Champion', - category: 'farm', - act: 1, - areaCn: '地穴 / 古代通道 / 混沌避难所', - areaEn: 'Level 85 Zones', - mlvl: { normal: 19, nightmare: 59, hell: 87 }, - tc: { normal: 'Act 5 Champ A', nightmare: 'Act 5 (N) Champ A', hell: 'Act 5 (H) Champ C' }, - picks: -2, - baseNoDrop: 0, - icon: '🔷', - descCn: '85场景蓝怪小队,确定性掉落2次,丰厚经验值与高级底模出产地。', - }, - - // --- Chests --- - { - id: 'lk_superchests', - nameCn: '库拉斯特下层 超级宝箱 (LK Superchests)', - nameEn: 'LK Superchests', - category: 'chest', - act: 3, - areaCn: '库拉斯特下层 (Lower Kurast)', - areaEn: 'Lower Kurast', - mlvl: { normal: 22, nightmare: 52, hell: 80 }, - tc: { normal: 'Act 3 Chest A', nightmare: 'Act 3 (N) Chest C', hell: 'Act 3 (H) Chest C' }, - picks: 4, - baseNoDrop: 100, - icon: '📦', - descCn: '著名LK双营火6箱子速开,单人模式下高级符文(最高至30#贝)与技能大板的最高效出处。', - }, - - // --- Normal Mobs --- - { - id: 'act1_h2h', - nameCn: '第一幕 近战小怪 (Act 1 Melee)', - nameEn: 'Act 1 Fallen / Zombie', - category: 'normal', - act: 1, - areaCn: '鲜血荒地至修道院', - areaEn: 'Blood Moor to Monastery', - mlvl: { normal: 3, nightmare: 38, hell: 68 }, - tc: { normal: 'Act 1 H2H A', nightmare: 'Act 1 (N) H2H A', hell: 'Act 1 (H) H2H A' }, - picks: 1, - baseNoDrop: 100, - icon: '🗡️', - descCn: '沉沦魔、僵尸等第一幕近战小怪基准掉落。', - }, - { - id: 'act5_h2h', - nameCn: '第五幕 近战小怪 (Act 5 Melee)', - nameEn: 'Act 5 Minions', - category: 'normal', - act: 5, - areaCn: '毁灭王座与世界之石要塞', - areaEn: 'Worldstone Keep', - mlvl: { normal: 36, nightmare: 62, hell: 85 }, - tc: { normal: 'Act 5 H2H C', nightmare: 'Act 5 (N) H2H C', hell: 'Act 5 (H) H2H C' }, - picks: 1, - baseNoDrop: 100, - icon: '🛡️', - descCn: '第五幕毁灭王座强力近战小怪基准掉落。', - }, -]; - -/** - * Resolves the effective TreasureClass name for a given monster preset, difficulty, and quest drop status. - * If customOverride is provided, customOverride takes precedence. - */ -export function getMonsterPresetTc( - monster: MonsterPreset, - difficulty: 'normal' | 'nightmare' | 'hell', - quest: boolean = false, - customOverride?: string | null, -): string { - if (customOverride && customOverride.trim()) { - return customOverride.trim(); - } - if (quest && monster.questTc) { - return monster.questTc[difficulty]; - } - return monster.tc[difficulty]; -} - -export type ItemCategoryFilter = 'all' | 'unique' | 'set' | 'rune' | 'base' | 'charm_gem' | 'misc'; - -export const FAMOUS_ITEMS_META: Readonly> = { - uap: { famousTitle: '谐角之冠 (Harlequin Crest) · 军帽', itemTypeCn: '防具 · 头盔', qlvl: 69 }, - uit: { famousTitle: '暴风之盾 (Stormshield) · 统御者大盾', itemTypeCn: '防具 · 盾牌', qlvl: 77 }, - ci3: { famousTitle: "格利风之眼 (Griffon's Eye) · 权冠", itemTypeCn: '防具 · 头环', qlvl: 85 }, - ci2: { famousTitle: "奇拉的守护 (Kira's Guardian) · 头饰", itemTypeCn: '防具 · 头环', qlvl: 77 }, - utp: { famousTitle: '执政官铠甲 · 极品谜团/刚毅轻甲底模', itemTypeCn: '防具 · 铠甲', qlvl: 84 }, - xtp: { famousTitle: '法师铠甲 · 主流3孔谜团底模', itemTypeCn: '防具 · 铠甲', qlvl: 60 }, - uar: { famousTitle: "泰瑞尔之力 (Tyrael's Might) · 最稀有暗金", itemTypeCn: '防具 · 重甲', qlvl: 87 }, - xea: { famousTitle: '蛇魔法师之皮 (Skin of the Vipermagi) · 海蛇皮', itemTypeCn: '防具 · 铠甲', qlvl: 37 }, - xsk: { famousTitle: "塔·拉夏的赫拉迪克纹章 · 死亡面具", itemTypeCn: '防具 · 头盔', qlvl: 42 }, - xui: { famousTitle: "诗寇蒂的愤怒 (Skullder's Ire) · 幽灵战甲", itemTypeCn: '防具 · 铠甲', qlvl: 50 }, - urn: { famousTitle: '年纪之冠 (Crown of Ages) · 日冕之冠', itemTypeCn: '防具 · 头盔', qlvl: 86 }, - ulc: { famousTitle: '蜘蛛之网 (Arachnid Mesh) · 蛛网腰带 (技能腰)', itemTypeCn: '防具 · 腰带', qlvl: 87 }, - umc: { famousTitle: "维尔登戈的心结 (Verdungo's Hearty Cord) · 秘银腰带", itemTypeCn: '防具 · 腰带', qlvl: 71 }, - xtb: { famousTitle: '战争之旅 (War Traveler) · 战旅50MF打宝靴', itemTypeCn: '防具 · 靴子', qlvl: 50 }, - xhb: { famousTitle: '蚀肉骑士 (Gore Rider) · 骑士靴 (CB/DS/OW)', itemTypeCn: '防具 · 靴子', qlvl: 55 }, - uvb: { famousTitle: '沙暴之旅 (Sandstorm Trek) · 蛛网之靴', itemTypeCn: '防具 · 靴子', qlvl: 74 }, - umb: { famousTitle: '骨髓行走 (Marrowwalk) · 骸骨之靴', itemTypeCn: '防具 · 靴子', qlvl: 74 }, - tgl: { famousTitle: '法师之拳 (Magefist) · 轻型铁手套 (20FCR)', itemTypeCn: '防具 · 手套', qlvl: 31 }, - '7cr': { famousTitle: '碧蓝怒火 / 悔恨/最后希望 极品幻化之刃底模', itemTypeCn: '武器 · 单手剑', qlvl: 73 }, - '7wa': { famousTitle: '狂战士斧 · 死亡之刀 / 悔恨/野兽顶级底模', itemTypeCn: '武器 · 单手斧', qlvl: 86 }, - '7pa': { famousTitle: '神秘之斧 · 佣兵眼光/无限 极品长柄底模', itemTypeCn: '武器 · 长柄武器', qlvl: 79 }, - '7vo': { famousTitle: '巨神之斧 · 必出4孔 佣兵眼光/无限底模', itemTypeCn: '武器 · 长柄武器', qlvl: 84 }, - '7s8': { famousTitle: "死神的丧钟 (The Reaper's Toll) · 锐利之斧", itemTypeCn: '武器 · 长柄武器', qlvl: 71 }, - '7wc': { famousTitle: '巨神长柄斧 · 攻速底模', itemTypeCn: '武器 · 长柄武器', qlvl: 86 }, - amc: { famousTitle: '大院长之弓 · +3技能 信心 (Faith) 毕业底模', itemTypeCn: '武器 · 亚马逊弓', qlvl: 78 }, - '6lw': { famousTitle: '风之力 (Windforce) · 九头蛇弓', itemTypeCn: '武器 · 弓', qlvl: 85 }, - oba: { famousTitle: '眼球 (The Oculus) · 涡流水晶', itemTypeCn: '武器 · 法师球', qlvl: 50 }, - obf: { famousTitle: "死亡深度 (Death's Fathom) · 次元碎片", itemTypeCn: '武器 · 法师球', qlvl: 85 }, - '7gw': { famousTitle: "破隐一击 (Death's Web) · 破隐法杖", itemTypeCn: '武器 · 死灵法杖', qlvl: 86 }, - baa: { famousTitle: "亚瑞特的面容 (Arreat's Face) · 飞机头", itemTypeCn: '野蛮人专属', qlvl: 50 }, - dra: { famousTitle: "加尔的长发 (Jalal's Mane) · 狼头", itemTypeCn: '德鲁伊专属', qlvl: 50 }, - pa9: { famousTitle: '撒卡兰姆使者 (Herald of Zakarum) · 饰金盾', itemTypeCn: '圣骑士专属', qlvl: 50 }, - pab: { famousTitle: '神圣小盾 · 极品45全抗 流亡/精神底模', itemTypeCn: '圣骑士专属', qlvl: 63 }, - ne9: { famousTitle: '侏儒 (Homunculus) · 圣堂总管之颅', itemTypeCn: '死灵法师专属', qlvl: 50 }, - rin: { famousTitle: '乔丹之石 (SOJ) / 乌鸦之霜 / 婚戒', itemTypeCn: '珠宝首饰 · 戒指', qlvl: 1 }, - amu: { famousTitle: '马拉的万花筒 / 大君之怒 / 塔拉夏护身符', itemTypeCn: '珠宝首饰 · 护身符', qlvl: 1 }, - jew: { famousTitle: '彩虹刻面 (Rainbow Facet) / 40增伤15攻速宝珠', itemTypeCn: '珠宝首饰 · 珠宝', qlvl: 1 }, - cm1: { famousTitle: '小护身符 · 20生命5全抗 / 毁灭 (Annihilus)', itemTypeCn: '护身符 · 小护身符', qlvl: 1 }, - cm2: { famousTitle: '中护身符 · 地狱火炬 (Hellfire Torch)', itemTypeCn: '护身符 · 中护身符', qlvl: 1 }, - cm3: { famousTitle: '超大护身符 · 职业+1技能板 / 吉黑德的运气', itemTypeCn: '护身符 · 超大护身符', qlvl: 1 }, - pk1: { famousTitle: '恐惧之钥 (Key of Terror) · 女伯爵掉落', itemTypeCn: '任务道具 · 钥匙', qlvl: 80 }, - pk2: { famousTitle: '憎恨之钥 (Key of Hate) · 召唤者掉落', itemTypeCn: '任务道具 · 钥匙', qlvl: 80 }, - pk3: { famousTitle: '毁灭之钥 (Key of Destruction) · 尼拉塞克掉落', itemTypeCn: '任务道具 · 钥匙', qlvl: 80 }, - mbr: { famousTitle: '痛苦之精华 · 安姐/督瑞尔掉落', itemTypeCn: '洗点材料 · 精华', qlvl: 80 }, - dhn: { famousTitle: '憎恨之精华 · 劳模掉落', itemTypeCn: '洗点材料 · 精华', qlvl: 80 }, - bet: { famousTitle: '恐惧之精华 · 大菠萝掉落', itemTypeCn: '洗点材料 · 精华', qlvl: 80 }, - fed: { famousTitle: '毁灭之精华 · 巴尔掉落', itemTypeCn: '洗点材料 · 精华', qlvl: 80 }, - tes: { famousTitle: '赦免徽章 (Token of Absolution) · 4色精华洗点水', itemTypeCn: '重置道具', qlvl: 80 }, - r20: { famousTitle: '20# 蓝姆 (Lem) · 财富/打钱蛮子', itemTypeCn: '高级符文 · 20#', qlvl: 43 }, - r21: { famousTitle: '21# 普尔 (Pul) · 誓约/石块', itemTypeCn: '高级符文 · 21#', qlvl: 45 }, - r22: { famousTitle: '22# 乌姆 (Um) · 强制/盔甲镶嵌22全抗', itemTypeCn: '高级符文 · 22#', qlvl: 47 }, - r23: { famousTitle: '23# 马尔 (Mal) · 战争召唤/弑君者', itemTypeCn: '高级符文 · 23#', qlvl: 49 }, - r24: { famousTitle: '24# 伊司特 (Ist) · 战争召唤/25MF打宝神珠', itemTypeCn: '高级符文 · 24#', qlvl: 51 }, - r25: { famousTitle: '25# 古尔 (Gul) · 死亡之呼吸/橡树之心', itemTypeCn: '高级符文 · 25#', qlvl: 53 }, - r26: { famousTitle: '26# 伐克斯 (Vex) · 橡树之心 (HOTO)/凤凰', itemTypeCn: '大号符文 · 26#', qlvl: 55 }, - r27: { famousTitle: '27# 欧姆 (Ohm) · 战争召唤 (CTA)/混沌', itemTypeCn: '大号符文 · 27#', qlvl: 57 }, - r28: { famousTitle: '28# 罗 (Lo) · 刚毅 (Fortitude)/悔恨 (Grief)', itemTypeCn: '大号符文 · 28#', qlvl: 59 }, - r29: { famousTitle: '29# 瑟 (Sur) · 双瑟合贝/最后希望', itemTypeCn: '大号符文 · 29#', qlvl: 61 }, - r30: { famousTitle: '30# 贝 (Ber) · 谜团 (Enigma)/无限 (Infinity)/野兽', itemTypeCn: '核心神级符文 · 30#', qlvl: 63 }, - r31: { famousTitle: '31# 乔 (Jah) · 谜团 (Enigma)/最后希望/信仰', itemTypeCn: '核心神级符文 · 31#', qlvl: 65 }, - r32: { famousTitle: '32# 查姆 (Cham) · 傲慢/末日/不灭神珠', itemTypeCn: '大号符文 · 32#', qlvl: 67 }, - r33: { famousTitle: '33# 萨德 (Zod) · 死亡之呼吸 (BOTD)/无法破坏', itemTypeCn: '终极符文 · 33#', qlvl: 69 }, -}; - -export function classifyItemCategory(code: string, quality?: string): ItemCategoryFilter { - if (code.startsWith('r') && /^r(?:0[1-9]|[12]\d|3[0-3])$/.test(code)) { - return 'rune'; - } - if (['jew', 'cm1', 'cm2', 'cm3', 'rin', 'amu'].includes(code)) { - return 'charm_gem'; - } - if (/^g[cfszpy][vrbygw]$/.test(code) || /^sk[cfulz]$/.test(code)) { - return 'charm_gem'; - } - if ( - ['pk1', 'pk2', 'pk3', 'dhn', 'mbr', 'bet', 'fed', 'tes', 'ceh', 'std', 'gld', 'key', 'tsc', 'isc'].includes(code) || - /^[hm]p[1-5]$/.test(code) || - ['rvs', 'rvl', 'vps', 'yps', 'wms'].includes(code) - ) { - return 'misc'; - } - if (quality === 'unique') return 'unique'; - if (quality === 'set') return 'set'; - return 'base'; -} - -// --- Browser DOM Controller -------------------------------------------------- - -if (typeof document !== 'undefined') { - window.addEventListener('DOMContentLoaded', () => { - initLootUi(); - }); -} - -export function initLootUi(): void { - const { tcTable, autoTcs } = loadCanonicalTreasureClasses(); - - // State - let currentDifficulty: 'normal' | 'nightmare' | 'hell' = 'hell'; - let currentMonster: MonsterPreset = MONSTER_PRESETS.find(m => m.id === 'baal') ?? MONSTER_PRESETS[0]!; - let isQuestDrop = false; - let customTcOverride: string | null = null; - let activeMonsterCategory: MonsterCategory | 'all' = 'all'; - let activeItemCategory: ItemCategoryFilter = 'all'; - let currentSortOrder: 'odds-desc' | 'odds-asc' | 'quality' | 'name' | 'qlvl' = 'odds-desc'; - let activeViewTab: 'exact' | 'sim' | 'reverse' = 'exact'; - let itemSearchQuery = ''; - - let isSimRunning = false; - let isSimPaused = false; - let abortRequested = false; - - let currentAnalyticResult: AnalyticTcResult | null = null; - let currentSimResult: SimulationResult | null = null; - let latestSummaries: DroppedItemSummary[] = []; - - // DOM Elements - Navigation & Difficulty - const diffButtons = document.querySelectorAll('.diff-btn'); - const difficultySelect = document.getElementById('difficulty') as HTMLSelectElement | null; - - // Monster Selector Elements - const monsterSearchInput = document.getElementById('monster-search-input') as HTMLInputElement | null; - const monsterCatChips = document.querySelectorAll('.monster-cat-chip'); - const monsterListContainer = document.getElementById('monster-list'); - const presetSelect = document.getElementById('tc-preset') as HTMLSelectElement | null; - - // Monster Spec Card Elements - const monsterCardIcon = document.getElementById('monster-card-icon'); - const monsterCardTitle = document.getElementById('monster-card-title'); - const monsterCardBadge = document.getElementById('monster-card-badge'); - const monsterCardArea = document.getElementById('monster-card-area'); - const monsterCardMlvl = document.getElementById('monster-card-mlvl'); - const monsterCardPicks = document.getElementById('monster-card-picks'); - const monsterCardNodrop = document.getElementById('monster-card-nodrop'); - const monsterCardTc = document.getElementById('monster-card-tc'); - const monsterCardDesc = document.getElementById('monster-card-desc'); - const questDropToggle = document.getElementById('quest-drop-toggle') as HTMLInputElement | null; - const questDropCheckbox = document.getElementById('quest-drop') as HTMLInputElement | null; - const questDropBanner = document.getElementById('quest-drop-banner'); - const tcInput = document.getElementById('tc-input') as HTMLInputElement | null; - const btnApplyCustomTc = document.getElementById('btn-apply-custom-tc') as HTMLButtonElement | null; - - // Settings - const gamePlayersInput = document.getElementById('game-players') as HTMLInputElement | null; - const partyPlayersInput = document.getElementById('party-players') as HTMLInputElement | null; - const playerChips = document.querySelectorAll('.player-chip'); - const mfInput = document.getElementById('magic-find') as HTMLInputElement | null; - const mfChips = document.querySelectorAll('.mf-chip'); - const effectivePlayersHint = document.getElementById('effective-players-hint'); - - // Simulation Controls - const runsSelect = document.getElementById('sim-runs') as HTMLSelectElement | null; - const seedInput = document.getElementById('sim-seed') as HTMLInputElement | null; - const btnStart = document.getElementById('btn-start') as HTMLButtonElement | null; - const btnPause = document.getElementById('btn-pause') as HTMLButtonElement | null; - const btnStop = document.getElementById('btn-stop') as HTMLButtonElement | null; - const btnCalcExact = document.getElementById('btn-calc-exact') as HTMLButtonElement | null; - const progressBar = document.getElementById('sim-progress-bar') as HTMLElement | null; - const progressText = document.getElementById('sim-progress-text') as HTMLElement | null; - - // View Switcher & Toolbar - const viewTabButtons = document.querySelectorAll('.view-tab-btn'); - const viewPanes = document.querySelectorAll('.view-pane'); - const itemCatTabs = document.querySelectorAll('.item-cat-tab'); - const itemSearchInput = document.getElementById('item-search-input') as HTMLInputElement | null; - const itemSortSelect = document.getElementById('item-sort-select') as HTMLSelectElement | null; - const btnExportCsv = document.getElementById('btn-export-csv') as HTMLButtonElement | null; - - // Stats Bar Elements - const statTargetName = document.getElementById('stat-target-name'); - const statCurrentTc = document.getElementById('stat-current-tc'); - const statEffPlayers = document.getElementById('stat-eff-players'); - const statTotalItems = document.getElementById('stat-total-items'); - const statNoDropPct = document.getElementById('stat-nodrop-pct'); - const statAvgDrops = document.getElementById('stat-avg-drops'); - - // Results Containers - const exactContainer = document.getElementById('exact-items-container'); - const simContainer = document.getElementById('sim-items-container'); - const exactTbody = document.getElementById('exact-tbody'); - const itemsTbody = document.getElementById('items-tbody'); - const qualityTbody = document.getElementById('quality-tbody'); - const exactTcInfo = document.getElementById('exact-tc-info'); - - // Reverse Search Elements - const reverseInput = document.getElementById('reverse-search-input') as HTMLInputElement | null; - const reverseTbody = document.getElementById('reverse-tbody'); - const reverseMatchCount = document.getElementById('reverse-match-count'); - - function getEffectiveTcName(): string { - return getMonsterPresetTc(currentMonster, currentDifficulty, isQuestDrop, customTcOverride); - } - - function getEffectivePlayers(): number { - const g = parseInt(gamePlayersInput?.value || '1', 10) || 1; - const p = parseInt(partyPlayersInput?.value || '1', 10) || 1; - return computeEffectivePlayers(g, p); - } - - // --- Render Monster List ---------------------------------------------------- - - function renderMonsterList() { - if (!monsterListContainer) return; - const query = monsterSearchInput?.value.trim().toLowerCase() || ''; - - const filtered = MONSTER_PRESETS.filter(m => { - if (activeMonsterCategory !== 'all' && m.category !== activeMonsterCategory) { - return false; - } - if (!query) return true; - return ( - m.nameCn.toLowerCase().includes(query) || - m.nameEn.toLowerCase().includes(query) || - m.areaCn.toLowerCase().includes(query) || - m.areaEn.toLowerCase().includes(query) || - m.id.toLowerCase().includes(query) - ); - }); - - if (filtered.length === 0) { - monsterListContainer.innerHTML = `
无匹配的怪物
`; - return; - } - - const html = filtered - .map(m => { - const isSelected = m.id === currentMonster.id; - const currentMlvl = m.mlvl[currentDifficulty]; - const categoryBadge = m.category === 'boss' ? '幕首领' : m.category === 'superunique' ? '固定暗金' : m.category === 'chest' ? '宝箱' : m.category === 'farm' ? '刷宝' : '常规'; - - return ` -
-
${m.icon}
-
-
- ${m.nameCn} - ${categoryBadge} -
-
${m.areaCn} · mlvl ${currentMlvl} · ${m.picks} picks
-
-
- `; - }) - .join(''); - - monsterListContainer.innerHTML = html; - - monsterListContainer.querySelectorAll('.monster-item-card').forEach(el => { - el.addEventListener('click', () => { - const id = el.getAttribute('data-mon-id'); - const m = MONSTER_PRESETS.find(x => x.id === id); - if (m) { - customTcOverride = null; - currentMonster = m; - if (tcInput) tcInput.value = getEffectiveTcName(); - renderMonsterList(); - updateMonsterSpecCard(); - updateExactTable(); - } - }); - }); - } - - function updateMonsterSpecCard() { - const mlvl = currentMonster.mlvl[currentDifficulty]; - const tc = getEffectiveTcName(); - const hasQuestDrop = Boolean(currentMonster.questTc); - - if (monsterCardIcon) monsterCardIcon.textContent = currentMonster.icon; - if (monsterCardTitle) monsterCardTitle.textContent = currentMonster.nameCn; - if (monsterCardBadge) { - monsterCardBadge.textContent = - currentMonster.category === 'boss' - ? `第 ${currentMonster.act} 幕 · 幕关底首领` - : currentMonster.category === 'superunique' - ? `第 ${currentMonster.act} 幕 · 固定暗金怪` - : currentMonster.category === 'chest' - ? `第 ${currentMonster.act} 幕 · 超级宝箱` - : currentMonster.category === 'farm' - ? `高收益刷宝圣地` - : `普通怪物`; - } - if (monsterCardArea) monsterCardArea.textContent = currentMonster.areaCn; - if (monsterCardMlvl) monsterCardMlvl.textContent = String(mlvl); - if (monsterCardPicks) { - const node = tcTable.get(tc); - const picks = node ? Math.abs(node.picks) : currentMonster.picks; - monsterCardPicks.textContent = `${picks} 次`; - } - if (monsterCardNodrop) { - const g = parseInt(gamePlayersInput?.value || '1', 10) || 1; - const p = parseInt(partyPlayersInput?.value || '1', 10) || 1; - const eff = computeEffectivePlayers(g, p); - const node = tcTable.get(tc); - if (node && node.noDrop > 0) { - const totalProb = node.totalProbExpansion; - const scaledNoDrop = computeScaledNoDrop(node.noDrop, totalProb, eff); - const singleNoDrop = scaledNoDrop / (scaledNoDrop + totalProb); - const compoundNoDrop = node.picks > 0 ? Math.pow(singleNoDrop, node.picks) : 0; - monsterCardNodrop.textContent = `${(singleNoDrop * 100).toFixed(1)}%`; - monsterCardNodrop.title = `单次 NoDrop: ${(singleNoDrop * 100).toFixed(2)}%, 全空概率: ${(compoundNoDrop * 100).toFixed(2)}%`; - } else { - monsterCardNodrop.textContent = '0.00%'; - monsterCardNodrop.title = 'NoDrop 为 0 (必定掉落)'; - } - } - if (monsterCardTc) { - monsterCardTc.textContent = tc; - monsterCardTc.title = customTcOverride ? `自定义 TC: ${tc}` : `标准 TC: ${tc}`; - } - if (monsterCardDesc) monsterCardDesc.textContent = currentMonster.descCn; - - if (questDropToggle) { - questDropToggle.checked = isQuestDrop; - questDropToggle.disabled = !hasQuestDrop; - } - if (questDropCheckbox) { - questDropCheckbox.checked = isQuestDrop; - questDropCheckbox.disabled = !hasQuestDrop; - } - if (questDropBanner) { - questDropBanner.style.display = isQuestDrop && hasQuestDrop ? 'flex' : 'none'; - } - - if (effectivePlayersHint) { - const eff = getEffectivePlayers(); - effectivePlayersHint.textContent = `当前有效掉落人数: ${eff} 人 (NoDrop 缩减已生效)`; - } - - if (statTargetName) statTargetName.textContent = customTcOverride ? `${currentMonster.nameCn} (自定义TC)` : currentMonster.nameCn; - if (statCurrentTc) statCurrentTc.textContent = tc; - if (statEffPlayers) statEffPlayers.textContent = `${getEffectivePlayers()} P`; - } - - // --- Difficulty Switcher ---------------------------------------------------- - - function setDifficulty(diff: 'normal' | 'nightmare' | 'hell') { - currentDifficulty = diff; - customTcOverride = null; - diffButtons.forEach(btn => { - btn.classList.toggle('active', btn.getAttribute('data-diff') === diff); - }); - if (difficultySelect) { - difficultySelect.value = diff; - } - if (tcInput) tcInput.value = getEffectiveTcName(); - renderMonsterList(); - updateMonsterSpecCard(); - updateExactTable(); - } - - diffButtons.forEach(btn => { - btn.addEventListener('click', () => { - const diff = btn.getAttribute('data-diff') as any; - if (diff === 'normal' || diff === 'nightmare' || diff === 'hell') { - setDifficulty(diff); - } - }); - }); - - difficultySelect?.addEventListener('change', () => { - setDifficulty(difficultySelect.value as any); - }); - - // --- Monster Filters -------------------------------------------------------- - - monsterCatChips.forEach(chip => { - chip.addEventListener('click', () => { - monsterCatChips.forEach(c => c.classList.remove('active')); - chip.classList.add('active'); - activeMonsterCategory = chip.getAttribute('data-cat') as any; - renderMonsterList(); - }); - }); - - monsterSearchInput?.addEventListener('input', () => { - renderMonsterList(); - }); - - // Quest Drop toggle - function setQuestDrop(val: boolean) { - isQuestDrop = val; - customTcOverride = null; - if (questDropToggle) questDropToggle.checked = val; - if (questDropCheckbox) questDropCheckbox.checked = val; - if (tcInput) tcInput.value = getEffectiveTcName(); - updateMonsterSpecCard(); - updateExactTable(); - } - - questDropToggle?.addEventListener('change', () => { - setQuestDrop(questDropToggle.checked); - }); - questDropCheckbox?.addEventListener('change', () => { - setQuestDrop(questDropCheckbox.checked); - }); - - // Custom TC input - btnApplyCustomTc?.addEventListener('click', () => { - const val = tcInput?.value.trim(); - if (val) { - customTcOverride = val; - } else { - customTcOverride = null; - if (tcInput) tcInput.value = getEffectiveTcName(); - } - updateMonsterSpecCard(); - updateExactTable(); - }); - - presetSelect?.addEventListener('change', () => { - const val = presetSelect.value; - if (val) { - customTcOverride = val; - if (tcInput) tcInput.value = val; - updateMonsterSpecCard(); - updateExactTable(); - } - }); - - tcInput?.addEventListener('keydown', (e: KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault(); - btnApplyCustomTc?.click(); - } - }); - - // Player counts & MF - gamePlayersInput?.addEventListener('change', () => { - if (gamePlayersInput && partyPlayersInput) { - const g = parseInt(gamePlayersInput.value, 10) || 1; - const p = parseInt(partyPlayersInput.value, 10) || 1; - if (p > g) partyPlayersInput.value = String(g); - } - updateMonsterSpecCard(); - updateExactTable(); - }); - - partyPlayersInput?.addEventListener('change', () => { - if (gamePlayersInput && partyPlayersInput) { - const g = parseInt(gamePlayersInput.value, 10) || 1; - const p = parseInt(partyPlayersInput.value, 10) || 1; - if (p > g) partyPlayersInput.value = String(g); - } - updateMonsterSpecCard(); - updateExactTable(); - }); - - playerChips.forEach(chip => { - chip.addEventListener('click', () => { - playerChips.forEach(c => c.classList.remove('active')); - chip.classList.add('active'); - const p = chip.getAttribute('data-players'); - if (p && gamePlayersInput) { - gamePlayersInput.value = p; - if (partyPlayersInput) partyPlayersInput.value = '1'; - updateMonsterSpecCard(); - updateExactTable(); - } - }); - }); - - mfChips.forEach(chip => { - chip.addEventListener('click', () => { - mfChips.forEach(c => c.classList.remove('active')); - chip.classList.add('active'); - const mf = chip.getAttribute('data-mf'); - if (mf && mfInput) { - mfInput.value = mf; - updateExactTable(); - } - }); - }); - - // --- Category Tabs & View Switching ---------------------------------------- - - viewTabButtons.forEach(btn => { - btn.addEventListener('click', () => { - viewTabButtons.forEach(b => b.classList.remove('active')); - viewPanes.forEach(p => p.classList.remove('active')); - btn.classList.add('active'); - const view = btn.getAttribute('data-view') as 'exact' | 'sim' | 'reverse'; - activeViewTab = view; - document.getElementById(`view-${view}`)?.classList.add('active'); - renderDropList(); - }); - }); - - itemCatTabs.forEach(tab => { - tab.addEventListener('click', () => { - itemCatTabs.forEach(t => t.classList.remove('active')); - tab.classList.add('active'); - activeItemCategory = tab.getAttribute('data-cat') as ItemCategoryFilter; - renderDropList(); - }); - }); - - itemSearchInput?.addEventListener('input', () => { - itemSearchQuery = itemSearchInput.value.trim().toLowerCase(); - renderDropList(); - }); - - itemSortSelect?.addEventListener('change', () => { - currentSortOrder = itemSortSelect.value as any; - renderDropList(); - }); - - // --- CSV Export ------------------------------------------------------------ - - btnExportCsv?.addEventListener('click', () => { - exportCurrentDataCsv(); - }); - - function exportCurrentDataCsv() { - const tcName = getEffectiveTcName(); - let csv = '\uFEFF'; // UTF-8 BOM - - if (activeViewTab === 'sim' && currentSimResult) { - csv += '物品代码,物品名称,掉落品质,实测掉落数,每轮占比(%)\n'; - for (const s of latestSummaries) { - csv += `"${s.code}","${s.name.replace(/"/g, '""')}","${s.quality}",${s.count},${s.percentage.toFixed(4)}\n`; - } - } else if (currentAnalyticResult) { - csv += '排名,物品代码,物品名称,分类,单次概率(%),综合概率(%),掉落几率比(1 in X),每轮期望掉落数\n'; - currentAnalyticResult.leaves.forEach((l, idx) => { - const cat = classifyItemCategory(l.item); - const singlePct = (l.singleProbability * 100).toFixed(6); - const compPct = (l.compoundProbability * 100).toFixed(6); - const odds = l.oneInOdds < 1000000 ? l.oneInOdds.toFixed(1) : Math.round(l.oneInOdds); - csv += `${idx + 1},"${l.item}","${l.name.replace(/"/g, '""')}","${cat}",${singlePct},${compPct},"1 in ${odds}",${l.expectedDrops.toFixed(6)}\n`; - }); - } else { - alert('暂无数据可导出'); - return; - } - - const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `d2_drop_${tcName.replace(/[^a-zA-Z0-9_-]/g, '_')}_${Date.now()}.csv`; - a.click(); - URL.revokeObjectURL(url); - } - - // --- Calculate & Update Exact Odds ----------------------------------------- - - function updateExactTable() { - const tcName = getEffectiveTcName(); - const g = parseInt(gamePlayersInput?.value || '1', 10) || 1; - const p = parseInt(partyPlayersInput?.value || '1', 10) || 1; - - const result = calculateAnalyticProbabilities(tcName, { - tcTable, - autoTcs, - gamePlayers: g, - partyPlayers: p, - }); - - currentAnalyticResult = result; - - if (result.leaves.length === 0 && !tcTable.get(tcName) && !autoTcs.has(tcName)) { - if (exactTcInfo) { - exactTcInfo.innerHTML = `⚠️ 未找到名为 "${tcName}" 的 TreasureClass。请检查名称拼写或从左侧预设选择。`; - } - if (exactContainer) { - exactContainer.innerHTML = `
未找到名为 "${tcName}" 的 TreasureClass,请检查拼写
`; - } - return; - } - - if (statTotalItems) statTotalItems.textContent = String(result.leaves.length); - if (statNoDropPct) statNoDropPct.textContent = `${(result.noDropCompoundProb * 100).toFixed(1)}%`; - if (statAvgDrops) statAvgDrops.textContent = `${(result.picks * (1 - result.noDropSingleProb)).toFixed(2)}`; - - updateCategoryCounts(); - renderDropList(); - } - - function updateCategoryCounts() { - if (!currentAnalyticResult) return; - const counts: Record = { - all: 0, - unique: 0, - set: 0, - rune: 0, - base: 0, - charm_gem: 0, - misc: 0, - }; - - if (activeViewTab === 'sim' && currentSimResult) { - counts.all = latestSummaries.length; - for (const s of latestSummaries) { - const cat = classifyItemCategory(s.code, s.quality); - counts[cat]++; - } - } else { - counts.all = currentAnalyticResult.leaves.length; - for (const l of currentAnalyticResult.leaves) { - const cat = classifyItemCategory(l.item); - counts[cat]++; - // If has famous unique meta, increment unique count - if (FAMOUS_ITEMS_META[l.item]) { - counts.unique++; - } - } - } - - itemCatTabs.forEach(tab => { - const cat = tab.getAttribute('data-cat') as ItemCategoryFilter; - const badge = tab.querySelector('.cat-count'); - if (badge) { - badge.textContent = String(counts[cat] || 0); - } - }); - } - - // --- Render Maxroll-Style Drop List ---------------------------------------- - - function renderDropList() { - if (activeViewTab === 'exact') { - renderExactDropCards(); - } else if (activeViewTab === 'sim') { - renderSimDropCards(); - } - } - - function renderExactDropCards() { - if (!exactContainer || !currentAnalyticResult) return; - const leaves = currentAnalyticResult.leaves; - if (leaves.length === 0) { - exactContainer.innerHTML = `
暂无掉落数据
`; - return; - } - - // Maximum compound probability for relative bar width - const maxProb = Math.max(...leaves.map(l => l.compoundProbability), 0.0001); - - // Filter - let filtered = leaves.filter(l => { - const cat = classifyItemCategory(l.item); - const isFamousUnique = Boolean(FAMOUS_ITEMS_META[l.item]); - - if (activeItemCategory !== 'all') { - if (activeItemCategory === 'unique' && !isFamousUnique) return false; - if (activeItemCategory !== 'unique' && cat !== activeItemCategory) return false; - } - - if (!itemSearchQuery) return true; - const meta = FAMOUS_ITEMS_META[l.item]; - const matchText = `${l.item} ${l.name} ${meta?.famousTitle || ''} ${meta?.itemTypeCn || ''}`.toLowerCase(); - return matchText.includes(itemSearchQuery); - }); - - // Sort - filtered = [...filtered].sort((a, b) => { - if (currentSortOrder === 'odds-desc') return b.compoundProbability - a.compoundProbability; - if (currentSortOrder === 'odds-asc') return a.compoundProbability - b.compoundProbability; - if (currentSortOrder === 'name') return a.name.localeCompare(b.name); - if (currentSortOrder === 'qlvl') { - const qA = FAMOUS_ITEMS_META[a.item]?.qlvl || 0; - const qB = FAMOUS_ITEMS_META[b.item]?.qlvl || 0; - return qB - qA; - } - if (currentSortOrder === 'quality') { - const catA = classifyItemCategory(a.item); - const catB = classifyItemCategory(b.item); - return catA.localeCompare(catB); - } - return 0; - }); - - if (filtered.length === 0) { - exactContainer.innerHTML = `
没有符合条件的物品
`; - return; - } - - const cardsHtml = filtered.slice(0, 250).map((l, index) => { - const meta = FAMOUS_ITEMS_META[l.item]; - const cat = classifyItemCategory(l.item); - const pct = (l.compoundProbability * 100).toFixed(4); - const odds = l.oneInOdds < 1000000 ? l.oneInOdds.toFixed(1) : Math.round(l.oneInOdds).toLocaleString(); - const barWidth = Math.max(2, Math.min(100, (l.compoundProbability / maxProb) * 100)); - - let qualityTagClass = 'tag-base'; - let qualityTagName = '装备底模'; - let qualityColor = '#e8dcc8'; - - if (l.isRune) { - qualityTagClass = 'tag-rune'; - qualityTagName = '符文'; - qualityColor = '#ff8000'; - } else if (l.isGold) { - qualityTagClass = 'tag-gold'; - qualityTagName = '金币'; - qualityColor = '#ffd700'; - } else if (cat === 'charm_gem') { - qualityTagClass = 'tag-gem'; - qualityTagName = '护身符/珠宝'; - qualityColor = '#d880ff'; - } else if (cat === 'misc') { - qualityTagClass = 'tag-misc'; - qualityTagName = '杂项/材料'; - qualityColor = '#ffd700'; - } else if (meta?.famousTitle) { - qualityTagClass = 'tag-unique'; - qualityTagName = '暗金底模'; - qualityColor = '#c8a15a'; - } - - return ` -
-
- #${index + 1} - ${qualityTagName} -
-
- ${meta?.famousTitle || l.name} -
-
- ${l.item} - ${meta?.itemTypeCn || (l.isRune ? '符文' : '物品底模')} - ${meta?.qlvl ? `· qlvl ${meta.qlvl}` : ''} -
-
-
- -
-
-
-
-
- ${pct}% - 单次: ${(l.singleProbability * 100).toFixed(4)}% -
-
- -
-
1 : ${odds}
-
平均 ~${odds} 轮 / 件
-
-
- `; - }).join(''); - - exactContainer.innerHTML = cardsHtml; - } - - function renderSimDropCards() { - if (!simContainer) return; - if (!currentSimResult || latestSummaries.length === 0) { - simContainer.innerHTML = `
暂无模拟结果,请点击左侧「开始模拟」运行 Monte Carlo 抽样
`; - return; - } - - const totalRuns = currentSimResult.totalRuns; - const maxCount = Math.max(...latestSummaries.map(s => s.count), 1); - - // Filter - let filtered = latestSummaries.filter(s => { - const cat = classifyItemCategory(s.code, s.quality); - if (activeItemCategory !== 'all' && cat !== activeItemCategory) return false; - if (!itemSearchQuery) return true; - const meta = FAMOUS_ITEMS_META[s.code]; - const matchText = `${s.code} ${s.name} ${meta?.famousTitle || ''} ${s.quality}`.toLowerCase(); - return matchText.includes(itemSearchQuery); - }); - - if (filtered.length === 0) { - simContainer.innerHTML = `
没有符合条件的模拟物品
`; - return; - } - - const cardsHtml = filtered.slice(0, 250).map((s, index) => { - const meta = FAMOUS_ITEMS_META[s.code]; - const barWidth = Math.max(2, Math.min(100, (s.count / maxCount) * 100)); - const pct = s.percentage.toFixed(4); - const expectedOdds = s.count > 0 ? (totalRuns / s.count).toFixed(1) : '∞'; - - let qualityTagClass = 'tag-base'; - let qualityColor = '#e8dcc8'; - - if (s.quality === 'unique') { - qualityTagClass = 'tag-unique'; - qualityColor = '#c8a15a'; - } else if (s.quality === 'set') { - qualityTagClass = 'tag-set'; - qualityColor = '#00ff00'; - } else if (s.quality === 'rare') { - qualityTagClass = 'tag-rare'; - qualityColor = '#ffff60'; - } else if (s.quality === 'magic') { - qualityTagClass = 'tag-magic'; - qualityColor = '#4880ff'; - } else if (s.quality === 'rune') { - qualityTagClass = 'tag-rune'; - qualityColor = '#ff8000'; - } else if (s.quality === 'gold') { - qualityTagClass = 'tag-gold'; - qualityColor = '#ffd700'; - } - - return ` -
-
- #${index + 1} - ${s.quality.toUpperCase()} -
-
- ${meta?.famousTitle || s.name} -
-
- ${s.code} - ${meta?.itemTypeCn || '掉落物'} -
-
-
- -
-
-
-
-
- ${pct}% / 轮 - 实测掉落: ${s.count.toLocaleString()} 件 -
-
- -
-
实测 1 : ${expectedOdds}
-
${s.count.toLocaleString()} 件 / ${totalRuns.toLocaleString()} 轮
-
-
- `; - }).join(''); - - simContainer.innerHTML = cardsHtml; - } - - // --- Simulation Execution -------------------------------------------------- - - btnStart?.addEventListener('click', async () => { - if (isSimRunning) return; - - const tcName = getEffectiveTcName(); - if (!tcTable.get(tcName) && !autoTcs.has(tcName)) { - alert(`未找到名为 "${tcName}" 的 TreasureClass,请检查名称拼写!`); - return; - } - - const runs = parseInt(runsSelect?.value || '10000', 10) || 10000; - const g = parseInt(gamePlayersInput?.value || '1', 10) || 1; - const p = parseInt(partyPlayersInput?.value || '1', 10) || 1; - const mf = parseInt(mfInput?.value || '0', 10) || 0; - const diff = currentDifficulty; - const seedVal = seedInput?.value.trim() ? parseInt(seedInput.value, 10) : undefined; - - isSimRunning = true; - isSimPaused = false; - abortRequested = false; - - if (btnStart) btnStart.disabled = true; - if (btnPause) { - btnPause.disabled = false; - btnPause.textContent = '⏸ 暂停'; - } - if (btnStop) btnStop.disabled = false; - - // Switch to simulation view automatically - viewTabButtons.forEach(b => b.classList.toggle('active', b.getAttribute('data-view') === 'sim')); - viewPanes.forEach(pane => pane.classList.toggle('active', pane.id === 'view-sim')); - activeViewTab = 'sim'; - - try { - const result = await runMonteCarloSimulation( - { - tcName, - runs, - gamePlayers: g, - partyPlayers: p, - magicFind: mf, - difficulty: diff, - seed: seedVal, - tcTable, - autoTcs, - }, - async progress => { - if (abortRequested) throw new Error('Simulation aborted'); - while (isSimPaused && !abortRequested) { - if (progressText) { - progressText.textContent = `已暂停 (${progress.completedRuns.toLocaleString()} / ${progress.totalRuns.toLocaleString()}) - 点击「继续」恢复`; - } - await new Promise(r => setTimeout(r, 50)); - } - if (abortRequested) throw new Error('Simulation aborted'); - renderProgress(progress); - }, - ); - - currentSimResult = result; - latestSummaries = [...result.itemSummaries]; - updateCategoryCounts(); - renderFinalResults(result); - } catch (err: any) { - if (err.message === 'Simulation aborted') { - if (progressText) progressText.textContent = '模拟已中止'; - } else { - console.error(err); - } - } finally { - isSimRunning = false; - isSimPaused = false; - if (btnStart) btnStart.disabled = false; - if (btnPause) { - btnPause.disabled = true; - btnPause.textContent = '⏸ 暂停'; - } - if (btnStop) btnStop.disabled = true; - } - }); - - btnPause?.addEventListener('click', () => { - isSimPaused = !isSimPaused; - if (btnPause) { - btnPause.textContent = isSimPaused ? '▶ 继续' : '⏸ 暂停'; - } - if (progressText && isSimPaused) { - progressText.textContent = '已暂停 - 点击「继续」恢复'; - } - }); - - btnStop?.addEventListener('click', () => { - abortRequested = true; - }); - - btnCalcExact?.addEventListener('click', () => { - updateExactTable(); - }); - - function renderProgress(p: SimulationProgress) { - const pct = ((p.completedRuns / p.totalRuns) * 100).toFixed(1); - if (progressBar) progressBar.style.width = `${pct}%`; - if (progressText) { - progressText.textContent = `模拟进度: ${p.completedRuns.toLocaleString()} / ${p.totalRuns.toLocaleString()} (${pct}%) · 掉落 ${p.itemsDropped.toLocaleString()} 件`; - } - renderQualityTable(p.qualityCounts, p.completedRuns, p.itemsDropped); - } - - function renderFinalResults(r: SimulationResult) { - renderProgress({ - completedRuns: r.totalRuns, - totalRuns: r.totalRuns, - itemsDropped: r.itemsDropped, - qualityCounts: r.qualityCounts, - isFinished: true, - isPaused: false, - }); - renderSimDropCards(); - } - - function renderQualityTable(q: QualityCounts, runs: number, totalDrops: number) { - if (!qualityTbody) return; - const items = [ - { name: 'Unique (暗金)', count: q.unique, color: '#c8a15a' }, - { name: 'Set (套装)', count: q.set, color: '#00ff00' }, - { name: 'Rare (稀有)', count: q.rare, color: '#ffff60' }, - { name: 'Magic (魔法)', count: q.magic, color: '#4880ff' }, - { name: 'Rune (符文)', count: q.rune, color: '#ff8000' }, - { name: 'Normal / Sup (底模)', count: q.normal + q.superior, color: '#e8e8e8' }, - { name: 'Gold (金币)', count: q.gold, color: '#ffd700' }, - { name: 'NoDrop (空掉落轮数)', count: q.noDrop, color: '#666666' }, - ]; - - const rows = items.map(it => { - const denominator = it.name.startsWith('NoDrop') ? runs : totalDrops; - const pct = denominator > 0 ? ((it.count / denominator) * 100).toFixed(2) : '0.00'; - return ` - - ${it.name} - ${it.count.toLocaleString()} - ${pct}% - -
-
-
- - - `; - }); - - qualityTbody.innerHTML = rows.join(''); - } - - // --- Reverse Lookup -------------------------------------------------------- - - let reverseIndex: Map | undefined; - function getReverseIndex(): Map { - if (!reverseIndex) { - reverseIndex = buildReverseLookupIndex(tcTable, autoTcs); - } - return reverseIndex; - } - - reverseInput?.addEventListener('input', () => { - const query = reverseInput.value.trim(); - if (!query) { - if (reverseTbody) reverseTbody.innerHTML = ''; - if (reverseMatchCount) reverseMatchCount.textContent = '0 项匹配'; - return; - } - const results = reverseLookup(getReverseIndex(), query); - renderReverseLookupResults(results); - }); - - function renderReverseLookupResults(results: ReverseLookupResult[]) { - if (!reverseTbody) return; - let totalMatches = 0; - const allMatches: { res: ReverseLookupResult; entry: ReverseLookupEntry }[] = []; - for (const res of results) { - for (const entry of res.entries) { - allMatches.push({ res, entry }); - } - } - allMatches.sort((a, b) => b.entry.compoundProbability - a.entry.compoundProbability); - - const rows: string[] = []; - for (const m of allMatches) { - totalMatches++; - const res = m.res; - const entry = m.entry; - const pct = (entry.compoundProbability * 100).toFixed(4) + '%'; - const odds = entry.oneInOdds < 1000000 ? '1 in ' + entry.oneInOdds.toFixed(1) : '1 in ' + Math.round(entry.oneInOdds).toLocaleString(); - rows.push(` - - ${entry.tcName} - ${res.matchedCode} (${res.matchedName}) - ${entry.picks} - ${(entry.singleProbability * 100).toFixed(4)}% - ${pct} - ${odds} - - `); - } - - reverseTbody.innerHTML = rows.slice(0, 200).join(''); - if (reverseMatchCount) { - reverseMatchCount.textContent = `找到 ${totalMatches} 条掉落记录${totalMatches > 200 ? ' (展示前 200 条)' : ''}`; - } - } - - // Initial render - renderMonsterList(); - updateMonsterSpecCard(); - updateExactTable(); -} - diff --git a/src/skills.ts b/src/skills.ts deleted file mode 100644 index 420e149..0000000 --- a/src/skills.ts +++ /dev/null @@ -1,787 +0,0 @@ -/** - * Diablo II v1.13c Skill Point Calculator Web Application. - * - * Implements full 7-class skill tree planner matching the reference layout: - * - 3 side-by-side authentic 256x432 skill trees per class - * - Left-click to allocate, right-click to deallocate (with orphan protection) - * - Authentic 1.13c synergy engine & masteries calculation - * - Dynamic tooltip with damage, mana, requirements, synergies, next-level preview - * - URL Hash serialization for build sharing - * - Chinese / English bilingual localization - */ - -import { - type CharacterClassCode, - CHARACTER_CLASS_CODES, -} from './game/classes.ts' -import { - type SkillCatalogEntry, - SKILLS_BY_ID, - SKILLS_CATALOG, -} from './data/skills-catalog.ts' -import { - type SkillCalcState, - allocatePoint, - deallocatePoint, - respec, - canAllocate, - canDeallocate, - getEffectiveLevel, - getTotalPointsSpent, - getTabPointsSpent, - getRequiredCharacterLevel, - buildTooltipViewModel, - serializeBuildToHash, - deserializeBuildFromHash, -} from './game/skill-calc-engine.ts' - -type Lang = 'zh' | 'en' - -// Authentic Diablo II class tab orders (top-to-bottom in game UI translates to left-to-right) -// Amazon: Javelin & Spear (tab 2), Passive & Magic (tab 1), Bow & Crossbow (tab 0) -> exact match with reference image! -const CLASS_TAB_ORDER: Record = { - ama: [2, 1, 0], - sor: [0, 1, 2], - nec: [2, 1, 0], - pal: [0, 1, 2], - bar: [0, 1, 2], - dru: [2, 1, 0], - ass: [2, 1, 0], -} - -const CLASS_NAMES: Record = { - ama: { zh: '亚马逊', en: 'Amazon' }, - sor: { zh: '法师', en: 'Sorceress' }, - nec: { zh: '死灵法师', en: 'Necromancer' }, - pal: { zh: '圣骑士', en: 'Paladin' }, - bar: { zh: '野蛮人', en: 'Barbarian' }, - dru: { zh: '德鲁伊', en: 'Druid' }, - ass: { zh: '刺客', en: 'Assassin' }, -} - -// UI State -let currentClass: CharacterClassCode = 'ama' -let currentLang: Lang = 'zh' -let activeTooltipSkillId: number | null = null - -let state: SkillCalcState = { - classCode: 'ama', - hardPoints: {}, - tabSoftPoints: { 0: 0, 1: 0, 2: 0 }, - allSkillsSoftPoints: 0, - specificSoftPoints: {}, -} - -// DOM Elements -const classTabsContainer = document.getElementById('class-tabs-container') as HTMLElement -const treesContainer = document.getElementById('trees-container') as HTMLElement -const tooltipEl = document.getElementById('skill-tooltip') as HTMLElement -const toastEl = document.getElementById('toast') as HTMLElement - -// Summary Bar Elements -const valReqLevel = document.getElementById('val-req-level') as HTMLElement -const valPointsSpent = document.getElementById('val-points-spent') as HTMLElement -const valPointsRemain = document.getElementById('val-points-remain') as HTMLElement -const valAllSkills = document.getElementById('val-all-skills') as HTMLElement -const btnAllSkillsDec = document.getElementById('btn-all-skills-dec') as HTMLButtonElement -const btnAllSkillsInc = document.getElementById('btn-all-skills-inc') as HTMLButtonElement -const btnRespec = document.getElementById('btn-respec') as HTMLButtonElement -const btnCopyLink = document.getElementById('btn-copy-link') as HTMLButtonElement -const btnLang = document.getElementById('btn-lang') as HTMLButtonElement - -// Static Label Elements for I18n -const txtAppTitle = document.getElementById('txt-app-title') as HTMLElement -const lblReqLevel = document.getElementById('lbl-req-level') as HTMLElement -const lblPointsSpent = document.getElementById('lbl-points-spent') as HTMLElement -const lblPointsRemain = document.getElementById('lbl-points-remain') as HTMLElement -const lblAllSkills = document.getElementById('lbl-all-skills') as HTMLElement -const lnkLoot = document.getElementById('lnk-loot') as HTMLAnchorElement -const lnkBoss = document.getElementById('lnk-boss') as HTMLAnchorElement -const lnkActs = document.getElementById('lnk-acts') as HTMLAnchorElement - -/** - * Shows a brief floating toast notification. - */ -function showToast(msg: string) { - if (!toastEl) return - toastEl.textContent = msg - toastEl.classList.add('show') - setTimeout(() => { - toastEl.classList.remove('show') - }, 2200) -} - -/** - * Initializes state from URL Hash or defaults. - */ -function initFromHash() { - const hash = window.location.hash - if (hash) { - const restored = deserializeBuildFromHash(hash) - if (restored) { - currentClass = restored.classCode - state = restored - return - } - } - // Default fresh build - state = { - classCode: currentClass, - hardPoints: {}, - tabSoftPoints: { 0: 0, 1: 0, 2: 0 }, - allSkillsSoftPoints: 0, - specificSoftPoints: {}, - } -} - -/** - * Syncs the state to URL hash without reloading. - */ -function syncHash() { - const hash = serializeBuildToHash(state) - if (window.location.hash !== hash) { - history.replaceState(null, '', hash) - } -} - -/** - * Updates static labels according to the current language. - */ -function updateStaticI18n() { - if (currentLang === 'zh') { - txtAppTitle.textContent = '暗黑破坏神 2 · 技能加点模拟器' - lblReqLevel.textContent = '需求角色等级:' - lblPointsSpent.textContent = '已用技能点:' - lblPointsRemain.textContent = '剩余技能点:' - lblAllSkills.textContent = '+ 全部技能:' - btnRespec.textContent = '重置技能 (Respec)' - btnCopyLink.textContent = '复制配置 (Share)' - btnLang.textContent = '中 / EN' - lnkLoot.textContent = '战利品模拟' - lnkBoss.textContent = '首领挑战' - lnkActs.textContent = '大地图' - } else { - txtAppTitle.textContent = 'Diablo II · Skill Calculator (v1.13c)' - lblReqLevel.textContent = 'Required Level:' - lblPointsSpent.textContent = 'Points Spent:' - lblPointsRemain.textContent = 'Remaining Points:' - lblAllSkills.textContent = '+ to All Skills:' - btnRespec.textContent = 'Respec' - btnCopyLink.textContent = 'Copy Build Link' - btnLang.textContent = 'EN / 中' - lnkLoot.textContent = 'Loot Sim' - lnkBoss.textContent = 'Boss Arena' - lnkActs.textContent = 'Campaign Map' - } -} - -/** - * Renders the top 7 class switcher tabs. - */ -function renderClassTabs() { - classTabsContainer.innerHTML = '' - for (const code of CHARACTER_CLASS_CODES) { - const btn = document.createElement('button') - btn.className = `class-btn ${code === currentClass ? 'active' : ''}` - btn.textContent = currentLang === 'zh' ? CLASS_NAMES[code].zh : CLASS_NAMES[code].en - btn.dataset.classCode = code - btn.addEventListener('click', () => { - if (currentClass === code) return - currentClass = code - state = { - classCode: code, - hardPoints: {}, - tabSoftPoints: { 0: 0, 1: 0, 2: 0 }, - allSkillsSoftPoints: 0, - specificSoftPoints: {}, - } - syncHash() - render() - }) - classTabsContainer.appendChild(btn) - } -} - -/** - * Renders the 3 skill tree panels for the active character class. - */ -function renderTrees() { - treesContainer.innerHTML = '' - const tabIndices = CLASS_TAB_ORDER[currentClass] - - for (const tabIndex of tabIndices) { - // Collect 10 skills for this tab - const skillsInTab = SKILLS_CATALOG.filter( - s => s.classCode === currentClass && s.tabIndex === tabIndex - ) - if (skillsInTab.length === 0) continue - - const tabName = currentLang === 'zh' ? skillsInTab[0]!.tabNameZh : skillsInTab[0]!.tabName - const tabSpent = getTabPointsSpent(state, tabIndex) - - // Tree Column container - const colDiv = document.createElement('div') - colDiv.className = 'tree-column' - - // Header - const headerDiv = document.createElement('div') - headerDiv.className = 'tree-header' - headerDiv.innerHTML = ` -
${tabName}
-
- ${currentLang === 'zh' ? `该系已投入 ${tabSpent} 点` : `${tabSpent} POINTS IN THIS TREE`} -
- ` - colDiv.appendChild(headerDiv) - - // Stone Tree Canvas - const canvasDiv = document.createElement('div') - canvasDiv.className = 'tree-canvas' - canvasDiv.style.backgroundImage = `url('/skills/bg_${currentClass}_${tabIndex}.png')` - - // Render 10 skill sockets - for (const skill of skillsInTab) { - const socket = createSkillSocket(skill, tabIndex) - canvasDiv.appendChild(socket) - } - - colDiv.appendChild(canvasDiv) - - // Footer with Tree Soft Points stepper - const footerDiv = document.createElement('div') - footerDiv.className = 'tree-footer' - const currentTabSoft = state.tabSoftPoints[tabIndex] ?? 0 - footerDiv.innerHTML = ` - ${currentLang === 'zh' ? `+ 该系技能:` : `+ to ${tabName}:`} -
- - ${currentTabSoft} - -
- ` - - // Wire stepper buttons - const btnDec = footerDiv.querySelector('.tab-soft-dec') as HTMLButtonElement - const btnInc = footerDiv.querySelector('.tab-soft-inc') as HTMLButtonElement - btnDec.addEventListener('click', () => { - if ((state.tabSoftPoints[tabIndex] ?? 0) > 0) { - state.tabSoftPoints[tabIndex]!-- - syncHash() - render() - if (activeTooltipSkillId) refreshTooltip(activeTooltipSkillId) - } - }) - btnInc.addEventListener('click', () => { - if ((state.tabSoftPoints[tabIndex] ?? 0) < 99) { - state.tabSoftPoints[tabIndex] = (state.tabSoftPoints[tabIndex] ?? 0) + 1 - syncHash() - render() - if (activeTooltipSkillId) refreshTooltip(activeTooltipSkillId) - } - }) - - colDiv.appendChild(footerDiv) - treesContainer.appendChild(colDiv) - } -} - -/** - * Creates an individual skill socket DOM element on the 256x432 stone canvas. - */ -function createSkillSocket(skill: SkillCatalogEntry, tabIndex: number): HTMLElement { - const socket = document.createElement('div') - socket.className = 'skill-socket' - - // Diablo II Grid layout coordinates: - // x = 10 + (col - 1) * 72 - // y = 14 + (row - 1) * 64 - const posX = 10 + (skill.col - 1) * 72 - const posY = 14 + (skill.row - 1) * 64 - socket.style.left = `${posX}px` - socket.style.top = `${posY}px` - socket.dataset.skillId = String(skill.id) - - const hard = state.hardPoints[skill.id] ?? 0 - const eff = getEffectiveLevel(state, skill.id) - const canAlloc = canAllocate(state, skill.id) - const isMaxed = hard >= 20 - const isLocked = hard === 0 && !canAlloc - - if (isLocked) socket.classList.add('locked') - if (hard > 0) socket.classList.add('active') - if (isMaxed) socket.classList.add('maxed') - - // Icon Image - const img = document.createElement('img') - img.className = 'skill-icon' - img.src = hard > 0 ? skill.iconActive : skill.icon - img.alt = currentLang === 'zh' ? skill.nameZh : skill.name - img.draggable = false - socket.appendChild(img) - - // Point Badge Box (Positioned at right-bottom: posX + 46, posY + 26) - const badge = document.createElement('div') - badge.className = 'skill-pts-badge' - badge.style.left = '46px' - badge.style.top = '26px' - - if (hard > 0) { - badge.classList.add('has-points') - if (eff > hard) { - badge.classList.add('has-soft-points') - badge.textContent = String(eff) - badge.title = `${hard} base + ${eff - hard} gear` - } else { - badge.textContent = String(hard) - } - } else { - badge.textContent = '0' - } - if (isMaxed) badge.classList.add('is-maxed') - - socket.appendChild(badge) - - // Interaction: Left Click (+1 or +5) - socket.addEventListener('click', (e: MouseEvent) => { - e.preventDefault() - const count = e.shiftKey ? 5 : 1 - const changed = allocatePoint(state, skill.id, count) - if (changed) { - syncHash() - render() - refreshTooltip(skill.id) - } - }) - - // Interaction: Right Click (-1 or refund all) - socket.addEventListener('contextmenu', (e: MouseEvent) => { - e.preventDefault() - const count = e.shiftKey ? 20 : 1 - const allowed = canDeallocate(state, skill.id) - if (!allowed && (state.hardPoints[skill.id] ?? 0) > 0) { - showToast( - currentLang === 'zh' - ? '无法撤除技能点:有后续依赖技能已加点!' - : 'Cannot deallocate: Dependent skills require this prerequisite!' - ) - return - } - const changed = deallocatePoint(state, skill.id, count) - if (changed) { - syncHash() - render() - refreshTooltip(skill.id) - } - }) - - // Tooltip tracking - socket.addEventListener('mouseenter', (e: MouseEvent) => { - activeTooltipSkillId = skill.id - renderTooltip(skill.id) - positionTooltip(e.clientX, e.clientY) - }) - socket.addEventListener('mousemove', (e: MouseEvent) => { - positionTooltip(e.clientX, e.clientY) - }) - socket.addEventListener('mouseleave', () => { - activeTooltipSkillId = null - hideTooltip() - }) - - return socket -} - -/** - * Positions the tooltip safely within the viewport boundaries. - */ -function positionTooltip(mouseX: number, mouseY: number) { - if (!tooltipEl || tooltipEl.style.display !== 'block') return - - const tooltipWidth = tooltipEl.offsetWidth || 320 - const tooltipHeight = tooltipEl.offsetHeight || 300 - const pad = 16 - - let left = mouseX + pad - let top = mouseY + pad - - // Right edge boundary clamp - if (left + tooltipWidth > window.innerWidth - 10) { - left = mouseX - tooltipWidth - pad - } - // Bottom edge boundary clamp - if (top + tooltipHeight > window.innerHeight - 10) { - top = window.innerHeight - tooltipHeight - 10 - } - // Top boundary clamp (keep below header and stats bar at 94px) - if (top < 96) top = 96 - if (left < 10) left = 10 - - tooltipEl.style.left = `${left}px` - tooltipEl.style.top = `${top}px` -} - -/** - * Hides the tooltip. - */ -function hideTooltip() { - if (tooltipEl) tooltipEl.style.display = 'none' -} - -/** - * Re-renders the tooltip content if active. - */ -function refreshTooltip(skillId: number) { - if (activeTooltipSkillId === skillId) { - renderTooltip(skillId) - } -} - -/** - * Builds and displays the authentic Diablo II hover tooltip. - */ -function renderTooltip(skillId: number) { - const reqCharLvl = getRequiredCharacterLevel(state) - const vm = buildTooltipViewModel(state, skillId, reqCharLvl) - if (!vm) { - hideTooltip() - return - } - - const skillName = currentLang === 'zh' ? vm.skill.nameZh : vm.skill.name - const className = currentLang === 'zh' ? CLASS_NAMES[currentClass].zh : CLASS_NAMES[currentClass].en - const tabName = currentLang === 'zh' ? vm.skill.tabNameZh : vm.skill.tabName - const soft = vm.effectiveLevel > vm.hardPoints ? vm.effectiveLevel - vm.hardPoints : 0 - - let levelText = '' - if (vm.isLearned) { - if (soft > 0) { - levelText = currentLang === 'zh' - ? `当前技能等级: ${vm.effectiveLevel} (基础: ${vm.hardPoints}, 装备: +${soft})` - : `Current Skill Level: ${vm.effectiveLevel} (Base: ${vm.hardPoints}, Gear: +${soft})` - } else { - levelText = currentLang === 'zh' - ? `当前技能等级: ${vm.effectiveLevel}` - : `Current Skill Level: ${vm.effectiveLevel}` - } - } else { - levelText = currentLang === 'zh' ? `未习得 (预览 1 级效果)` : `Not Learned (Preview Level 1)` - } - - let html = ` -
-
${skillName}
-
${className} · ${tabName}
-
${levelText}
-
- -
-
- ${currentLang === 'zh' ? '需求角色等级:' : 'Required Character Level:'} - ${vm.reqlevel} -
- ` - - // Prerequisites - if (vm.prereqs.length > 0) { - html += ` -
- ${currentLang === 'zh' ? '前置技能需求:' : 'Prerequisites:'} -
- ` - for (const prereq of vm.prereqs) { - const pName = currentLang === 'zh' ? prereq.nameZh : prereq.name - const statusIcon = prereq.met ? '✓' : '✗' - const statusClass = prereq.met ? 'ok' : 'err' - const statusDesc = prereq.met - ? (currentLang === 'zh' ? '已达成' : 'Satisfied') - : (currentLang === 'zh' ? '未投入点数' : 'Missing') - html += ` -
- ${statusIcon} ${pName} - ${statusDesc} -
- ` - } - } - - // Mana Cost & Delay - if (vm.current.manaCost > 0 || vm.skill.mana > 0) { - html += ` -
- ${currentLang === 'zh' ? '法力消耗:' : 'Mana Cost:'} - ${vm.current.manaCost} -
- ` - } - if (vm.current.delaySeconds > 0) { - html += ` -
- ${currentLang === 'zh' ? '施法延迟:' : 'Casting Delay:'} - ${vm.current.delaySeconds} ${currentLang === 'zh' ? '秒' : 'sec'} -
- ` - } - - html += `
` // close tt-section - - // Damage & Mechanics Box - const dmg = vm.current.damage - if (dmg.hasDamage || dmg.weaponDamagePercent) { - html += `
` - - // Final Damage - if (dmg.hasDamage) { - const typeLabel = currentLang === 'zh' ? dmg.typeZh : `${dmg.type.toUpperCase()} Damage` - html += ` -
- ${dmg.finalMin} - ${dmg.finalMax} ${typeLabel} -
- ` - if (dmg.durationSeconds) { - html += ` -
- ${currentLang === 'zh' ? `持续时间: ${dmg.durationSeconds} 秒` : `Duration: ${dmg.durationSeconds} seconds`} -
- ` - } - } - - // Weapon Damage % - if (dmg.weaponDamagePercent) { - html += ` -
- ${currentLang === 'zh' ? `武器伤害: ${dmg.weaponDamagePercent}%` : `Weapon Damage: ${dmg.weaponDamagePercent}%`} -
- ` - } - - // Base damage breakdown if multiplier applied - if (dmg.hasDamage && (dmg.synergyMultiplier > 1.0 || dmg.masteryMultiplier > 1.0)) { - html += ` -
- ${currentLang === 'zh' ? `基础伤害: ${dmg.baseMin} - ${dmg.baseMax}` : `Base: ${dmg.baseMin} - ${dmg.baseMax}`} - ${dmg.synergyBonusPct > 0 ? ` · 协同 +${dmg.synergyBonusPct}%` : ''} - ${dmg.masteryBonusPct > 0 ? ` · 精通 +${dmg.masteryBonusPct}%` : ''} -
- ` - } - - html += `
` - } - - // Skill Effects & Mechanics Box (Stats, Auras, Minions, Buffs, Curses) - if (vm.current.effects && vm.current.effects.length > 0) { - html += `
` - for (const eff of vm.current.effects) { - const lineText = currentLang === 'zh' ? eff.textZh : eff.textEn - html += `
${lineText}
` - } - html += `
` - } - - // Synergies Received List - if (vm.synergiesReceived.length > 0) { - html += ` -
-
- ${currentLang === 'zh' ? '接收协同加成 (Synergies Received):' : 'Synergies Received:'} -
- ` - for (const syn of vm.synergiesReceived) { - const pName = currentLang === 'zh' ? syn.providerNameZh : syn.providerName - const activeClass = syn.hardPoints > 0 ? 'active' : '' - html += ` -
- • ${pName}: +${syn.percentPerLevel}% / ${currentLang === 'zh' ? '级' : 'lvl'} - (${currentLang === 'zh' ? `当前 ${syn.hardPoints} 点: +${syn.currentBonusPct}%` : `Current ${syn.hardPoints} pts: +${syn.currentBonusPct}%`}) -
- ` - } - html += `
` - } - - // Mastery info if applicable - if (vm.masteryInfo && vm.masteryInfo.active) { - html += ` -
-
- ${currentLang === 'zh' ? '精通加成 (Mastery Applied):' : 'Mastery Applied:'} -
-
- • ${currentLang === 'zh' ? vm.masteryInfo.descriptionZh : vm.masteryInfo.descriptionEn} -
-
- ` - } - - // Synergies Given List (为此技能提供协同) - if (vm.synergiesGiven.length > 0) { - html += ` -
-
- ${currentLang === 'zh' ? '输出协同加成 (Synergies Given):' : 'Synergies Given:'} -
- ` - for (const given of vm.synergiesGiven) { - const tName = currentLang === 'zh' ? given.targetNameZh : given.targetName - html += ` -
- • ${tName}: +${given.percentPerLevel}% / ${currentLang === 'zh' ? '级' : 'lvl'} -
- ` - } - html += `
` - } - - // Next Level Preview - if (vm.nextLevel) { - const nextDmg = vm.nextLevel.damage - const hasNextDmg = nextDmg.hasDamage - const hasNextEffects = vm.nextLevel.effects && vm.nextLevel.effects.length > 0 - const hasManaChange = vm.nextLevel.manaCost !== vm.current.manaCost - - if (hasNextDmg || hasNextEffects || hasManaChange) { - html += ` -
-
${currentLang === 'zh' ? '[下一级效果]' : '[Next Level]'} (Lv ${vm.nextLevel.level})
- ` - if (hasNextDmg) { - const typeLabel = currentLang === 'zh' ? nextDmg.typeZh : `${nextDmg.type.toUpperCase()} Damage` - html += `
${nextDmg.finalMin} - ${nextDmg.finalMax} ${typeLabel}
` - } - if (hasNextEffects) { - for (const eff of vm.nextLevel.effects) { - const lineText = currentLang === 'zh' ? eff.textZh : eff.textEn - html += `
${lineText}
` - } - } - if (hasManaChange) { - html += `
${currentLang === 'zh' ? '法力消耗:' : 'Mana Cost:'} ${vm.nextLevel.manaCost}
` - } - html += `
` - } - } - - // Description / Lore - const desc = currentLang === 'zh' ? (vm.shortDescZh || vm.shortDesc) : vm.shortDesc - if (desc) { - html += `
${desc}
` - } - - // Helper Hint - html += ` -
- ${currentLang === 'zh' - ? '左键: +1点 (Shift+点击: +5) | 右键: -1点 (Shift+右键: 快速撤点)' - : 'Left-click: +1 (Shift: +5) | Right-click: -1 (Shift: Refund max)'} -
- ` - - tooltipEl.innerHTML = html - tooltipEl.style.display = 'block' -} - -/** - * Updates the summary statistics bar. - */ -function updateSummaryBar() { - const reqLevel = getRequiredCharacterLevel(state) - const spent = getTotalPointsSpent(state) - const remain = Math.max(0, 110 - spent) - - valReqLevel.textContent = String(reqLevel) - if (reqLevel > 99) { - valReqLevel.classList.add('warning') - } else { - valReqLevel.classList.remove('warning') - } - - valPointsSpent.textContent = `${spent} / 110` - valPointsRemain.textContent = String(remain) - valAllSkills.textContent = String(state.allSkillsSoftPoints || 0) -} - -/** - * Full page render pass. - */ -function render() { - updateStaticI18n() - renderClassTabs() - renderTrees() - updateSummaryBar() -} - -/** - * Setup event handlers. - */ -function setupEvents() { - // All Skills Stepper - btnAllSkillsDec.addEventListener('click', () => { - if (state.allSkillsSoftPoints > 0) { - state.allSkillsSoftPoints-- - syncHash() - render() - if (activeTooltipSkillId) refreshTooltip(activeTooltipSkillId) - } - }) - btnAllSkillsInc.addEventListener('click', () => { - if (state.allSkillsSoftPoints < 99) { - state.allSkillsSoftPoints++ - syncHash() - render() - if (activeTooltipSkillId) refreshTooltip(activeTooltipSkillId) - } - }) - - // Respec - btnRespec.addEventListener('click', () => { - const refunded = respec(state) - state.allSkillsSoftPoints = 0 - state.tabSoftPoints = { 0: 0, 1: 0, 2: 0 } - syncHash() - render() - hideTooltip() - showToast( - currentLang === 'zh' - ? `重置完成!已退还 ${refunded} 点技能点。` - : `Respec complete! Refunded ${refunded} skill points.` - ) - }) - - // Copy Build Link - btnCopyLink.addEventListener('click', async () => { - syncHash() - const url = window.location.href - try { - await navigator.clipboard.writeText(url) - showToast( - currentLang === 'zh' - ? '已复制配点链接到剪贴板!' - : 'Build link copied to clipboard!' - ) - } catch { - prompt( - currentLang === 'zh' ? '请复制以下配点链接:' : 'Copy build link below:', - url - ) - } - }) - - // Language Toggle - btnLang.addEventListener('click', () => { - currentLang = currentLang === 'zh' ? 'en' : 'zh' - render() - if (activeTooltipSkillId) refreshTooltip(activeTooltipSkillId) - }) - - // Window Hash Change (Back/Forward navigation) - window.addEventListener('hashchange', () => { - initFromHash() - render() - }) -} - -// Initialization -initFromHash() -setupEvents() -render() diff --git a/tests/boss.test.ts b/tests/boss.test.ts deleted file mode 100644 index b4f9e6a..0000000 --- a/tests/boss.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { describe, it, expect, beforeAll } from 'vitest' -import { ACT_BOSSES, type BossPreset } from '../src/boss.ts' -import { getEmbeddedDropTables } from '../src/game/embedded-drop-tables.ts' -import { executeDropPipeline, createDroppedItem, type DropTables } from '../src/game/drop-pipeline.ts' -import { formatItemTooltip } from '../src/game/item-tooltip.ts' -import type { Item } from '../src/game/items.ts' - -describe('Act Boss Loot Simulator (boss.html)', () => { - let dropTables: DropTables - - beforeAll(() => { - dropTables = getEmbeddedDropTables() - }) - - it('1. ACT_BOSSES catalog includes all 5 canonical Act Bosses', () => { - expect(ACT_BOSSES).toHaveLength(5) - const bossIds = ACT_BOSSES.map(b => b.id) - expect(bossIds).toEqual(['andariel', 'duriel', 'mephisto', 'diablo', 'baal']) - }) - - it('2. All standard TCs and Quest TCs resolve to valid nodes in TreasureClassEx', () => { - const difficulties: Array<'normal' | 'nightmare' | 'hell'> = ['normal', 'nightmare', 'hell'] - - for (const boss of ACT_BOSSES) { - for (const diff of difficulties) { - const stdTc = boss.tc[diff] - const questTc = boss.questTc[diff] - - const stdNode = dropTables.tcTable.get(stdTc) - const questNode = dropTables.tcTable.get(questTc) - - expect(stdNode, `Boss ${boss.id} [${diff}] std TC ${stdTc} should exist`).toBeDefined() - expect(questNode, `Boss ${boss.id} [${diff}] quest TC ${questTc} should exist`).toBeDefined() - - expect(stdNode?.picks).toBeGreaterThan(0) - expect(questNode?.picks).toBeGreaterThan(0) - } - } - }) - - it('3. Simulates single Hell Mephisto kill and generates realistic dropped items with attributes', () => { - const drops: Item[] = executeDropPipeline({ - tcName: 'Mephistoq (H)', - nLevel: 87, - dropTables, - difficulty: 'hell', - playerMf: 300, - gamePlayers: 1, - partyPlayers: 1, - monsterRng: 42, - }) - - expect(drops.length).toBeGreaterThan(0) - - for (const item of drops) { - expect(item.name.length).toBeGreaterThan(0) - expect(item.code).toBeDefined() - expect(item.code!.length).toBeGreaterThan(0) - expect(item.base).toBeDefined() - - const tooltip = formatItemTooltip(item) - expect(tooltip.title.length).toBeGreaterThan(0) - expect(tooltip.quality.length).toBeGreaterThan(0) - expect(tooltip.ilvl).toBe(87) - - // If equipment, verify stats formatting - if (item.base.kind === 'weapon' || item.base.kind === 'armor') { - expect(tooltip.baseType).toMatch(/Weapon|Armor/) - } - } - }) - - it('4. Simulates Hell Baal kill with 7 Players (low NoDrop) and rolls multi-tier affixes', () => { - const drops: Item[] = executeDropPipeline({ - tcName: 'Baal (H)', - nLevel: 99, - dropTables, - difficulty: 'hell', - playerMf: 250, - gamePlayers: 7, - partyPlayers: 1, - monsterRng: 99999, - }) - - expect(drops.length).toBeGreaterThan(0) - const qualities = drops.map(d => d.rarity) - expect(qualities.length).toBe(drops.length) - }) - - it('5. Verifies formatItemTooltip generates correct properties for Unique, Rare, and Magic items', () => { - // 5.1 Unique Item Tooltip Test - const dummyUniqueBase = dropTables.getBase('uap')! // Shako - const dummyUniqueDef = dropTables.uniques.getEnabledByCode('uap')[0]! - const uniqueItem: Item = { - base: dummyUniqueBase, - prefix: null, - suffix: null, - level: 87, - ilvl: 87, - name: dummyUniqueDef.index, - stats: { defense: 141, allskills: 2, mag: 50 }, - invWidth: dummyUniqueBase.invWidth, - invHeight: dummyUniqueBase.invHeight, - stack: 1, - value: 10000, - rarity: 'unique', - uniqueItemDef: dummyUniqueDef, - } - - const uniqueTooltip = formatItemTooltip(uniqueItem) - expect(uniqueTooltip.title).toBe(dummyUniqueDef.index) - expect(uniqueTooltip.quality.toLowerCase()).toBe('unique') - expect(uniqueTooltip.lines.length).toBeGreaterThan(0) - - // 5.2 Rune Tooltip Test - const dummyRuneBase = dropTables.getBase('r30')! // Ber Rune - const runeItem: Item = { - base: dummyRuneBase, - prefix: null, - suffix: null, - level: 63, - ilvl: 63, - name: 'Ber Rune', - code: 'r30 ', - stats: {}, - invWidth: 1, - invHeight: 1, - stack: 1, - value: 100, - rarity: 'normal', - } - - const runeTooltip = formatItemTooltip(runeItem) - expect(runeTooltip.title).toBe('Ber Rune') - expect(runeTooltip.quality).toBe('Rune') - expect(runeTooltip.lines.some(l => l.text.includes('Crushing Blow'))).toBe(true) - }) - - it('6. Verifies concrete rolled values for variable modifiers (e.g. +X to Mana, not range)', () => { - const ringBase = dropTables.getBase('rin')! - // Roll an item with mana stat - const rolledItem = createDroppedItem(ringBase, 'magic', { - magicAffixes: { - name: "Bahamut's Ring", - prefix: { - name: "Bahamut's", - affix: dropTables.magicAffixes.prefixes.getByName("Bahamut's")[0]!, - level: 37, - mods: [{ code: 'mana', min: 80, max: 120, value: 105 }], - }, - } as any, - ilvl: 50, - dwInitSeed: 12345, - }) - - const tooltip = formatItemTooltip(rolledItem) - expect(tooltip.lines.some(l => l.text === '+105 to Mana')).toBe(true) - expect(tooltip.lines.some(l => l.text.includes('80-120'))).toBe(false) - }) -}) diff --git a/tests/loot.test.ts b/tests/loot.test.ts deleted file mode 100644 index 1024947..0000000 --- a/tests/loot.test.ts +++ /dev/null @@ -1,412 +0,0 @@ -import { describe, it, expect, beforeAll } from 'vitest' -import { - calculateDirectProbabilities, - calculateCompoundProbability, - calculateAnalyticProbabilities, - buildReverseLookupIndex, - reverseLookup, - runMonteCarloSimulation, - loadCanonicalTreasureClasses, - CANONICAL_ITEM_NAMES, - MONSTER_PRESETS, - getMonsterPresetTc, - type ReverseLookupEntry, -} from '../src/loot.ts' - -describe('Loot Drop Simulator & Exact Probability Table (Issue #110)', () => { - const { tcTable, autoTcs } = loadCanonicalTreasureClasses() - - describe('1. Analytic Expansion Canonical Probabilities (Act 1 H2H A)', () => { - it('matches canonical probabilities for Act 1 H2H A direct drop rates', () => { - // Act 1 H2H A base parameters: - // picks: 1, NoDrop: 100, TotalProb: 60, TotalWeight: 160 - // Expected direct probabilities: - // NoDrop: 100 / 160 = 0.625 (62.5%) - // gld: 21 / 160 = 0.13125 (13.125%) - // Act 1 Equip A: 16 / 160 = 0.10 (10%) - // Act 1 Junk: 21 / 160 = 0.13125 (13.125%) - // Act 1 Good: 2 / 160 = 0.0125 (1.25%) - const direct = calculateDirectProbabilities('Act 1 H2H A', { - tcTable, - autoTcs, - gamePlayers: 1, - partyPlayers: 1, - }) - - expect(direct.picks).toBe(1) - expect(direct.noDrop).toBe(100) - expect(direct.totalWeight).toBe(160) - - const pNoDrop = direct.probabilities.get('NoDrop')! - const pGld = direct.probabilities.get('gld')! - const pEquip = direct.probabilities.get('Act 1 Equip A')! - const pJunk = direct.probabilities.get('Act 1 Junk')! - const pGood = direct.probabilities.get('Act 1 Good')! - - expect(pNoDrop).toBeCloseTo(0.625, 6) // 62.5% - expect(pGld).toBeCloseTo(0.13125, 6) // 13.125% - expect(pEquip).toBeCloseTo(0.10, 6) // 10.0% - expect(pJunk).toBeCloseTo(0.13125, 6) // 13.125% - expect(pGood).toBeCloseTo(0.0125, 6) // 1.25% - - // Sum of all direct branch probabilities must equal exactly 1.0 (100%) - const sum = pNoDrop + pGld + pEquip + pJunk + pGood - expect(sum).toBeCloseTo(1.0, 6) - }) - - it('propagates direct probabilities to analytic result object', () => { - const analytic = calculateAnalyticProbabilities('Act 1 H2H A', { - tcTable, - autoTcs, - gamePlayers: 1, - partyPlayers: 1, - }) - - expect(analytic.direct.get('NoDrop')).toBeCloseTo(0.625, 6) - expect(analytic.direct.get('gld')).toBeCloseTo(0.13125, 6) - expect(analytic.direct.get('Act 1 Equip A')).toBeCloseTo(0.10, 6) - expect(analytic.direct.get('Act 1 Junk')).toBeCloseTo(0.13125, 6) - expect(analytic.direct.get('Act 1 Good')).toBeCloseTo(0.0125, 6) - - expect(analytic.noDropSingleProb).toBeCloseTo(0.625, 6) - expect(analytic.noDropCompoundProb).toBeCloseTo(0.625, 6) - - // Leaf items include gld with single probability 0.13125 - const gldLeaf = analytic.leafMap.get('gld') - expect(gldLeaf).toBeDefined() - expect(gldLeaf?.singleProbability).toBeCloseTo(0.13125, 6) - expect(gldLeaf?.compoundProbability).toBeCloseTo(0.13125, 6) - }) - }) - - describe('2. Compound Probability Calculation for picks > 1', () => { - it('calculates compound probability 1 - (1 - p)^picks accurately', () => { - // 7 picks with p = 0.1: 1 - 0.9^7 = 0.5217031 - expect(calculateCompoundProbability(0.1, 7)).toBeCloseTo(1 - Math.pow(0.9, 7), 8) - - // 7 picks with p = 0.5: 1 - 0.5^7 = 0.9921875 - expect(calculateCompoundProbability(0.5, 7)).toBeCloseTo(1 - Math.pow(0.5, 7), 8) - - // 1 pick equals single probability - expect(calculateCompoundProbability(0.25, 1)).toBe(0.25) - - // Boundary values - expect(calculateCompoundProbability(0, 7)).toBe(0) - expect(calculateCompoundProbability(1, 7)).toBe(1) - expect(calculateCompoundProbability(0.5, -2)).toBe(1) // deterministic picks - }) - - it('never exceeds 1.0 across extensive parameter sweeps', () => { - const pickCounts = [1, 2, 3, 4, 5, 6, 7, 8, 10, 20, 50, 100, 1000] - const probabilities = [0.0001, 0.001, 0.01, 0.05, 0.1, 0.2, 0.33, 0.5, 0.75, 0.9, 0.99, 0.9999, 1.0, 1.5] - - for (const picks of pickCounts) { - for (const p of probabilities) { - const cp = calculateCompoundProbability(p, picks) - expect(cp).toBeGreaterThanOrEqual(0) - expect(cp).toBeLessThanOrEqual(1.0) - } - } - }) - - it('strictly exhibits monotonicity with respect to pick count for p in (0, 1)', () => { - const p = 0.15 - let prev = calculateCompoundProbability(p, 1) - for (let picks = 2; picks <= 20; picks++) { - const curr = calculateCompoundProbability(p, picks) - expect(curr).toBeGreaterThan(prev) - expect(curr).toBeLessThanOrEqual(1.0) - prev = curr - } - }) - }) - - describe('3. Monte Carlo Convergence within 99% Confidence Interval', () => { - it('converges to analytic drop probabilities within 99% CI for Act 1 H2H A', async () => { - const runs = 30000 - const seed = 424242 - - const result = await runMonteCarloSimulation({ - tcName: 'Act 1 H2H A', - runs, - gamePlayers: 1, - partyPlayers: 1, - seed, - tcTable, - autoTcs, - }) - - expect(result.totalRuns).toBe(runs) - - // True analytic probabilities - const pExpectedNoDrop = 0.625 // 100 / 160 - const pExpectedGld = 0.13125 // 21 / 160 - - // Observed sample proportions - const pObservedNoDrop = result.emptyRuns / runs - const gldSummary = result.itemSummaries.find(it => it.code === 'gld') - const pObservedGld = (gldSummary?.count ?? 0) / runs - - // 99% Confidence Interval Critical Value z = 2.575829 - const z99 = 2.575829 - const seNoDrop = Math.sqrt((pExpectedNoDrop * (1 - pExpectedNoDrop)) / runs) - const seGld = Math.sqrt((pExpectedGld * (1 - pExpectedGld)) / runs) - - const marginNoDrop = z99 * seNoDrop - const marginGld = z99 * seGld - - const diffNoDrop = Math.abs(pObservedNoDrop - pExpectedNoDrop) - const diffGld = Math.abs(pObservedGld - pExpectedGld) - - expect(diffNoDrop).toBeLessThanOrEqual(marginNoDrop) - expect(diffGld).toBeLessThanOrEqual(marginGld) - }) - - it('verifies player scaling reduces empty runs under /players 8', async () => { - const runs = 10000 - const seed = 999111 - - const resultSolo8 = await runMonteCarloSimulation({ - tcName: 'Act 1 H2H A', - runs, - gamePlayers: 8, - partyPlayers: 1, - seed, - tcTable, - autoTcs, - }) - - // Under gamePlayers = 8, partyPlayers = 1: - // effectivePlayers = 1 + trunc(7/2) = 4 - // Scaled NoDrop = trunc(60 * (100/160)^4 / (1 - (100/160)^4)) = 10 - // Total = 70. Expected empty rate = 10 / 70 ~ 14.28% - const observedRate8 = resultSolo8.emptyRuns / runs - const expectedRate8 = 10 / 70 - const se8 = Math.sqrt((expectedRate8 * (1 - expectedRate8)) / runs) - - expect(Math.abs(observedRate8 - expectedRate8)).toBeLessThanOrEqual(2.576 * se8) - expect(observedRate8).toBeLessThan(0.20) - }) - - it('simulates drops without seed (undefined seed) without PRNG zero-lock', async () => { - // Regression test for Issue #110 bug: When seed is omitted (default UI state), - // PRNG must not lock into (0, 0) state which produced 100% NoDrop across 1000 runs. - const runs = 1000 - const result = await runMonteCarloSimulation({ - tcName: 'Baal (H)', - runs, - gamePlayers: 1, - partyPlayers: 1, - tcTable, - autoTcs, - }) - - expect(result.totalRuns).toBe(runs) - expect(result.itemsDropped).toBeGreaterThan(2000) // Baal rolls up to 7 picks, usually ~5-6 items per run - expect(result.emptyRuns).toBeLessThan(10) // Baal has extremely low NoDrop - expect(result.itemSummaries.length).toBeGreaterThan(10) - expect(result.qualityCounts.magic + result.qualityCounts.rare + result.qualityCounts.set + result.qualityCounts.unique).toBeGreaterThan(0) - }) - - it('simulates drops with seed=0 safely mapping to non-zero seed', async () => { - const runs = 1000 - const result = await runMonteCarloSimulation({ - tcName: 'Baal (H)', - runs, - seed: 0, - gamePlayers: 1, - partyPlayers: 1, - tcTable, - autoTcs, - }) - - expect(result.totalRuns).toBe(runs) - expect(result.itemsDropped).toBeGreaterThan(2000) - expect(result.emptyRuns).toBeLessThan(10) - }) - - it('supports isQuestDrop option routing to Baalq (H)', async () => { - const runs = 500 - const result = await runMonteCarloSimulation({ - tcName: 'Baal (H)', - runs, - isQuestDrop: true, - gamePlayers: 1, - partyPlayers: 1, - tcTable, - autoTcs, - }) - - expect(result.totalRuns).toBe(runs) - expect(result.itemsDropped).toBeGreaterThan(1000) - // Quest drop has 0 NoDrop - expect(result.emptyRuns).toBe(0) - }) - - it('handles negative picks analytic probabilities without producing 100% compound for rare items', () => { - // Find a TC with negative picks in tcTable - let negativeTcName: string | undefined - for (const node of tcTable.all) { - if (node.picks < 0 && node.items.length > 1) { - negativeTcName = node.name - break - } - } - - if (negativeTcName) { - const analytic = calculateAnalyticProbabilities(negativeTcName, { - tcTable, - autoTcs, - gamePlayers: 1, - partyPlayers: 1, - }) - for (const leaf of analytic.leaves) { - if (leaf.singleProbability < 0.5) { - expect(leaf.compoundProbability).toBeLessThan(1.0) - } - } - } - }) - }) - - describe('4. Reverse Lookup Indexing All Eligible Items', () => { - let reverseIndex: Map - - beforeAll(() => { - reverseIndex = buildReverseLookupIndex(tcTable, autoTcs) - }) - - it('indexes all eligible unique leaf items', () => { - // 605 distinct item codes (604 items + unified gold) - expect(reverseIndex.size).toBe(605) - expect(reverseIndex.has('gld')).toBe(true) - expect(reverseIndex.has('uap')).toBe(true) - expect(reverseIndex.has('r33')).toBe(true) - expect(reverseIndex.has('cap')).toBe(true) - }) - - it('finds dropping TCs when searching for Shako / uap', () => { - const byCode = reverseLookup(reverseIndex, 'uap', CANONICAL_ITEM_NAMES) - expect(byCode.length).toBeGreaterThan(0) - const uapEntry = byCode.find(r => r.matchedCode === 'uap')! - expect(uapEntry).toBeDefined() - expect(uapEntry.entries.length).toBeGreaterThan(50) - - // Bosses like Mephisto (H) and Baal (H) must be in the drop list for Shako - const tcNames = uapEntry.entries.map(e => e.tcName) - expect(tcNames).toContain('Mephisto (H)') - expect(tcNames).toContain('Baal (H)') - - // Name search for "Shako" - const byName = reverseLookup(reverseIndex, 'Shako', CANONICAL_ITEM_NAMES) - expect(byName.some(r => r.matchedCode === 'uap')).toBe(true) - }) - - it('finds dropping TCs when searching for high rune Zod / r33', () => { - const byCode = reverseLookup(reverseIndex, 'r33', CANONICAL_ITEM_NAMES) - expect(byCode.length).toBeGreaterThan(0) - const r33Entry = byCode.find(r => r.matchedCode === 'r33')! - expect(r33Entry).toBeDefined() - - const tcNames = r33Entry.entries.map(e => e.tcName) - expect(tcNames).toContain('Runes 17') - expect(tcNames).toContain('Baal (H)') - expect(tcNames).toContain('Cow (H)') - - // Name search for "Zod" - const byName = reverseLookup(reverseIndex, 'zod', CANONICAL_ITEM_NAMES) - expect(byName.some(r => r.matchedCode === 'r33')).toBe(true) - }) - - it('finds dropping TCs when searching for cap (armo3)', () => { - const byCode = reverseLookup(reverseIndex, 'cap', CANONICAL_ITEM_NAMES) - expect(byCode.length).toBeGreaterThan(0) - const capEntry = byCode.find(r => r.matchedCode === 'cap')! - expect(capEntry).toBeDefined() - - const tcNames = capEntry.entries.map(e => e.tcName) - expect(tcNames).toContain('armo3') - expect(tcNames).toContain('Act 1 Equip A') - expect(tcNames).toContain('Act 1 H2H A') - }) - }) - - describe('5. Monster Preset TC Resolution & Accuracy (Issue #121)', () => { - it('verifies that all MONSTER_PRESETS define valid TCs that exist in tcTable', () => { - expect(MONSTER_PRESETS.length).toBeGreaterThanOrEqual(15) - - for (const m of MONSTER_PRESETS) { - for (const diff of ['normal', 'nightmare', 'hell'] as const) { - const tcName = m.tc[diff] - expect(tcName).toBeDefined() - expect(tcName.length).toBeGreaterThan(0) - const node = tcTable.get(tcName) ?? autoTcs.get(tcName) - expect(node, `Monster ${m.id} difficulty ${diff} TC "${tcName}" must exist in tcTable`).toBeDefined() - - if (m.questTc) { - const qtcName = m.questTc[diff] - expect(qtcName).toBeDefined() - const qnode = tcTable.get(qtcName) ?? autoTcs.get(qtcName) - expect(qnode, `Monster ${m.id} quest TC "${qtcName}" must exist in tcTable`).toBeDefined() - } - } - } - }) - - it('resolves Travincal Council (Council) to Council TCs, not Baal (H) (Issue #121 bug reproduction)', () => { - const council = MONSTER_PRESETS.find(m => m.id === 'council')! - expect(council).toBeDefined() - - // In Nightmare difficulty, Council must resolve to 'Council (N)', NOT 'Baal (H)' - const tcNightmare = getMonsterPresetTc(council, 'nightmare', false) - expect(tcNightmare).toBe('Council (N)') - expect(tcNightmare).not.toBe('Baal (H)') - - // In Hell difficulty, Council must resolve to 'Council (H)', NOT 'Baal (H)' - const tcHell = getMonsterPresetTc(council, 'hell', false) - expect(tcHell).toBe('Council (H)') - expect(tcHell).not.toBe('Baal (H)') - - // In Normal difficulty, Council must resolve to 'Council' - const tcNormal = getMonsterPresetTc(council, 'normal', false) - expect(tcNormal).toBe('Council') - }) - - it('resolves distinct TCs for every monster preset without leaking Baal (H)', () => { - for (const m of MONSTER_PRESETS) { - if (m.id === 'baal') continue - - for (const diff of ['normal', 'nightmare', 'hell'] as const) { - const tc = getMonsterPresetTc(m, diff, false) - expect(tc, `Monster ${m.id} [${diff}] should never resolve to Baal (H)`).not.toBe('Baal (H)') - expect(tc, `Monster ${m.id} [${diff}] should never resolve to Baal`).not.toBe('Baal') - expect(tc, `Monster ${m.id} [${diff}] should never resolve to Baal (N)`).not.toBe('Baal (N)') - } - } - }) - - it('resolves quest TCs correctly when quest drop is enabled', () => { - const andariel = MONSTER_PRESETS.find(m => m.id === 'andariel')! - expect(getMonsterPresetTc(andariel, 'hell', true)).toBe('Andarielq (H)') - expect(getMonsterPresetTc(andariel, 'nightmare', true)).toBe('Andarielq (N)') - expect(getMonsterPresetTc(andariel, 'normal', true)).toBe('Andarielq') - - const mephisto = MONSTER_PRESETS.find(m => m.id === 'mephisto')! - expect(getMonsterPresetTc(mephisto, 'hell', true)).toBe('Mephistoq (H)') - - const baal = MONSTER_PRESETS.find(m => m.id === 'baal')! - expect(getMonsterPresetTc(baal, 'hell', true)).toBe('Baalq (H)') - }) - - it('respects customOverride when explicitly provided, but monster selection clears it', () => { - const council = MONSTER_PRESETS.find(m => m.id === 'council')! - - // When custom override is active - expect(getMonsterPresetTc(council, 'nightmare', false, 'Runes 17')).toBe('Runes 17') - - // When custom override is empty or null, returns monster TC - expect(getMonsterPresetTc(council, 'nightmare', false, null)).toBe('Council (N)') - expect(getMonsterPresetTc(council, 'nightmare', false, '')).toBe('Council (N)') - }) - }) -}) diff --git a/tests/parity-1.13c.test.ts b/tests/parity-1.13c.test.ts index 91c3706..7a9ae0a 100644 --- a/tests/parity-1.13c.test.ts +++ b/tests/parity-1.13c.test.ts @@ -1,5 +1,4 @@ import { describe, it, expect, beforeAll } from 'vitest' -import { ACT_BOSSES, BOSS_LIST, type BossDefinition } from '../src/boss.ts' import { getEmbeddedDropTables } from '../src/game/embedded-drop-tables.ts' import * as fs from 'fs' import { MpqArchive } from '../src/mpq/archive.ts' @@ -37,7 +36,7 @@ import { isAffixEligible, type MagicAffix, type AffixTable } from '../src/game/a import type { RareNameTable } from '../src/game/rare-names.ts' import type { UpgradableItemBase } from '../src/game/item-upgrade.ts' import type { Item, ItemBase } from '../src/game/items.ts' -import { runParityAudit } from '../scripts/audit-parity.ts' +import { ACT_BOSSES, BOSS_LIST, runParityAudit, type BossDefinition } from '../scripts/audit-parity.ts' export type BaseTier = 'normal' | 'exceptional' | 'elite' diff --git a/vite.config.ts b/vite.config.ts index 09234d7..9a6060e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,8 +6,8 @@ const __dirname = fileURLToPath(new URL('.', import.meta.url)) /** * The engine is plain TypeScript with no runtime dependencies: Vite only serves - * and bundles it. Two entries: the real campaign scene (`acts.html`) and the - * combat math arena (`arena.html`). `samples/` is served in dev so all of them can + * and bundles it. Main campaign entries (`index.html`, `acts.html`, `acts-zh.html`, + * `acts-en.html`, `bnet.html`). `samples/` is served in dev so all of them can * be smoke-tested over HTTP (`?sample=samples/spawn.mpq`, * `?data=samples/fixtures`). */ @@ -27,10 +27,6 @@ export default defineConfig({ 'acts-zh': resolve(__dirname, 'acts-zh.html'), 'acts-en': resolve(__dirname, 'acts-en.html'), bnet: resolve(__dirname, 'bnet.html'), - arena: resolve(__dirname, 'arena.html'), - loot: resolve(__dirname, 'loot.html'), - boss: resolve(__dirname, 'boss.html'), - skills: resolve(__dirname, 'skills.html'), }, }, },