打开/关闭搜索
搜索
打开/关闭菜单
933
1525
112
1.4万
植物大战僵尸杂交版Wiki
导航
首页
最近更改
随机页面
MediaWiki帮助
特殊页面
上传文件
打开/关闭外观设置菜单
notifications
打开/关闭个人菜单
未登录
未登录用户的IP地址会在进行任意编辑后公开展示。
user-interface-preferences
个人工具
创建账号
登录
查看“︁微件:LevelAnalyzer”︁的源代码
来自植物大战僵尸杂交版Wiki
查看
阅读
查看源代码
查看历史
associated-pages
微件
讨论
更多操作
←
微件:LevelAnalyzer
因为以下原因,您没有权限编辑该页面:
您请求的操作仅限属于该用户组的用户执行:
用户
您没有权限编辑
微件
命名空间内的页面。
您可以查看和复制此页面的源代码。
<includeonly> <div class="level-analyzer-widget"> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes, viewport-fit=cover, maximum-scale=5.0"> <title>🌻 植物大战僵尸杂交版 · 关卡分析器 Pro 7.1</title> <style> :root { --bg-dark: #1a2c1e; --wood: #6b4f2c; --gold: #d4a745; --light-bg: #f9f3d9; --card-bg: #fffbe6; --green-btn: #5f7a36; --blue-btn: #2c5aa0; --danger: #c44 } * { margin: 0; padding: 0; box-sizing: border-box } html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; -webkit-tap-highlight-color: transparent } body { background: var(--bg-dark); font-family: 'Segoe UI', 'Roboto', 'Noto Sans SC', system-ui, sans-serif; display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; padding: 1rem } .container { max-width: 1400px; width: 100%; background: linear-gradient(165deg, #f9f3d9, #e8d9a0, #dcc880); border: 4px solid var(--wood); border-radius: 1.5rem; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6); padding: 1rem } .header { text-align: center; margin-bottom: .8rem } h1 { font-size: 1.4rem; font-weight: 900; color: #3e2b15; text-shadow: 2px 2px 0 var(--gold) } h1 .badge { background: #5c3b1a; color: #ffda7a; font-size: .7rem; padding: .2rem .9rem; border-radius: 2rem } .drop-zone { background: rgba(218, 194, 130, .4); border: 3px dashed #8b6b3c; border-radius: 1.2rem; padding: 1rem; margin-bottom: .8rem; text-align: center; cursor: pointer; min-height: 80px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .3rem; transition: all 0.3s ease } .drop-zone.drag-over { background: rgba(180, 210, 120, .5); border-color: #5f7a36; border-style: solid; transform: scale(1.02) } .drop-zone-icon { font-size: 2rem } .file-zone { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: .5rem; margin-bottom: .8rem } .file-card { background: var(--card-bg); border-radius: .7rem; padding: .5rem; box-shadow: 0 4px 12px rgba(0, 0, 0, .15); border: 2px solid #bb9740; text-align: center; transition: all 0.3s ease } .file-card:hover { border-color: #d4a745; transform: translateY(-2px); box-shadow: 0 6px 16px rgba(0, 0, 0, .2) } .file-card h3 { font-size: .7rem; color: #3d2c15; margin-bottom: .2rem; display: flex; align-items: center; justify-content: center; gap: .2rem } .file-card input[type="file"] { display: none } .file-label { display: inline-block; background: #e0ba75; padding: .3rem .6rem; border-radius: .8rem; font-weight: 700; cursor: pointer; font-size: .65rem; border: 2px solid #b8872e; width: 100%; max-width: 130px; text-align: center; transition: all 0.3s ease } .file-label:hover { background: #cfaa54 } .file-label.loaded { background: #7cb342; color: #fff; border-color: #4a7a1a } .status-dot { display: inline-block; width: .5rem; height: .5rem; border-radius: 50%; flex-shrink: 0; transition: all 0.3s ease } .status-dot.ready { background: #3cb043; box-shadow: 0 0 6px #3cb043 } .status-dot.missing { background: #ccc } .file-name-display { font-size: .55rem; color: #6b4f2c; margin-top: .2rem; word-break: break-all; max-height: 2.5em; overflow-y: auto } .btn { background: var(--green-btn); border: none; color: white; font-weight: 700; padding: .4rem .8rem; border-radius: 1.5rem; font-size: .7rem; cursor: pointer; box-shadow: 0 3px 6px rgba(0, 0, 0, .25); border-bottom: 3px solid #334d1a; display: inline-flex; align-items: center; gap: .3rem; white-space: nowrap; transition: all 0.3s ease } .btn:disabled { background: #a5a5a5; cursor: not-allowed; opacity: .6; transform: none } .btn:hover:not(:disabled) { background: #719541; transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0, 0, 0, .3) } .btn:active:not(:disabled) { transform: translateY(1px); box-shadow: 0 1px 3px rgba(0, 0, 0, .3) } .btn-export { background: var(--blue-btn); border-bottom-color: #1a3a6a } .btn-export:hover:not(:disabled) { background: #3a6ab0 } .btn-cache { background: #6b4f2c; font-size: .6rem } .btn-cache:hover:not(:disabled) { background: #8b6f4c } .level-input-area { background: rgba(228, 209, 155, .7); border-radius: .7rem; padding: .5rem; margin: .5rem 0; display: flex; align-items: center; flex-wrap: wrap; gap: .4rem; border: 2px solid #b18530 } .export-name-input { padding: .3rem .5rem; border-radius: 1rem; border: 2px solid #b8872e; font-size: .7rem; background: var(--card-bg); width: 160px; outline: none; transition: border-color 0.3s ease } .export-name-input:focus { border-color: #d4a745; box-shadow: 0 0 0 2px rgba(212, 167, 69, 0.3) } .result-panel { background: #fef9e8; border: 3px solid #7c6033; border-radius: .7rem; padding: .6rem; min-height: 200px; max-height: 500px; overflow-y: auto; font-size: .7rem; scroll-behavior: smooth } .info-grid { display: flex; flex-wrap: wrap; gap: .3rem; margin-bottom: .4rem } .info-badge { background: #c6a45b; padding: .15rem .5rem; border-radius: .6rem; font-weight: 700; font-size: .65rem; white-space: nowrap } .info-badge.survival { background: #5b8c3e; color: #fff } .info-badge.endless { background: #9b4a2e; color: #fff; animation: pulse-glow 2s infinite } .info-badge.shooting { background: #c44; color: #fff } .info-badge.vase { background: #6a4c1a; color: #fff } .info-badge.izm { background: #3a3060; color: #fff } .info-badge.star { background: #d4a017; color: #1a1a1a } .info-badge.shuffle-anim { background: #c44; color: #fff; animation: pulse 1.5s infinite } .info-badge.dynamic { background: #6b3a8a; color: #fff } .info-badge.shuffle { background: #8b3a6a; color: #fff } .info-badge.gem { background: #e04090; color: #fff } .info-badge.conveyor { background: #e67e22; color: #fff } .info-badge.choose { background: #3498db; color: #fff } .info-badge.preset { background: #2e7d32; color: #fff } @keyframes pulse { 0%, 100% { opacity: 1 } 50% { opacity: .6 } } @keyframes pulse-glow { 0%, 100% { box-shadow: 0 0 5px #9b4a2e } 50% { box-shadow: 0 0 15px #ff6b3a } } .zombie-tag, .plant-tag { display: inline-block; padding: .12rem .4rem; border-radius: .4rem; margin: .08rem; font-size: .6rem; word-break: break-all; max-width: 100% } .zombie-tag { background: #2e3b1f; color: #f5e7c6 } .plant-tag { background: #2c5c2c; color: #f2f2c0 } .reward-block { background: #faf0c8; border: 2px solid #d4a017; border-radius: .5rem; padding: .3rem; margin: .25rem 0; font-size: .65rem } .event-block { background: #f5e8c9; border: 1px solid #c9a24b; border-radius: .5rem; padding: .3rem; margin: .25rem 0; font-size: .65rem } .event-block .event-header { font-weight: 700; color: #6b3a1a; display: flex; align-items: baseline; gap: .2rem; flex-wrap: wrap } .event-block .event-detail { color: #4a3020; font-size: .6rem; margin-top: .08rem } .section-title { font-weight: 700; font-size: .75rem; margin-top: .35rem; color: #3d2e12; border-bottom: 2px solid #c9a24b; padding-bottom: .12rem; display: flex; align-items: center; gap: .25rem } .event-count { background: #8b5a2b; color: #fff; border-radius: .3rem; padding: .04rem .3rem; font-size: .55rem; flex-shrink: 0 } .grid-container { overflow-x: auto; -webkit-overflow-scrolling: touch; max-width: 100% } .grid-row { display: flex } .grid-cell { width: clamp(1.1rem, 2.8vw, 1.8rem); height: clamp(1.1rem, 2.8vw, 1.8rem); line-height: clamp(1.1rem, 2.8vw, 1.8rem); text-align: center; background: #e9dbb0; border-radius: .2rem; margin: 1px; font-size: clamp(.3rem, .7vw, .45rem); font-weight: bold; border: 1px solid #8b7a2b; color: #3d2e12; overflow: hidden; cursor: default; position: relative; transition: transform .15s; flex-shrink: 0 } .grid-cell:hover { transform: scale(1.12); z-index: 10 } .grid-cell.preplant { background: #5a8a3c; color: #fff } .grid-cell.preplant-stacked { background: #3a6a2c; color: #fff } .grid-cell.vase-plant { background: #4a9a3c; color: #fff } .grid-cell.vase-zombie { background: #8b3a3a; color: #fff } .grid-cell.vase-normal { background: #b8863a; color: #fff } .grid-cell.lookstar { background: #d4a017; color: #1a1a1a } .grid-cell.izm-plant { background: #5c3a8a; color: #e8d9ff } .grid-cell.izm-plant-stacked { background: #3c1a6a; color: #e8d9ff } .grid-cell .shuffle-marker { position: absolute; top: 0; right: 1px; font-size: .3rem; color: #ff0; text-shadow: 0 0 3px #000; font-weight: bold; line-height: 1; pointer-events: none } .grid-legend { font-size: clamp(.35rem, .8vw, .5rem); margin: .08rem 0; color: #6b4f2c } .col-header, .row-label { text-align: center; color: #5a432b } .col-header { width: clamp(1.1rem, 2.8vw, 1.8rem); font-size: clamp(.3rem, .7vw, .45rem); flex-shrink: 0 } .row-label { width: .7rem; font-size: clamp(.3rem, .7vw, .45rem); line-height: clamp(1.1rem, 2.8vw, 1.8rem); flex-shrink: 0 } .grid-col-headers { display: flex; margin-left: .7rem } .upgrade-line { background: #f0e2b0; border: 2px solid #c9a24b; border-radius: .5rem; padding: .3rem; margin: .25rem 0; font-size: .65rem } .gem-upgrade-line { background: #f8e0f0; border: 2px solid #e04090 } .upgrade-step { display: inline-block; background: #5c3b1a; color: #ffda7a; padding: .08rem .35rem; border-radius: .35rem; margin: .06rem; font-size: .6rem } .arrow { color: #8b5a2b; font-weight: bold; margin: 0 .08rem } .toast { position: fixed; top: 20px; right: 20px; background: #333; color: #fff; padding: .5rem 1rem; border-radius: .7rem; font-size: .75rem; z-index: 9999; opacity: 0; transform: translateX(100%); transition: all .3s ease; pointer-events: none; box-shadow: 0 4px 12px rgba(0, 0, 0, .3); max-width: 90vw } .toast.show { opacity: 1; transform: translateX(0) } .toast.success { background: #5f7a36 } .toast.error { background: #c44 } .zombie-pool-section { background: #f0e6d0; border: 1px dashed #8b6b3c; border-radius: .4rem; padding: .25rem .4rem; margin: .15rem 0; font-size: .6rem; color: #5a3d1a } .table-container { overflow-x: auto; max-width: 100% } .vase-table { width: 100%; border-collapse: collapse; font-size: .6rem; margin: .25rem 0; min-width: 400px } .vase-table th { background: #8b6b2c; color: #fff; padding: .15rem; border: 1px solid #5a3d1a; font-size: .55rem; position: sticky; top: 0; z-index: 5 } .vase-table td { padding: .12rem; border: 1px solid #b8943a; text-align: center; word-break: break-word } .vase-table tr:nth-child(even) { background: #f5e8c9 } .vase-table tr:nth-child(odd) { background: #fef9e8 } .vase-table tr:hover { background: #e8d5a0 } .tutorial-box { background: #e8f0f8; border: 2px solid #3498db; border-radius: .5rem; padding: .4rem; margin: .3rem 0; font-size: .65rem } .tutorial-box .tutorial-title { font-weight: 700; color: #2471a3 } .conveyor-section { background: #fdf2e9; border: 2px solid #e67e22; border-radius: .5rem; padding: .4rem; margin: .25rem 0; font-size: .65rem } .loading-spinner { display: inline-block; width: .9rem; height: .9rem; border: 2px solid rgba(255, 255, 255, .3); border-top-color: #fff; border-radius: 50%; animation: spin .6s linear infinite; vertical-align: middle; margin-right: .25rem } @keyframes spin { to { transform: rotate(360deg) } } @media(max-width:768px) { body { padding: .3rem } .container { border-radius: 1rem; border-width: 3px } .file-zone { grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: .3rem } .level-input-area { justify-content: center } .grid-cell { width: 1rem; height: 1rem; line-height: 1rem; font-size: .28rem } .col-header { width: 1rem; font-size: .28rem } .toast { top: auto; bottom: 15px; right: 8px; left: 8px; max-width: none; transform: translateY(100%) } .toast.show { transform: translateY(0) } } </style> </head> <body> <div class="container"> <div class="header"> <h1>🌻 杂交版关卡分析器 <span class="badge">PVZ HE Pro 7.1</span></h1> <p style="font-size:.7rem;color:#5a432b;">⚡ 修复射击误判 · 精确模式识别 · PacketBank完善</p> </div> <div class="drop-zone" id="dropZone"> <div class="drop-zone-icon">📂</div> <div class="drop-zone-text">拖拽文件到此处上传</div> <div style="font-size:.6rem;color:#7a5c33;">options.json / templates.json / 关卡.json / 生存配置.json</div> </div> <div class="file-zone"> <div class="file-card"> <h3><span class="status-dot missing" id="optDot"></span> options.json</h3> <label class="file-label" for="optInput" id="optLabel">📁 选择文件</label> <input type="file" id="optInput" accept=".json"> <div class="file-name-display" id="optName">未选择</div> </div> <div class="file-card"> <h3><span class="status-dot missing" id="tplDot"></span> templates.json</h3> <label class="file-label" for="tplInput" id="tplLabel">📁 选择文件</label> <input type="file" id="tplInput" accept=".json"> <div class="file-name-display" id="tplName">未选择</div> </div> <div class="file-card"> <h3>🎯 关卡文件</h3> <label class="file-label" for="levelInput" id="levelLabel">📤 上传关卡</label> <input type="file" id="levelInput" accept=".json"> <div class="file-name-display" id="levelFileName">未选择</div> </div> <div class="file-card"> <h3><span class="status-dot missing" id="surDot"></span> 生存配置</h3> <label class="file-label" for="surInput" id="surLabel">📁 选择文件</label> <input type="file" id="surInput" accept=".json" multiple> <div class="file-name-display" id="surName">未选择</div> </div> </div> <div class="level-input-area"> <button class="btn btn-cache" id="cacheBtn">📦 缓存加载</button> <button class="btn" id="analyzeBtn" disabled>🔍 解析关卡</button> <span style="font-weight:600;font-size:.65rem;">📥 导出名:</span> <input type="text" class="export-name-input" id="exportFileName" placeholder="自定义文件名" value="" spellcheck="false"> <button class="btn btn-export" id="exportBtn" disabled>📥 导出CSV</button> <button class="btn" id="resetBtn" style="background:var(--danger);border-bottom-color:#8b1a1a;">🔄 重置</button> </div> <div id="output" class="result-panel">🌿 请上传 options.json 和 templates.json,然后选择或拖入关卡文件并点击解析。<br><span style="font-size:.6rem;color:#8b6b3c;">💡 可直接拖拽JSON文件到上方区域 | 📦 点击"缓存加载"恢复上次文件 | Ctrl+Enter 快速解析 | Ctrl+S 导出CSV</span></div> </div> <div class="toast" id="toast"></div> <script> (function() { 'use strict'; const $ = id => document.getElementById(id); const S = { optionsData: null, templatesData: null, levelData: null, survivalConfigs: new Map(), lastMaps: null, isAnalyzing: false, MAX_FILE_SIZE: 100 * 1024 * 1024, TIMEOUT: 30000, CACHE_KEY: 'pvz_cache_v21' }; let toastTimer = null; let lastToastMsg = ''; let toastQueue = []; function toast(m, t, force = false) { if (!m) return; if (!force && m === lastToastMsg) return; toastQueue.push({ m, t, force }); if (toastQueue.length > 3) toastQueue.shift(); processToastQueue(); } function processToastQueue() { if (toastQueue.length === 0) return; if (toastTimer) return; const { m, t } = toastQueue.shift(); lastToastMsg = m; const el = $('toast'); if (!el) return; el.textContent = m; el.className = 'toast ' + (t || '') + ' show'; toastTimer = setTimeout(() => { el.classList.remove('show'); toastTimer = null; setTimeout(processToastQueue, 300); }, 2500); } function saveCache() { try { const c = { opt: S.optionsData, tpl: S.templatesData, surv: Array.from(S.survivalConfigs.entries()), timestamp: Date.now() }; const j = JSON.stringify(c); if (j.length > 8 * 1024 * 1024) { const compressed = { opt: S.optionsData ? { plant: S.optionsData.plant, zombie: S.optionsData.zombie, zombieExtra: S.optionsData.zombieExtra, map: S.optionsData.map, packetBank: S.optionsData.packetBank, PacketBank: S.optionsData.PacketBank, bgm: S.optionsData.bgm, projectile: S.optionsData.projectile, collectable: S.optionsData.collectable, item: S.optionsData.item } : null, tpl: S.templatesData ? { plant: S.templatesData.plant, zombie: S.templatesData.zombie, zombieExtra: S.templatesData.zombieExtra, map: S.templatesData.map, packetBank: S.templatesData.packetBank, PacketBank: S.templatesData.PacketBank } : null, surv: Array.from(S.survivalConfigs.entries()), timestamp: Date.now() }; const cj = JSON.stringify(compressed); if (cj.length < 8 * 1024 * 1024) { localStorage.setItem(S.CACHE_KEY, cj); } } else { localStorage.setItem(S.CACHE_KEY, j); } } catch (e) {} } function loadCache() { try { const s = localStorage.getItem(S.CACHE_KEY); if (!s) return false; const c = JSON.parse(s); if (!c) return false; if (c.timestamp && Date.now() - c.timestamp > 7 * 24 * 60 * 60 * 1000) { localStorage.removeItem(S.CACHE_KEY); return false; } if (c.opt) S.optionsData = c.opt; if (c.tpl) S.templatesData = c.tpl; if (Array.isArray(c.surv)) S.survivalConfigs = new Map(c.surv); return !!(c.opt || c.tpl); } catch (e) { return false; } } const dZE = $('dropZone'); function pd(e) { e.preventDefault(); e.stopPropagation(); } if (dZE) { dZE.addEventListener('click', () => { $('levelInput')?.click(); }); ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(ev => dZE.addEventListener(ev, pd, false)); let dc = 0; dZE.addEventListener('dragenter', () => { dc++; dZE.classList.add('drag-over'); }, false); dZE.addEventListener('dragleave', () => { dc--; if (dc <= 0) { dc = 0; dZE.classList.remove('drag-over'); } }, false); dZE.addEventListener('drop', (e) => { dc = 0; dZE.classList.remove('drag-over'); hD(e); }, false); } async function hD(e) { const dt = e.dataTransfer; if (!dt?.files || !dt.files.length) { toast('请拖入JSON文件', 'error'); return; } const files = [...dt.files].filter(f => { const n = (f.name || '').toLowerCase(); return n.endsWith('.json') || n .endsWith('.txt'); }); if (!files.length) { toast('仅支持.json/.txt文件', 'error'); return; } let lc = 0; const errors = []; const rs = await Promise.allSettled(files.map(f => rF(f).then(dt => ({ file: f, data: dt })))); rs.forEach(r => { if (r.status === 'fulfilled') { const { file, data } = r.value; const nl = (file.name || '').toLowerCase(); try { if (nl.includes('option')) { S.optionsData = data; $('optName').textContent = '✅ ' + file.name; lc++; } else if (nl.includes('template')) { S.templatesData = data; $('tplName').textContent = '✅ ' + file.name; lc++; } else if (data.ZombiePoolSetting || data.RoundLimit !== undefined) { S.survivalConfigs .set(file.name.replace(/\.(json|txt)$/i, ''), data); $('surName').textContent = '✅ ' + S.survivalConfigs.size + '个'; lc++; } else { S.levelData = data; $('levelFileName').textContent = '✅ ' + file.name; lc++; } } catch (err) { errors.push(file.name + ': ' + err.message); } } else { errors.push(r.reason?.message || '未知错误'); } }); if (lc > 0) { updateUI(); saveCache(); $('output').innerHTML = '✅ 成功加载 ' + lc + ' 个文件'; if (errors.length > 0) toast('部分加载失败: ' + errors.join( ', '), 'error', true); } else if (errors.length > 0) toast('加载失败: ' + errors.join(', '), 'error', true); } document.body.addEventListener('dragover', pd, false); document.body.addEventListener('drop', pd, false); function rF(file) { return new Promise((resolve, reject) => { if (!file || !(file instanceof Blob)) return reject(new Error('无效文件')); if (!file.name) return reject(new Error('文件名缺失')); if (file.size === 0) return reject(new Error('空文件')); if (file.size > S.MAX_FILE_SIZE) return reject(new Error('文件过大(>100MB)')); const reader = new FileReader(); const timeout = setTimeout(() => { reader.abort(); reject(new Error('读取超时')); }, S.TIMEOUT); reader.onload = (e) => { clearTimeout(timeout); try { const text = e.target.result; if (!text || (typeof text === 'string' && text.trim().length === 0)) return reject(new Error('内容为空')); let data; try { data = JSON.parse(text); } catch (pe) { return reject(new Error('JSON格式错误: ' + pe.message)); } if ( data === null || typeof data !== 'object') return reject(new Error('不是有效JSON对象')); resolve(data); } catch (err) { reject(new Error('处理失败: ' + err.message)); } }; reader.onerror = () => { clearTimeout(timeout); reject(new Error('读取失败')); }; reader.onabort = () => { clearTimeout(timeout); reject(new Error('读取被中断')); }; try { reader.readAsText(file, 'UTF-8'); } catch (e) { clearTimeout( timeout); reject(new Error('无法读取文件')); } }); } function updateUI() { const oR = !!S.optionsData, tR = !!S.templatesData, lR = !!S.levelData; ['optDot', 'tplDot'].forEach(id => { const el = $(id); if (el) el.className = 'status-dot ' + ((id === 'optDot' ? oR : tR) ? 'ready' : 'missing'); }); const sD = $('surDot'); if (sD) sD.className = 'status-dot ' + (S.survivalConfigs.size > 0 ? 'ready' : 'missing'); ['optLabel', 'tplLabel', 'surLabel', 'levelLabel'].forEach(id => { const el = $(id); if (el) el.className = 'file-label' + ((id === 'optLabel' ? oR : id === 'tplLabel' ? tR : id === 'surLabel' ? S.survivalConfigs .size > 0 : lR) ? ' loaded' : ''); }); const ab = $('analyzeBtn'), eb = $('exportBtn'); if (ab) ab.disabled = !(oR && tR && lR) || S.isAnalyzing; if (eb) eb.disabled = !(oR && tR && lR && !!S.lastMaps); } function setupFileInput(inputEl, setDataFn, nameSpan) { if (!inputEl || !nameSpan) return; inputEl.addEventListener('change', function() { const file = inputEl.files?.[0]; if (!file) return; const fileName = (file.name || '').toLowerCase(); if (!fileName.endsWith('.json') && !fileName.endsWith( '.txt')) { toast('仅支持.json/.txt文件', 'error'); inputEl.value = ''; return; } nameSpan.textContent = '⏳ 加载中...'; rF(file).then(data => { setDataFn(data); nameSpan.textContent = '✅ ' + file.name; updateUI(); saveCache(); toast('已加载:' + file.name, 'success'); }).catch(err => { toast('❌ ' + (err.message || '加载失败'), 'error', true); nameSpan.textContent = '加载失败'; updateUI(); }).finally(() => { inputEl.value = ''; }); }); } function setupSurvivalInput(inputEl) { if (!inputEl) return; inputEl.addEventListener('change', async function() { const files = [...inputEl.files].filter(f => { const n = f .name.toLowerCase(); return n.endsWith('.json') || n.endsWith('.txt'); }); let sc = 0; for (const file of files) { try { const data = await rF(file); S.survivalConfigs.set(file.name.replace(/\.(json|txt)$/i, ''), data); sc++; } catch (err) { toast('❌ ' + file.name + ': ' + err.message, 'error', true); } } if (sc > 0) { $('surName').textContent = '✅ ' + S.survivalConfigs.size + '个'; toast('成功加载' + sc + '个生存配置', 'success'); } updateUI(); saveCache(); inputEl.value = ''; }); } $('cacheBtn')?.addEventListener('click', () => { if (loadCache()) { $('optName').textContent = S.optionsData ? '✅ 缓存' : '未选择'; $('tplName').textContent = S.templatesData ? '✅ 缓存' : '未选择'; $('surName').textContent = S.survivalConfigs.size > 0 ? '✅ ' + S.survivalConfigs.size + '个' : '未选择'; updateUI(); toast('已从缓存加载', 'success'); } else toast('缓存为空或已过期', 'error'); }); $('resetBtn')?.addEventListener('click', () => { if (confirm('确定要重置所有数据吗?此操作不可恢复!')) { S.optionsData = null; S.templatesData = null; S.levelData = null; S.survivalConfigs.clear(); S.lastMaps = null; ['optName', 'tplName', 'levelFileName', 'surName'].forEach(id => { const el = $(id); if (el) el .textContent = '未选择'; }); $('exportFileName').value = ''; $('output').innerHTML = '🌿 已重置所有数据。请重新上传文件。'; updateUI(); try { localStorage.removeItem(S.CACHE_KEY); } catch (e) {} toast('已重置', 'success'); } }); function esc(t) { if (t == null) return ''; return String(t).replace(/[&<>"]/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[m]); } function gMS(mv) { if (!S.optionsData?.map) return { cols: 9, rows: 5 }; const mi = S.optionsData.map.find(m => m.value === mv); if (mi?.size) { const p = String(mi.size).split(/[x×X]/); const c = parseInt(p[0], 10), r = parseInt(p[1], 10); if (!isNaN(c) && c > 0 && c <= 20 && !isNaN(r) && r > 0 && r <= 10) return { cols: c, rows: r }; } if (S.templatesData?.map) { const tmi = S.templatesData.map.find(m => m.value === mv); if (tmi?.size) { const p = String(tmi.size).split(/[x×X]/); const c = parseInt(p[0], 10), r = parseInt(p[1], 10); if (!isNaN(c) && c > 0 && c <= 20 && !isNaN(r) && r > 0 && r <= 10) return { cols: c, rows: r }; } } return { cols: 9, rows: 5 }; } function pGP(gp) { if (!Array.isArray(gp) || gp.length < 2) return null; const c = Math.round(Number(gp[0])), r = Math.round(Number(gp[1])); if (isNaN(c) || isNaN(r) || c < 1 || r < 1) return null; return { col: c, row: r }; } function bM() { const m = { plant: new Map(), zombie: new Map(), zombieExtra: new Map(), item: new Map(), map: new Map(), bgm: new Map(), projectile: new Map(), collectable: new Map(), packetBank: new Map() }; if (!S.optionsData) return m; const sa = arr => Array.isArray(arr) ? arr : []; [ ['plant', m.plant], ['zombie', m.zombie], ['zombieExtra', m.zombieExtra], ['item', m.item], ['bgm', m.bgm], ['projectile', m.projectile], ['collectable', m.collectable] ].forEach(([k, t]) => { sa(S.optionsData[k]).forEach(i => { if (i && i.value != null) t.set(String(i.value), i .label || i.alias || String(i.value)); }); }); sa(S.optionsData.map).forEach(mp => { if (mp && mp.value != null) m.map.set(String(mp.value), { label: mp.label || mp.alias || String(mp.value), size: mp.size || '9x5' }); }); const pks = ['packetBank', 'PacketBank', 'packetbank', 'PACKETBANK']; const aks = ['Packets', 'packets', 'Packet', 'packet', 'Value', 'value', 'Cards', 'cards']; pks.forEach(key => { sa(S.optionsData[key]).forEach(pb => { if (pb && pb.value != null && !m.packetBank.has( String(pb.value))) { const packets = []; aks.forEach(ak => { if (Array.isArray(pb[ak])) packets.push(...pb[ak]); }); if (packets.length > 0) m.packetBank.set(String(pb.value), { label: pb.label || pb.name || pb.alias || String(pb .value), packets: packets }); } }); }); if (S.templatesData) { pks.forEach(key => { sa(S.templatesData[key]).forEach(pb => { if (pb && pb.value != null && !m.packetBank.has(String(pb.value))) { const packets = []; aks.forEach(ak => { if (Array.isArray(pb[ak])) packets.push(...pb[ak]); }); if (packets .length > 0) m.packetBank.set(String(pb.value), { label: pb.label || pb.name || pb .alias || String(pb.value), packets: packets }); } }); }); } return m; } function gWI(l) { const wm = l.WaveManager || l.Feature?.find(f => f.Name === 'Wave')?.Data; if (!wm?.Wave || !Array.isArray(wm.Wave) || wm.Wave.length === 0) return null; const tw = wm.Wave.length, fi = wm.FlagWaveInterval || 6; if (typeof fi !== 'number' || fi <= 0 || !isFinite(fi)) return { totalWaves: tw, flagInterval: 6, flagCount: Math.floor((tw - 1) / 6) + 1 }; return { totalWaves: tw, flagInterval: fi, flagCount: Math.floor((tw - 1) / fi) + 1 }; } function tN(n, m) { if (!n || typeof n !== 'string') return ''; const t = n.trim(); if (!t) return ''; return m.plant.get( t) || m.zombie.get(t) || m.zombieExtra.get(t) || m.item.get(t) || m.collectable.get(t) || t; } function pN(pv, m) { return pv ? (m.projectile.get(pv) || pv) : ''; } function zL(v, m) { if (!v || typeof v !== 'string') return ''; return m.zombie.get(v) || m.zombieExtra.get(v) || v; } function pL(v, m) { if (!v || typeof v !== 'string') return ''; return m.plant.get(v) || v; } function aL(v, m) { if (!v || typeof v !== 'string') return ''; return m.plant.get(v) || m.zombie.get(v) || m.zombieExtra .get(v) || m.item.get(v) || v; } function pR(rw, m) { if (!rw?.RewardType || rw.RewardType === 'Noone') return '无奖励'; const ty = rw.RewardType, f = rw.RewardFirst || ''; let r = ''; switch (ty) { case 'Coin': case 'COIN': r = '💰 金币奖励'; if (f) r += `:首次${f}`; break; case 'Packet': r = '🎴 卡牌奖励'; if (f) r += `:${tN(f,m)}`; break; case 'Collectable': r = '📦 收藏品奖励'; if (f) r += `:${tN(f,m)}`; break; default: r = `🎁 ${ty}`; if (f) r += ` (${tN(f,m)})`; } return r; } const mFL = { 'LineUseSet': '设置出怪行状态', 'UseStripe': '限制种植列(红线分隔)', 'Row1Create': '创建3行草皮', 'ShowRow3': '裸地第二三四行显示草皮', 'Row5CreateFromRow3': '在第一五行创建草皮', 'ClearAll': '清除所有对象', 'UseWarningLine': '设置警戒线', 'CreateRow': '创建行' }; const sTL = { 'Default': '正常冒出', 'Bungi': '蹦极放置', 'Normal': '正常' }; const cL = { 'White': '白卡', 'Gold': '金卡', 'Diamond': '钻卡', 'Colour': '彩卡', 'Star': '星卡', 'Original': '原卡+紫卡', 'Purple': '紫卡' }; function eDD(ev, m) { if (!ev || typeof ev !== 'object') return '未知事件'; const n = ev.EventName || '', v = ev.Value || {}; switch (n) { case 'GravestoneCreateRandom': return `随机生成物体:${Array.isArray(v.GravestoneNames)?v.GravestoneNames.map(x=>aL(x,m)).join('、'):'?'} ×${v.GravestoneNum||'?'}`; case 'GravestoneSpawnZombie': return `墓碑生成僵尸:${Array.isArray(v.ZombieNames)?v.ZombieNames.map(x=>zL(x,m)).join('、'):'?'} ×${v.ZombieNum||'?'}`; case 'GridSpawnZombie': return `图格生成僵尸:${Array.isArray(v.ZombieNames)?v.ZombieNames.map(x=>zL(x,m)).join('、'):'?'} ×${v.ZombieNum||'?'}${v.SpawnPos?`,区域:[${v.SpawnPos.x??'?'},${v.SpawnPos.y??'?'}]~[${v.SpawnPos.z??'?'},${v.SpawnPos.w??'?'}]`:''}${v.SpawnType?`,${sTL[v.SpawnType]||v.SpawnType}`:''}`; case 'BungiSpawnZombie': return `蹦极放置僵尸:${Array.isArray(v.ZombieNames)?v.ZombieNames.map(x=>zL(x,m)).join('、'):'?'} ×${v.ZombieNum||'?'}${v.hypnoses?',魅惑':''}`; case 'SpawnZombie': return `生成僵尸:${zL(v.ZombieName||v.Zombie||'?',m)} ×${v.ZombieNum||v.Num||1}`; case 'CurrentMapUseWarningLine': return `设置警戒线:第${v.Row??'?'}列`; case 'CurrentMapUseStripe': return `限制种植列(红线分隔),左侧列数:${v.Row??'?'}`; case 'CreateProtal': return `创建传送门${v.Shape?',样式:'+esc(String(v.Shape)):''}`; case 'TipsPlay': return `显示提示${v.Text!==undefined&&v.Text!==null&&String(v.Text).trim()!==''?`:"${esc(String(v.Text))}"`:''}${v.Duration?`,持续${v.Duration}秒`:''}`; case 'SunCreate': return `生成阳光 ×${v.Num||'?'}${v.FromPacket?`(继承卡牌×${v.FromPacketPercentage||1})`:''}${v.ByCamp?',跟随阵营':''}`; case 'PacketCreate': return `创建角色卡牌${v.PacketName?`:${aL(v.PacketName,m)}`:v.PacketBankName?`:从选卡范围[${v.PacketBankName}]随机${v.CategoryName?`,品质:${cL[v.CategoryName]||v.CategoryName}`:''}`:''}${v.AliveTime?`,存在${v.AliveTime}秒`:''}`; case 'PacketSpawn': return `生成角色${v.PacketName?`:${aL(v.PacketName,m)}`:''}${v.Percentage!=null&&v.Percentage<1?`,概率${Math.round(v.Percentage*100)}%`:''}${v.ByCamp?',跟随阵营':''}`; case 'SetGameMode': return `改变模式:${v.GameMode||'?'}`; case 'CurrentMapCharacterClear': return '清除场景所有对象'; case 'CurrentMapFunctionExecute': return `地图行为:${mFL[v.FunctionName]||v.FunctionName||'未知行为'}${v.FunctionName==='UseStripe'?`,列数:${Array.isArray(v.Value)?v.Value[0]:(v.Value??'?')}`:v.FunctionName==='LineUseSet'?`,行${Array.isArray(v.Value)?v.Value[0]:'?'}${Array.isArray(v.Value)&&v.Value.length>1?v.Value[1]:true?'允许出怪':'禁止出怪'}`:''}`; case 'MapChange': return `切换地图:${m.map.get(v.MapName)?.label||v.MapName||'?'}`; case 'AddPacket': return `传送带增加卡牌:${aL(v.Name,m)},权重${v.Weight||'?'}`; case 'ChangePacket': return `改变卡牌属性:${pL(v.PacketName||'',m)||zL(v.PacketName||'',m)||v.PacketName||'?'}`; case 'Delete': return '删除卡牌'; case 'CraterCreate': return '创建弹坑'; case 'ChangeProtalPos': return '改变传送门位置'; case 'CheckCharacterNum': return `场上${aL(v.CharacterName||'?',m)}数量${{'=':'等于','>':'大于','<':'小于','>=':'大于等于','<=':'小于等于','==':'等于'}[v.Method||'=']||v.Method||'='}${v.Num??'?'}`; case 'CheckSunCollect': return `收集${v.Num||'?'}阳光`; default: return n || '未知事件'; } } function cAED(l) { const es = []; const a = (list, src) => { if (Array.isArray(list)) list.forEach(e => { if (e && typeof e === 'object') es.push({ ... e, _source: src }); }); }; a(l.Event?.EventInit, '初始化'); a(l.Event?.EventReady, '准备阶段'); a(l.Event?.EventStart, '开局'); if (l.Feature) { const ef = l.Feature.find(f => f.Name === 'Event'); if (ef?.Data) { a( ef.Data.EventInit, '初始化'); a(ef.Data.EventReady, '准备阶段'); a(ef.Data.EventStart, '开局'); } } const wm = l.WaveManager || l.Feature?.find(f => f.Name === 'Wave')?.Data; if (Array.isArray(wm?.Wave)) wm.Wave .forEach((w, i) => { if (w && typeof w === 'object') a(w.Event, `第${i+1}波`); }); if (wm?.SpawnOverride ?.DieEvent) a(wm.SpawnOverride.DieEvent, '全局出怪死亡'); if (wm?.SpawnOverride?.SpawnEvent) a(wm.SpawnOverride .SpawnEvent, '全局出怪生成'); return es; } function cAZ(l, m) { const zs = new Map(); const aZ = (k, c, id = false) => { if (!k || typeof k !== 'string') return; const n = (typeof c === 'number' && c > 0 && isFinite(c)) ? c : 1; const ex = zs.get(k); if (ex) { ex.count += n; if (id) ex.isDynamic = true; } else zs.set(k, { label: zL(k, m), count: n, isDynamic: id }); }; const wm = l.WaveManager || l.Feature?.find(f => f.Name === 'Wave')?.Data; if (Array.isArray(wm?.Wave)) { wm.Wave .forEach(w => { if (!w || typeof w !== 'object') return; if (Array.isArray(w.Spawn)) w.Spawn.forEach( s => { if (s?.Zombie) aZ(s.Zombie, s.Num || 1); }); if (w.Dynamic?.ZombiePool && Array.isArray(w .Dynamic.ZombiePool)) w.Dynamic.ZombiePool.forEach(z => { if (z && typeof z === 'string') aZ(z, -1, true); }); if (Array.isArray(w.Event)) w.Event.forEach(ev => { if (!ev) return; const en = ev.EventName || ''; if ((en === 'BungiSpawnZombie' || en === 'GravestoneSpawnZombie' || en === 'GridSpawnZombie') && Array.isArray(ev.Value ?.ZombieNames)) ev.Value.ZombieNames.forEach(z => aZ(z, ev.Value?.ZombieNum || 1)); if ( en === 'SpawnZombie' && ev.Value?.ZombieName) aZ(ev.Value.ZombieName, ev.Value .ZombieNum || 1); if (en === 'PacketSpawn' && ev.Value?.PacketName && (m.zombie.has( ev.Value.PacketName) || m.zombieExtra.has(ev.Value.PacketName))) aZ(ev.Value .PacketName, 1); }); }); } if (Array.isArray(l.VaseManager?.Vase)) l.VaseManager.Vase .forEach(v => { if (v?.PacketName && (m.zombie.has(v.PacketName) || m.zombieExtra.has(v.PacketName))) aZ(v .PacketName, 1); }); return zs; } function cAP(l, m) { const ps = new Map(); const aP = (k, c) => { if (!k || typeof k !== 'string' || !m.plant.has(k)) return; const n = (typeof c === 'number' && c > 0 && isFinite(c)) ? c : 1; const ex = ps.get(k); if (ex) ex.count += n; else ps.set(k, { label: pL(k, m), count: n }); }; if (Array.isArray(l.PreSpawn?.Packet)) l.PreSpawn.Packet .forEach(p => { if (p?.Name) aP(p.Name, 1); }); if (Array.isArray(l.VaseManager?.Vase)) l.VaseManager.Vase .forEach(v => { if (v?.PacketName) aP(v.PacketName, 1); }); return ps; } function pUC(pb, m) { let packets = null; if (pb?.Packet && Array.isArray(pb.Packet) && pb.Packet.length > 0) packets = pb.Packet; else if (pb?.Value && Array.isArray(pb.Value) && pb.Value.length > 0) packets = pb.Value; if (!packets) return null; const cs = [], MD = 20; packets.forEach(pkt => { if (!pkt) return; const ch = []; let cur = typeof pkt === 'string' ? { PacketName: pkt } : pkt; if (!cur || !cur.PacketName) return; const ei = (node, dn) => { const n = dn || node?.PacketName || '?'; if (!node || !node.Override) return { name: pL(n, m), cost: 0, interval: null, fireNum: null, projLabel: '' }; const cgs = Array.isArray(node.Override.CharacterOverride?.PropertyChange) ? node.Override.CharacterOverride.PropertyChange : []; return { name: pL(n, m), cost: node.Override .Cost ?? node.Cost ?? 0, interval: cgs.find(c => c?.PropertyName === 'fireInterval')?.Value ?? null, fireNum: cgs.find(c => c?.PropertyName === 'fireNum')?.Value ?? null, projLabel: pN(cgs .find(c => c?.PropertyName === 'projectileName')?.Value, m) }; }; ch.push(ei(cur, cur.PacketName)); if (!cur.Override?.EventPlant) return; let d = 0, cn = cur, ldn = cur.PacketName; while (cn?.Override?.EventPlant && d < MD) { const evt = cn.Override.EventPlant .find(e => e?.EventName === 'ChangePacket'); if (!evt?.Value) { if (cn.Override.EventPlant.find( e => e?.EventName === 'Delete')) break; break; } const ndn = evt.Value.PacketName || ldn; ldn = ndn; cn = evt.Value; d++; ch.push(ei(cn, ndn)); } if (ch.length > 1) cs.push(ch); }); return cs.length > 0 ? cs : null; } // 【7.1新增】检测是否存在射击升级线路 function hasShootingUpgrades(pb, m) { if (!pb) return false; let packets = null; if (pb?.Packet && Array.isArray(pb.Packet) && pb.Packet.length > 0) packets = pb.Packet; else if (pb?.Value && Array.isArray(pb.Value) && pb.Value.length > 0) packets = pb.Value; if (!packets) return false; return packets.some(pkt => { if (!pkt || typeof pkt === 'string') return false; if (!pkt.PacketName) return false; return !!(pkt.Override?.EventPlant?.some(e => e?.EventName === 'ChangePacket')); }); } function bCFL(list, m, lf) { const cs = new Map(); if (!Array.isArray(list)) return cs; list.forEach(item => { if (!item) return; const pos = pGP(item.GridPos); if (pos) { const k = `${pos.col},${pos.row}`; const lb = lf(item.PacketName || item.Name, m); if (cs.has(k)) { const ex = cs .get(k); ex.labels.push(lb); ex.stacked = true; } else cs.set(k, { labels: [lb], stacked: false }); } }); return cs; } function getVaseTypeClass(type) { return type === 'Plant' ? 'vase-plant' : type === 'Zombie' ? 'vase-zombie' : 'vase-normal'; } function getVaseTypeLabel(type) { return type === 'Plant' ? '植物罐' : type === 'Zombie' ? '僵尸罐' : '普通罐'; } function renderGrid(mv, cs, cc, title, shuffled = false) { const { cols, rows } = gMS(mv); let h = `<div class="section-title">${esc(title)}</div><div class="grid-legend"><span>列1-${cols} 行1-${rows}</span></div><div class="grid-col-headers">`; for (let c = 1; c <= cols; c++) h += `<span class="col-header">${c}</span>`; h += `</div><div class="grid-container">`; for (let r = 1; r <= rows; r++) { h += `<div class="grid-row"><span class="row-label">${r}</span>`; for (let c = 1; c <= cols; c++) { const cell = cs.get(`${c},${r}`); if (cell) { let cls = cc || ''; if (cell.vaseType) cls = getVaseTypeClass(cell.vaseType); if (cell.stacked) cls += (cc === 'izm-plant' ? ' izm-plant-stacked' : ' preplant-stacked'); const dl = cell.labels.map(l => (l || '?') .substring(0, 2)).join('/'); const tt = cell.labels.join(' + '); h += `<span class="grid-cell ${cls}" title="${esc(tt)}">${esc(dl)}${(shuffled||cell.shuffled)?'<span class="shuffle-marker">↻</span>':''}</span>`; } else h += `<span class="grid-cell">·</span>`; } h += `</div>`; } h += `</div>`; return h; } function parseGemMatch(gm, m) { if (!gm?.Data) return ''; const d = gm.Data; let h = '<div class="section-title">💎 宝石迷阵模式</div>'; const rows = d.boardRows || 5, cols = d.boardCols || 8; h += `<div class="info-grid"><span class="info-badge gem">💎 棋盘:${rows}行×${cols}列</span><span class="info-badge">☀️ 每消阳光:${d.sunPerMatch||0}</span><span class="info-badge">💰 每消分数:${d.matchValuePerMatch||0}</span><span class="info-badge">🎯 清空分数:${d.clearMatchValue||0}</span><span class="info-badge">⏬ 下落速度:${d.fallSpeed||0}</span><span class="info-badge">🔧 填洞花费:${d.fillHoleCost||0}</span></div>`; if (Array .isArray(d.plantList) && d.plantList.length > 0) { h += '<div class="section-title">🌱 棋盘可用植物</div><div>'; d.plantList.forEach(p => { if (p && typeof p === 'string') h += `<span class="plant-tag">💎 ${esc(pL(p,m))}</span>`; }); h += '</div>'; } if (Array.isArray(d.plantUpgradeList) && d.plantUpgradeList.length > 0) { h += '<div class="section-title">🔮 宝石迷阵升级路线</div>'; const um = new Map(), rm = new Map(), cs = new Map(); d.plantUpgradeList.forEach(up => { um.set(up.from, up.to); rm.set(up.to, up.from); cs.set(up.to, up.cost || 0); }); const starts = [...new Set(d.plantUpgradeList.filter(up => !rm.has(up .from)).map(up => up.from))]; starts.forEach((start, si) => { let cur = start; const ch = []; const visited = new Set(); while (cur && ! visited.has(cur)) { visited.add(cur); ch.push({ name: cur, cost: cs.get(cur) || 0 }); cur = um.get(cur); } if (ch.length >= 2) { h += `<div class="upgrade-line gem-upgrade-line"><strong>线路${si+1}:</strong>`; ch.forEach((s, i) => { if (i > 0) h += '<span class="arrow">→</span>'; h += `<span class="upgrade-step" style="background:#8b2050;">${esc(pL(s.name,m))}${s.cost?' ['+s.cost+']':''}</span>`; }); h += '</div>'; } }); } return h; } function pLvl(l, m) { if (!l || !m) return '<span style="color:#c44;">❌ 无效数据</span>'; const isNF = !!l.Feature; let wm, mapName, bgmName, sunData, packetBank, reward, vaseManager, preSpawn, lookStar, finishMethod; let gm = null, izS = undefined, eventData, conveyorData = null; let packetBankFeatureData = null; if (isNF) { const wf = l.Feature.find(f => f.Name === 'Wave'), mf = l.Feature.find(f => f.Name === 'Map'), bf = l.Feature.find(f => f.Name === 'BGM'), sf = l.Feature.find(f => f.Name === 'Sun'), sef = l.Feature.find(f => f.Name === 'SeedBank'), pf = l.Feature.find(f => f.Name === 'Process'), ef = l.Feature.find(f => f.Name === 'Event'), psf = l.Feature.find(f => f.Name === 'PreSpawn'); gm = l.Feature.find(f => f.Name === 'GemMatch'); const cbf = l.Feature.find(f => f.Name === 'ConveyorBelt'), pbf = l.Feature.find(f => f.Name === 'PacketBank'); wm = wf?.Data; mapName = mf?.Data?.MapName; bgmName = bf?.Data?.BGMName || bf?.Data?.BackgroundMusic; sunData = sf?.Data; packetBank = sef?.Data || {}; packetBankFeatureData = pbf?.Data; finishMethod = pf?.Data?.Name || pf?.Name || 'Wave'; if (pf?.Data?.Shuffle !== undefined) izS = pf.Data .Shuffle; else if (l.IZMManager?.Shuffle !== undefined) izS = l.IZMManager.Shuffle; else if (l.Process?.Data?.Shuffle !== undefined) izS = l.Process.Data.Shuffle; reward = l.Reward; vaseManager = l.VaseManager; preSpawn = psf?.Data || l.PreSpawn; lookStar = l.LookStarManager; eventData = ef?.Data || l.Event; if (cbf?.Data) conveyorData = cbf.Data; if (!conveyorData && packetBank ?.ConveyorPreset) conveyorData = packetBank.ConveyorPreset; if (!conveyorData && packetBankFeatureData?.ConveyorPreset) conveyorData = packetBankFeatureData.ConveyorPreset; } else { wm = l.WaveManager; mapName = l.Map; bgmName = l.BGM; sunData = l.SunManager; packetBank = l.PacketBank || {}; finishMethod = l.FinishMethod || 'Wave'; izS = l.IZMManager?.Shuffle; reward = l.Reward; vaseManager = l.VaseManager; preSpawn = l.PreSpawn; lookStar = l.LookStarManager; eventData = l.Event; if (packetBank?.ConveyorPreset) conveyorData = packetBank.ConveyorPreset; } const fin = (finishMethod || 'Wave').toUpperCase(); const isS = !!wm?.Survival; const isE = isS && String(wm.Survival).includes('Endless'); const isV = fin === 'VASE' || fin === 'Vase'; const isI = fin === 'IZM' || fin === 'IZM2'; const isI2 = fin === 'IZM2'; const isSt = !!lookStar?.Open; const isConveyor = !!conveyorData || packetBank?.Method === 'Conveyor' || packetBankFeatureData?.Method === 'Conveyor'; const isChoose = packetBank?.Method === 'Choose' || packetBankFeatureData?.Method === 'Choose'; const isPreset = packetBank?.Method === 'Preset' || packetBankFeatureData?.Method === 'Preset'; // 【7.1修复】射击模式判断:必须存在升级线路,不能仅凭Preset判断 const combinedPB = { ...(packetBankFeatureData || {}), ...(packetBank || {}) }; const hasUpgrades = hasShootingUpgrades(combinedPB, m); const isSh = !isS && !isV && !isI && !isConveyor && !isChoose && (isPreset || combinedPB.PacketBankName) && hasUpgrades && Array.isArray(wm?.Wave) && wm.Wave.length > 0; let hDyn = false; if (Array.isArray(wm?.Wave)) hDyn = wm.Wave.some(w => w?.Dynamic?.ZombiePool && Array.isArray(w .Dynamic.ZombiePool) && w.Dynamic.ZombiePool.length > 0); const { rows, cols } = gMS(mapName); const ml = m.map.get(mapName)?.label || mapName || '未知地图'; const rw = pR(reward, m); const wi = gWI(l); let h = `<div style="font-weight:bold;font-size:1rem;">📋 ${esc(l.LevelName||l.Name||'未命名')}</div><div class="info-grid">`; h += `<span class="info-badge">🗺️ ${esc(ml)}(${rows}行×${cols}列)</span><span class="info-badge">🎵 ${esc(m.bgm.get(bgmName)||bgmName||'默认')}</span><span class="info-badge">☀️${sunData?.Begin??'?'}</span><span class="info-badge">🏁${fin}</span>`; if (isNF) h += `<span class="info-badge" style="background:#e04090;color:#fff;">📦 Feature</span>`; if (gm) h += `<span class="info-badge gem">💎 宝石迷阵</span>`; if (isS) h += `<span class="info-badge survival">🔄生存${isE?'(无尽)':'(基础)'}</span>`; if (isE) h += `<span class="info-badge endless">♾️无尽</span>`; if (isSh) h += `<span class="info-badge shooting">🔫射击</span>`; if (isV) h += `<span class="info-badge vase">🏺罐子</span>`; if (isI) h += `<span class="info-badge izm">🧠${fin}</span>`; if (isSt) h += `<span class="info-badge star">⭐观星</span>`; if (hDyn) h += `<span class="info-badge dynamic">🎲动态出怪</span>`; if (isConveyor) h += `<span class="info-badge conveyor">📦 传送带</span>`; if (isChoose) h += `<span class="info-badge choose">🎯 选卡模式</span>`; if (isPreset && !isSh && !isConveyor && !isChoose) h += `<span class="info-badge preset">📋 预设卡牌</span>`; if (izS !== undefined) h += `<span class="info-badge ${izS?'shuffle-anim':'shuffle'}">🔀 植物${izS?'已打乱':'固定位置'}</span>`; h += `</div><div class="reward-block"><strong>🎁 通关奖励:</strong>${rw}</div>`; if (wi && !isS && !isI) h += `<div class="reward-block" style="background:#e8f0d0;border-color:#6b8c42;"><strong>🚩 波次:</strong>总${wi.totalWaves}波 | 旗间隔${wi.flagInterval} | 旗${wi.flagCount}面</div>`; if (l.Tutorial?.Step && Array.isArray(l.Tutorial.Step)) { h += '<div class="tutorial-box"><div class="tutorial-title">📖 教程提示</div>'; l.Tutorial.Step.forEach(s => { if (s.BroadCast?.Text) h += `<div style="margin-top:.2rem;">💬 ${esc(s.BroadCast.Text)}</div>`; }); h += '</div>'; } if (gm) h += parseGemMatch(gm, m); if (conveyorData) { const cp = conveyorData; h += '<div class="conveyor-section"><div class="section-title" style="border-bottom-color:#e67e22;">📦 传送带配置</div>'; h += `<div style="font-size:.6rem;color:#6b4f2c;">⏱ 初始间隔:${cp.Interval||0}秒 | 每${cp.IntervalIncreaseEvery||0}波缩短 | 倍率:${cp.IntervalMagnification||0}</div>`; if (cp.Type) h += `<div style="font-size:.6rem;color:#6b4f2c;">🔆 类型:${cp.Type}</div>`; if (Array.isArray(cp.Packet) && cp.Packet.length > 0) { h += '<div style="margin-top:.3rem;"><strong>🎴 传送带卡牌:</strong></div><div>'; cp.Packet.forEach(p => { if (!p) return; const n = p.Name || ''; if (!n) return; h += `<span class="plant-tag">🌱${esc(pL(n,m))}<span style="font-size:.45rem;color:#ffda7a;">[权重:${p.Weight||'?'}]</span></span>`; }); h += '</div>'; } if (Array.isArray(cp.PacketPrioritySpawnList) && cp.PacketPrioritySpawnList.length > 0) { h += '<div style="margin-top:.3rem;"><strong>⭐ 优先生成:</strong></div><div>'; cp.PacketPrioritySpawnList.forEach(n => { if (!n) return; h += `<span class="plant-tag" style="background:#8b6b2c;color:#fff;">⭐${esc(pL(n,m))}</span>`; }); h += '</div>'; } h += '</div>'; } if (isS && wm?.Survival) { const sk = wm.Survival, cfg = S.survivalConfigs.get(sk); if (cfg) { h += `<div class="reward-block" style="background:#e8f0d0;"><strong>🔄 生存:${cfg.Name||sk}</strong><br>📊 ${cfg.RoundLimit||1}轮 | 起始${cfg.PointBegin||0}分 | 每波+${cfg.PointIncrementPerWave||0}分</div>`; if (cfg.ZombiePoolSetting) { h += '<div class="section-title">🧟 生存僵尸池</div><div>'; const g = {}; if ( cfg.ZombiePoolSetting.ZombiePoolBase) g['基础池'] = cfg.ZombiePoolSetting.ZombiePoolBase.map( z => zL(z, m)); if (cfg.ZombiePoolSetting.ZombiePoolRoundAdd) cfg.ZombiePoolSetting .ZombiePoolRoundAdd.forEach(ra => { g[`第${ra.Round}轮起`] = ra.Zombie.map(z => zL(z, m)); }); for (const [r, zs] of Object.entries(g)) { h += `<div style="font-size:.6rem;color:#5a7a3a;">📌 ${r}:</div><div>`; zs.forEach(z => { h += `<span class="zombie-tag">🧟${esc(z)}</span>`; }); h += '</div>'; } h += '</div>'; } } else { h += `<div class="reward-block" style="background:#fff3cd;"><strong>⚠️ 生存模式</strong><br>未找到生存配置"${sk}"</div>`; } } if (isSh && combinedPB) { const cs = pUC(combinedPB, m); if (cs && cs.length > 0) { h += '<div class="section-title">🔫 升级线路</div>'; cs.forEach((ch, ci) => { h += `<div class="upgrade-line"><strong>线路${ci+1}:</strong>`; ch.forEach((s, i) => { if (i > 0) h += '<span class="arrow">→</span>'; const ps = []; if (s .interval != null) ps.push(`间隔${s.interval}s`); if (s.fireNum != null) ps.push( `${s.fireNum}发`); if (s.projLabel) ps.push(`子弹:${esc(s.projLabel)}`); h += `<span class="upgrade-step">${esc(s.name)}${s.cost?' ['+s.cost+']':''}${ps.length?'('+ps.join(',')+')':''}</span>`; }); h += '</div>'; }); } } let bankPackets = null, bankSourceName = ''; const arrayKeys = ['Packet', 'packet', 'Value', 'value', 'Packets', 'packets']; for (const key of arrayKeys) { if ( Array.isArray(combinedPB[key]) && combinedPB[key].length > 0) { bankPackets = combinedPB[key]; break; } } if (!bankPackets) { const bankName = combinedPB.PacketBankName || combinedPB.packetBankName || ''; if (bankName && m.packetBank.has(bankName)) { const bankRef = m.packetBank.get(bankName); bankPackets = bankRef.packets; bankSourceName = bankRef.label || bankName; } } if (!bankPackets) { const bankName = combinedPB .PacketBankName || combinedPB.packetBankName || ''; if (bankName) { for (const [key, value] of m .packetBank) { if (key.toLowerCase() === bankName.toLowerCase() || value.label.toLowerCase() === bankName.toLowerCase()) { bankPackets = value.packets; bankSourceName = value.label || key; break; } } } } if (bankPackets && bankPackets.length > 0 && !isConveyor) { const displayTitle = isChoose ? '绑定卡牌' : (isPreset ? '预设卡牌' : '卡牌列表'); const sourceInfo = bankSourceName ? ` (${esc(bankSourceName)})` : (combinedPB.PacketBankName ? ` (${esc(combinedPB.PacketBankName)})` : (combinedPB.packetBankName ? ` (${esc(combinedPB.packetBankName)})` : '')); h += `<div class="section-title">🎴 ${displayTitle}${sourceInfo}</div><div>`; bankPackets.forEach(p => { if (!p) return; const n = typeof p === 'string' ? p : (p?.PacketName || p ?.Name || p?.packetName || p?.name || ''); if (!n) return; let ex = ''; if (typeof p === 'object' && p.Override) { const pts = []; if (p.Override.Cost !== undefined && p.Override .Cost !== 0) pts.push(`费:${p.Override.Cost}`); if (p.Override.CharacterOverride?.DieEvent ?.some(e => e.EventName === 'PacketSpawn')) pts.push('💥死亡生成'); if (p.Override.Type) pts.push(cL[p.Override.Type] || p.Override.Type); if (pts.length) ex = ` <span style="font-size:.45rem;color:#ffda7a;">(${pts.join('|')})</span>`; } const dn = aL(n, m); const isPlant = m.plant.has(n); const isZombie = m.zombie.has(n) || m.zombieExtra .has(n); if (isI) h += `<span class="zombie-tag">🧟${esc(dn)}${ex}</span>`; else if (isPlant) h += `<span class="plant-tag">🌱${esc(dn)}${ex}</span>`; else if (isZombie) h += `<span class="zombie-tag">🧟${esc(dn)}${ex}</span>`; else h += `<span class="plant-tag">❓${esc(dn)}${ex}</span>`; }); h += '</div>'; } else if ((isChoose || isPreset) && !isConveyor && !bankPackets) { const bankName = combinedPB.PacketBankName || combinedPB.packetBankName || ''; if (bankName) h += `<div class="reward-block" style="background:#fff3cd;"><strong>⚠️ 卡牌引用</strong><br>PacketBankName: "${esc(bankName)}" 在options.json中未找到对应数据</div>`; } if (isSt && lookStar?.Check) { const cs = bCFL(lookStar.Check, m, pL); if (cs.size > 0) h += renderGrid( mapName, cs, 'lookstar', '⭐ 观星布局'); } if (preSpawn?.Packet?.length) { const cs = bCFL(preSpawn .Packet, m, aL); if (cs.size > 0) { if (izS) { for (const [k, cell] of cs) cell.shuffled = true; } h += renderGrid(mapName, cs, isI ? 'izm-plant' : 'preplant', isI ? `🧠 防守植物布局${izS?' (已打乱)':''}` : '📍 地图预放置', izS); } else { h += '<div class="section-title">📍 预放置(无坐标)</div><div>'; preSpawn.Packet.forEach(p => { if (p?.Name) h += `<span class="plant-tag">🌱 ${esc(aL(p.Name,m))}</span>`; }); h += '</div>'; } } if (isV && vaseManager?.Vase?.length) { const vm = vaseManager; if (vm.Shuffle) h += `<div class="info-grid"><span class="info-badge shuffle-anim">🔀 罐子已打乱</span></div>`; h += `<div class="section-title">🏺 罐子位置${vm.Shuffle?' (已打乱)':''}</div><div class="table-container"><table class="vase-table"><thead><tr><th>#</th><th>列</th><th>行</th><th>内容</th><th>类型</th></tr></thead><tbody>`; vm.Vase.forEach((v, idx) => { if (!v) return; const pos = pGP(v.GridPos); h += `<tr><td>${idx+1}</td><td>${pos?pos.col:'?'}</td><td>${pos?pos.row:'?'}</td><td>${esc(aL(v.PacketName,m))}</td><td>${getVaseTypeLabel(v.Type)}</td></tr>`; }); h += `</tbody></table></div>`; const vc = new Map(); vm.Vase.forEach(v => { if (!v) return; const pos = pGP(v.GridPos); if (pos) { const k = `${pos.col},${pos.row}`; const lb = aL(v.PacketName, m); if (vc.has(k)) { const ex = vc.get( k); ex.labels.push(lb); ex.stacked = true; } else vc.set(k, { labels: [lb], stacked: false, vaseType: v.Type || 'Normal', shuffled: !!vm.Shuffle }); } }); if (vc.size > 0) h += renderGrid(mapName, vc, 'vase-normal', `🏺 罐子布局${vm.Shuffle?' (原始位置)':''}`, vm.Shuffle); } if (!isI2 && !isI) { const zs = cAZ(l, m); if (zs.size > 0) { const fz = [], dz = []; for (const [k, v] of zs) { if (v.isDynamic) dz.push([k, v]); else fz.push([k, v]); } if (fz.length > 0) { h += '<div class="section-title">🧟 固定出怪</div><div>'; fz.sort((a, b) => b[1].count - a[1].count).forEach(([k, v]) => { h += `<span class="zombie-tag">🧟${esc(v.label)}×${v.count}</span>`; }); h += '</div>'; } if (dz.length > 0) { h += '<div class="section-title">🎲 动态随机出怪池</div><div class="zombie-pool-section">⚠️ 以下僵尸从出怪池中随机抽取</div><div>'; dz.forEach(([k, v]) => { h += `<span class="zombie-tag">🎲${esc(v.label)}</span>`; }); h += '</div>'; } } } if (isI) { const ps = cAP(l, m); if (ps.size > 0) { h += '<div class="section-title">🌱 防守植物</div><div>'; [...ps.entries()].sort((a, b) => b[1].count - a[1].count).forEach(([k, v]) => { h += `<span class="plant-tag">🌱${esc(v.label)}×${v.count}</span>`; }); h += '</div>'; } if (isI2 && packetBank?.Value?.length) { h += '<div class="section-title">🧟 进攻僵尸</div><div>'; packetBank.Value.forEach(p => { const n = typeof p === 'string' ? p : (p?.PacketName || ''); if (n) h += `<span class="zombie-tag">🧟${esc(zL(n,m))}</span>`; }); h += '</div>'; } } if (isV && vaseManager?.Vase) { const vp = new Map(), vz = new Map(); vaseManager.Vase.forEach(v => { if (!v?.PacketName) return; const n = v.PacketName; if (m.plant.has( n)) { const e = vp.get(n); if (e) e.count++; else vp.set(n, { label: pL(n, m), count: 1 }); } else if (m.zombie.has(n) || m.zombieExtra .has(n)) { const e = vz.get(n); if (e) e.count++; else vz.set(n, { label: zL(n, m), count: 1 }); } }); if (vp.size > 0) { h += '<div class="section-title">🏺 罐中植物</div><div>'; [...vp.entries()].sort((a, b) => b[1].count - a[1].count).forEach(([k, v]) => { h += `<span class="plant-tag">🌱${esc(v.label)}×${v.count}</span>`; }); h += '</div>'; } if (vz.size > 0) { h += '<div class="section-title">🏺 罐中僵尸</div><div>'; [...vz.entries()].sort((a, b) => b[1].count - a[1].count).forEach(([k, v]) => { h += `<span class="zombie-tag">🧟${esc(v.label)}×${v.count}</span>`; }); h += '</div>'; } } const aes = cAED(l); if (aes.length > 0) { h += `<div class="section-title">⚡ 整局全部事件<span class="event-count">${aes.length}个</span></div>`; const g = {}; aes.forEach(ev => { const d = eDD(ev, m); if (!g[d]) g[d] = { count: 0, sources: new Set() }; g[d].count++; if (ev._source) g[d].sources.add(ev._source); }); for (const [d, i] of Object.entries( g)) h += `<div class="event-block"><div class="event-header">⚡${esc(d)}${i.count>1?`<span class="event-count">×${i.count}</span>`:''}</div>${i.sources.size>0?`<div class="event-detail">📍触发:${[...i.sources].join('、')}</div>`:''}</div>`; } return h; } $('analyzeBtn')?.addEventListener('click', () => { if (S.isAnalyzing) return; if (!S.optionsData || !S.templatesData || !S.levelData) { toast('请先上传所有必需文件', 'error', true); return; } S.isAnalyzing = true; const ab = $('analyzeBtn'); if (ab) { ab.disabled = true; ab.innerHTML = '<span class="loading-spinner"></span>解析中...'; } setTimeout(() => { try { S.lastMaps = bM(); const hc = pLvl(S.levelData, S.lastMaps); $('output').innerHTML = hc + '<div style="color:#5a7a3a;margin-top:.4rem;">✅ 解析完成</div>'; $('output').scrollTop = 0; toast('解析完成', 'success'); } catch (e) { console.error('解析错误:', e); $('output').innerHTML = '<span style="color:#c44;">❌ 解析失败:' + esc(e.message || '未知错误') + '</span>'; toast('解析失败: ' + (e.message || '未知错误'), 'error', true); } finally { S.isAnalyzing = false; if (ab) ab.innerHTML = '🔍 解析关卡'; updateUI(); } }, 50); }); $('exportBtn')?.addEventListener('click', () => { if (!S.lastMaps || !S.levelData) { toast('请先解析关卡', 'error'); return; } try { const hc = pLvl(S.levelData, S.lastMaps); const tempDiv = document.createElement('div'); tempDiv.innerHTML = hc; let text = ''; const walker = document.createTreeWalker(tempDiv, NodeFilter .SHOW_TEXT, null, false); let node; const lines = []; let currentLine = ''; while (node = walker .nextNode()) { const t = node.textContent.trim(); if (t) { if (node.parentElement?.classList ?.contains('section-title') || node.parentElement?.classList?.contains('event-header') || node .parentElement?.tagName === 'STRONG') { if (currentLine) lines.push(currentLine); lines.push(''); lines.push(t); currentLine = ''; } else { currentLine += (currentLine ? ' | ' : '') + t; } } } if (currentLine) lines.push(currentLine); text = lines.join('\n'); if (!text.trim()) { toast('无内容可导出', 'error'); return; } const blob = new Blob( ['\uFEFF' + text], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; const fileName = ($('exportFileName').value.trim() || (S.levelData.LevelName || S.levelData .Name || '关卡分析')) + '.csv'; link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); setTimeout(() => URL.revokeObjectURL(url), 1000); toast('导出成功:' + fileName, 'success'); } catch (e) { console.error('导出错误:', e); toast('导出失败:' + e.message, 'error', true); } }); document.addEventListener('keydown', function(e) { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return; const ab = $('analyzeBtn'); if (e.ctrlKey && e.key === 'Enter' && ab && !ab.disabled) { e.preventDefault(); ab.click(); } if (e.ctrlKey && e.key === 's' && !e.shiftKey) { const eb = $('exportBtn'); if (eb && !eb .disabled) { e.preventDefault(); eb.click(); } } }); function init() { updateUI(); setupFileInput($('optInput'), (dt) => { S.optionsData = dt; }, $('optName')); setupFileInput($('tplInput'), (dt) => { S.templatesData = dt; }, $('tplName')); setupFileInput($('levelInput'), (dt) => { S.levelData = dt; }, $('levelFileName')); setupSurvivalInput($('surInput')); if (!S.optionsData || !S.templatesData) { if (loadCache()) { $('optName') .textContent = S.optionsData ? '✅ 缓存' : '未选择'; $('tplName').textContent = S.templatesData ? '✅ 缓存' : '未选择'; $('surName').textContent = S.survivalConfigs.size > 0 ? '✅ ' + S.survivalConfigs.size + '个' : '未选择'; updateUI(); toast('已从缓存恢复数据', 'success'); } } window.addEventListener('beforeunload', () => { if (S.optionsData || S.templatesData) saveCache(); }); console.log('🌻 PVZ HE Pro 7.1 已就绪'); console.log('💡 Ctrl+Enter 解析 | Ctrl+S 导出CSV'); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); })(); </script> </body> </html> </div> </includeonly>
返回
微件:LevelAnalyzer
。
查看“︁微件:LevelAnalyzer”︁的源代码
来自植物大战僵尸杂交版Wiki