微件:LevelAnalyzer:修订间差异
来自植物大战僵尸杂交版Wiki
更多操作
创建页面,内容为“<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 6.3</title> <style> :root { --bg-dark: #1a2c1e; --wood: #6b4f2c; --gold: #d4a745; --light-bg: #f9f3d9; --card-bg: #fffbe6; --green-btn: #5f7a36;…” |
无编辑摘要 |
||
| 第4行: | 第4行: | ||
<html lang="zh-CN"> | <html lang="zh-CN"> | ||
<head> | <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> | </head> | ||
<body> | <body> | ||
<div class="container"> | <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"> | |||
</div> | <h3><span class="status-dot missing" id="tplDot"></span> templates.json</h3> | ||
<div class="toast" id="toast"></div> | <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> | <script> | ||
(function(){'use strict'; | (function() { | ||
const $=id=>document.getElementById(id); | 'use strict'; | ||
const S={optionsData:null,templatesData:null,levelData:null,survivalConfigs:new Map(),lastMaps:null,isAnalyzing:false,MAX_FILE_SIZE: | const $ = id => document.getElementById(id); | ||
let | 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 | 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 | 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 | function saveCache() { | ||
try { | |||
const | 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 = { | |||
if( | 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(Array.isArray( | 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) { | |||
if( | 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( | 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( | 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(! | if (!S.optionsData?.map) return { cols: 9, rows: 5 }; | ||
if( | 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 | const m = { plant: new Map(), zombie: new Map(), zombieExtra: new Map(), item: new Map(), map: new Map(), | ||
return | 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; | |||
function | if (!wm?.Wave || !Array.isArray(wm.Wave) || wm.Wave.length === 0) return null; | ||
const tw = wm.Wave.length, | |||
</script> | 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> | </body> | ||
</html> | </html> | ||
</div> | </div> | ||
</includeonly> | </includeonly> | ||