打开/关闭菜单
933
1525
112
1.4万
植物大战僵尸杂交版Wiki
打开/关闭外观设置菜单
打开/关闭个人菜单
未登录
未登录用户的IP地址会在进行任意编辑后公开展示。

微件: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 charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes, viewport-fit=cover, maximum-scale=5.0">
    <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>
    <title>🌻 植物大战僵尸杂交版 · 关卡分析器 Pro 7.1</title>
  <style>
    <style>
    :root { --bg-dark: #1a2c1e; --wood: #6b4f2c; --gold: #d4a745; --light-bg: #f9f3d9; --card-bg: #fffbe6; --green-btn: #5f7a36; --blue-btn: #2c5aa0; --danger: #c44; }
        :root {
    * { margin: 0; padding: 0; box-sizing: border-box; }
            --bg-dark: #1a2c1e;
    html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; -webkit-tap-highlight-color: transparent; }
            --wood: #6b4f2c;
    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; }
            --gold: #d4a745;
    .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; }
            --light-bg: #f9f3d9;
    .header { text-align: center; margin-bottom: 0.8rem; }
            --card-bg: #fffbe6;
    h1 { font-size: 1.4rem; font-weight: 900; color: #3e2b15; text-shadow: 2px 2px 0 var(--gold); }
            --green-btn: #5f7a36;
    h1 .badge { background: #5c3b1a; color: #ffda7a; font-size: 0.7rem; padding: 0.2rem 0.9rem; border-radius: 2rem; }
            --blue-btn: #2c5aa0;
    .drop-zone { background: rgba(218,194,130,0.4); border: 3px dashed #8b6b3c; border-radius: 1.2rem; padding: 1rem; margin-bottom: 0.8rem; text-align: center; cursor: pointer; min-height: 80px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.3rem; }
            --danger: #c44
    .drop-zone.drag-over { background: rgba(180,210,120,0.5); border-color: #5f7a36; border-style: solid; }
        }
    .drop-zone-icon { font-size: 2rem; }
        * {
    .file-zone { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.5rem; margin-bottom: 0.8rem; }
            margin: 0;
    .file-card { background: var(--card-bg); border-radius: 0.7rem; padding: 0.5rem; box-shadow: 0 4px 12px rgba(0,0,0,0.15); border: 2px solid #bb9740; text-align: center; }
            padding: 0;
    .file-card:hover { border-color: #d4a745; }
            box-sizing: border-box
    .file-card h3 { font-size: 0.7rem; color: #3d2c15; margin-bottom: 0.2rem; display: flex; align-items: center; justify-content: center; gap: 0.2rem; }
        }
    .file-card input[type="file"] { display: none; }
        html {
    .file-label { display: inline-block; background: #e0ba75; padding: 0.3rem 0.6rem; border-radius: 0.8rem; font-weight: 700; cursor: pointer; font-size: 0.65rem; border: 2px solid #b8872e; width: 100%; max-width: 130px; text-align: center; }
            -webkit-text-size-adjust: 100%;
    .file-label:hover { background: #cfaa54; }
            text-size-adjust: 100%;
    .file-label.loaded { background: #7cb342; color: #fff; border-color: #4a7a1a; }
            -webkit-tap-highlight-color: transparent
    .status-dot { display: inline-block; width: 0.5rem; height: 0.5rem; border-radius: 50%; flex-shrink: 0; }
        }
    .status-dot.ready { background: #3cb043; box-shadow: 0 0 6px #3cb043; }
        body {
    .status-dot.missing { background: #ccc; }
            background: var(--bg-dark);
    .file-name-display { font-size: 0.55rem; color: #6b4f2c; margin-top: 0.2rem; word-break: break-all; max-height: 2.5em; overflow-y: auto; }
            font-family: 'Segoe UI', 'Roboto', 'Noto Sans SC', system-ui, sans-serif;
    .btn { background: var(--green-btn); border: none; color: white; font-weight: 700; padding: 0.4rem 0.8rem; border-radius: 1.5rem; font-size: 0.7rem; cursor: pointer; box-shadow: 0 3px 6px rgba(0,0,0,0.25); border-bottom: 3px solid #334d1a; display: inline-flex; align-items: center; gap: 0.3rem; white-space: nowrap; }
            display: flex;
    .btn:disabled { background: #a5a5a5; cursor: not-allowed; opacity: 0.6; }
            justify-content: center;
    .btn:hover:not(:disabled) { background: #719541; }
            align-items: flex-start;
    .btn-export { background: var(--blue-btn); border-bottom-color: #1a3a6a; }
            min-height: 100vh;
    .btn-cache { background: #6b4f2c; font-size: 0.6rem; }
            padding: 1rem
    .level-input-area { background: rgba(228,209,155,0.7); border-radius: 0.7rem; padding: 0.5rem; margin: 0.5rem 0; display: flex; align-items: center; flex-wrap: wrap; gap: 0.4rem; border: 2px solid #b18530; }
        }
    .export-name-input { padding: 0.3rem 0.5rem; border-radius: 1rem; border: 2px solid #b8872e; font-size: 0.7rem; background: var(--card-bg); width: 160px; outline: none; }
        .container {
    .result-panel { background: #fef9e8; border: 3px solid #7c6033; border-radius: 0.7rem; padding: 0.6rem; min-height: 200px; max-height: 500px; overflow-y: auto; font-size: 0.7rem; scroll-behavior: smooth; }
            max-width: 1400px;
    .info-grid { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-bottom: 0.4rem; }
            width: 100%;
    .info-badge { background: #c6a45b; padding: 0.15rem 0.5rem; border-radius: 0.6rem; font-weight: 700; font-size: 0.65rem; white-space: nowrap; }
            background: linear-gradient(165deg, #f9f3d9, #e8d9a0, #dcc880);
    .info-badge.survival{background:#5b8c3e;color:#fff}.info-badge.endless{background:#9b4a2e;color:#fff;animation:pulse-glow 2s infinite}
            border: 4px solid var(--wood);
    .info-badge.shooting{background:#c44;color:#fff}.info-badge.vase{background:#6a4c1a;color:#fff}.info-badge.izm{background:#3a3060;color:#fff}
            border-radius: 1.5rem;
    .info-badge.star{background:#d4a017;color:#1a1a1a}.info-badge.shuffle-anim{background:#c44;color:#fff;animation:pulse 1.5s infinite}
            box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
    .info-badge.dynamic{background:#6b3a8a;color:#fff}.info-badge.shuffle{background:#8b3a6a;color:#fff}.info-badge.gem{background:#e04090;color:#fff}
            padding: 1rem
    .info-badge.conveyor{background:#e67e22;color:#fff}
        }
    @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.6}}@keyframes pulse-glow{0%,100%{box-shadow:0 0 5px #9b4a2e}50%{box-shadow:0 0 15px #ff6b3a}}
        .header {
    .zombie-tag,.plant-tag{display:inline-block;padding:0.12rem 0.4rem;border-radius:0.4rem;margin:0.08rem;font-size:0.6rem;word-break:break-all;max-width:100%}
            text-align: center;
    .zombie-tag{background:#2e3b1f;color:#f5e7c6}.plant-tag{background:#2c5c2c;color:#f2f2c0}
            margin-bottom: .8rem
    .reward-block{background:#faf0c8;border:2px solid #d4a017;border-radius:0.5rem;padding:0.3rem;margin:0.25rem 0;font-size:0.65rem}
        }
    .event-block{background:#f5e8c9;border:1px solid #c9a24b;border-radius:0.5rem;padding:0.3rem;margin:0.25rem 0;font-size:0.65rem}
        h1 {
    .event-block .event-header{font-weight:700;color:#6b3a1a;display:flex;align-items:baseline;gap:0.2rem;flex-wrap:wrap}
            font-size: 1.4rem;
    .event-block .event-detail{color:#4a3020;font-size:0.6rem;margin-top:0.08rem}
            font-weight: 900;
    .section-title{font-weight:700;font-size:0.75rem;margin-top:0.35rem;color:#3d2e12;border-bottom:2px solid #c9a24b;padding-bottom:0.12rem;display:flex;align-items:center;gap:0.25rem}
            color: #3e2b15;
    .event-count{background:#8b5a2b;color:#fff;border-radius:0.3rem;padding:0.04rem 0.3rem;font-size:0.55rem;flex-shrink:0}
            text-shadow: 2px 2px 0 var(--gold)
    .grid-container{overflow-x:auto;-webkit-overflow-scrolling:touch;max-width:100%}
        }
    .grid-row{display:flex}
        h1 .badge {
    .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:0.2rem;margin:1px;font-size:clamp(0.3rem,0.7vw,0.45rem);font-weight:bold;border:1px solid #8b7a2b;color:#3d2e12;overflow:hidden;cursor:default;position:relative;transition:transform 0.15s;flex-shrink:0}
            background: #5c3b1a;
    .grid-cell:hover{transform:scale(1.12);z-index:10}
            color: #ffda7a;
    .grid-cell.preplant{background:#5a8a3c;color:#fff}.grid-cell.preplant-stacked{background:#3a6a2c;color:#fff}
            font-size: .7rem;
    .grid-cell.vase-plant{background:#4a9a3c;color:#fff}.grid-cell.vase-zombie{background:#8b3a3a;color:#fff}.grid-cell.vase-normal{background:#b8863a;color:#fff}
            padding: .2rem .9rem;
    .grid-cell.lookstar{background:#d4a017;color:#1a1a1a}.grid-cell.izm-plant{background:#5c3a8a;color:#e8d9ff}.grid-cell.izm-plant-stacked{background:#3c1a6a;color:#e8d9ff}
            border-radius: 2rem
    .grid-cell .shuffle-marker{position:absolute;top:0;right:1px;font-size:0.3rem;color:#ff0;text-shadow:0 0 3px #000;font-weight:bold;line-height:1;pointer-events:none}
        }
    .grid-legend{font-size:clamp(0.35rem,0.8vw,0.5rem);margin:0.08rem 0;color:#6b4f2c}
        .drop-zone {
    .col-header,.row-label{text-align:center;color:#5a432b}.col-header{width:clamp(1.1rem,2.8vw,1.8rem);font-size:clamp(0.3rem,0.7vw,0.45rem);flex-shrink:0}
            background: rgba(218, 194, 130, .4);
    .row-label{width:0.7rem;font-size:clamp(0.3rem,0.7vw,0.45rem);line-height:clamp(1.1rem,2.8vw,1.8rem);flex-shrink:0}
            border: 3px dashed #8b6b3c;
    .grid-col-headers{display:flex;margin-left:0.7rem}
            border-radius: 1.2rem;
    .upgrade-line{background:#f0e2b0;border:2px solid #c9a24b;border-radius:0.5rem;padding:0.3rem;margin:0.25rem 0;font-size:0.65rem}
            padding: 1rem;
    .gem-upgrade-line{background:#f8e0f0;border:2px solid #e04090}
            margin-bottom: .8rem;
    .upgrade-step{display:inline-block;background:#5c3b1a;color:#ffda7a;padding:0.08rem 0.35rem;border-radius:0.35rem;margin:0.06rem;font-size:0.6rem}
            text-align: center;
    .arrow{color:#8b5a2b;font-weight:bold;margin:0 0.08rem}
            cursor: pointer;
    .toast{position:fixed;top:20px;right:20px;background:#333;color:#fff;padding:0.5rem 1rem;border-radius:0.7rem;font-size:0.75rem;z-index:9999;opacity:0;transform:translateX(100%);transition:all 0.3s ease;pointer-events:none;box-shadow:0 4px 12px rgba(0,0,0,0.3);max-width:90vw}
            min-height: 80px;
    .toast.show{opacity:1;transform:translateX(0)}.toast.success{background:#5f7a36}.toast.error{background:#c44}
            display: flex;
    .zombie-pool-section{background:#f0e6d0;border:1px dashed #8b6b3c;border-radius:0.4rem;padding:0.25rem 0.4rem;margin:0.15rem 0;font-size:0.6rem;color:#5a3d1a}
            flex-direction: column;
    .table-container{overflow-x:auto;max-width:100%}
            align-items: center;
    .vase-table{width:100%;border-collapse:collapse;font-size:0.6rem;margin:0.25rem 0;min-width:400px}
            justify-content: center;
    .vase-table th{background:#8b6b2c;color:#fff;padding:0.15rem;border:1px solid #5a3d1a;font-size:0.55rem;position:sticky;top:0;z-index:5}
            gap: .3rem;
    .vase-table td{padding:0.12rem;border:1px solid #b8943a;text-align:center;word-break:break-word}
            transition: all 0.3s ease
    .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:0.5rem;padding:0.4rem;margin:0.3rem 0;font-size:0.65rem}
        .drop-zone.drag-over {
    .tutorial-box .tutorial-title{font-weight:700;color:#2471a3}
            background: rgba(180, 210, 120, .5);
    .conveyor-section{background:#fdf2e9;border:2px solid #e67e22;border-radius:0.5rem;padding:0.4rem;margin:0.25rem 0;font-size:0.65rem}
            border-color: #5f7a36;
    .loading-spinner{display:inline-block;width:0.9rem;height:0.9rem;border:2px solid rgba(255,255,255,0.3);border-top-color:#fff;border-radius:50%;animation:spin 0.6s linear infinite;vertical-align:middle;margin-right:0.25rem}
            border-style: solid;
    @keyframes spin{to{transform:rotate(360deg)}}
            transform: scale(1.02)
    @media(max-width:768px){body{padding:0.3rem}.container{border-radius:1rem;border-width:3px}.file-zone{grid-template-columns:repeat(auto-fit,minmax(110px,1fr));gap:0.3rem}.level-input-area{justify-content:center}.grid-cell{width:1rem;height:1rem;line-height:1rem;font-size:0.28rem}.col-header{width:1rem;font-size:0.28rem}.toast{top:auto;bottom:15px;right:8px;left:8px;max-width:none;transform:translateY(100%)}.toast.show{transform:translateY(0)}}
        }
  </style>
        .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 6.3</span></h1><p style="font-size:0.7rem;color:#5a432b;">⚡ 传送带显示 · 宝石迷阵 · IZM打乱 · 完整事件</p></div>
        <div class="header">
  <div class="drop-zone" id="dropZone"><div class="drop-zone-icon">📂</div><div class="drop-zone-text">拖拽文件到此处上传</div><div style="font-size:0.6rem;color:#7a5c33;">options.json / templates.json / 关卡.json / 生存配置.json</div></div>
            <h1>🌻 杂交版关卡分析器 <span class="badge">PVZ HE Pro 7.1</span></h1>
  <div class="file-zone">
            <p style="font-size:.7rem;color:#5a432b;">⚡ 修复射击误判 · 精确模式识别 · PacketBank完善</p>
    <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>
    <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="drop-zone" id="dropZone">
    <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="drop-zone-icon">📂</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 class="drop-zone-text">拖拽文件到此处上传</div>
  </div>
            <div style="font-size:.6rem;color:#7a5c33;">options.json / templates.json / 关卡.json / 生存配置.json</div>
  <div class="level-input-area">
        </div>
    <button class="btn btn-cache" id="cacheBtn">📦 缓存加载</button>
        <div class="file-zone">
    <button class="btn" id="analyzeBtn" disabled>🔍 解析关卡</button>
            <div class="file-card">
    <span style="font-weight:600;font-size:0.65rem;">📥 导出名:</span>
                <h3><span class="status-dot missing" id="optDot"></span> options.json</h3>
    <input type="text" class="export-name-input" id="exportFileName" placeholder="自定义文件名" value="" spellcheck="false">
                <label class="file-label" for="optInput" id="optLabel">📁 选择文件</label>
    <button class="btn btn-export" id="exportBtn" disabled>📥 导出CSV</button>
                <input type="file" id="optInput" accept=".json">
    <button class="btn" id="resetBtn" style="background:var(--danger);border-bottom-color:#8b1a1a;">🔄 重置</button>
                <div class="file-name-display" id="optName">未选择</div>
  </div>
            </div>
  <div id="output" class="result-panel">🌿 请上传 options.json 和 templates.json,然后选择或拖入关卡文件并点击解析。<br><span style="font-size:0.6rem;color:#8b6b3c;">💡 可直接拖拽JSON文件到上方区域 | 📦 点击"缓存加载"恢复上次文件</span></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:50*1024*1024,TIMEOUT:15000,CACHE_KEY:'pvz_cache_v14'};
            const $ = id => document.getElementById(id);
let tt=null,_lm='';
            const S = {
function toast(m,t){if(!m||m===_lm)return;_lm=m;const el=$('toast');if(tt)clearTimeout(tt);el.textContent=m;el.className='toast '+(t||'')+' show';tt=setTimeout(()=>{el.classList.remove('show');tt=null;_lm='';},2500);}
                optionsData: null,
function saveCache(){try{const c={opt:S.optionsData,tpl:S.templatesData,surv:Array.from(S.survivalConfigs.entries())};const j=JSON.stringify(c);if(j.length<6*1024*1024)localStorage.setItem(S.CACHE_KEY,j);}catch(e){}}
                templatesData: null,
function loadCache(){try{const s=localStorage.getItem(S.CACHE_KEY);if(!s)return false;const c=JSON.parse(s);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;}}
                levelData: null,
const dZE=$('dropZone');function pd(e){e.preventDefault();e.stopPropagation();}
                survivalConfigs: new Map(),
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);}
                lastMaps: null,
async function hD(e){const dt=e.dataTransfer;if(!dt?.files||!dt.files.length){toast('请拖入JSON文件','error');return;}const files=[...dt.files].filter(f=>(f.name||'').toLowerCase().endsWith('.json'));if(!files.length){toast('仅支持.json','error');return;}let lc=0;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();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',''),data);$('surName').textContent='✅ '+S.survivalConfigs.size+'个';lc++;}else{S.levelData=data;$('levelFileName').textContent='✅ '+file.name;lc++;}}});if(lc>0){updateUI();saveCache();$('output').innerHTML='✅ 成功加载 '+lc+' 个文件';}}
                isAnalyzing: false,
document.body.addEventListener('dragover',pd,false);
                MAX_FILE_SIZE: 100 * 1024 * 1024,
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('文件过大'));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格式错误'));}if(data===null||typeof data!=='object')return reject(new Error('不是有效JSON'));resolve(data);}catch(err){reject(new Error('处理失败'));}};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('无法读取'));}});}
                TIMEOUT: 30000,
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);}
                CACHE_KEY: 'pvz_cache_v21'
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')){toast('仅支持.json','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');nameSpan.textContent='加载失败';updateUI();}).finally(()=>{inputEl.value='';});});}
            };
function setupSurvivalInput(inputEl){if(!inputEl)return;inputEl.addEventListener('change',async function(){const files=[...inputEl.files].filter(f=>f.name.endsWith('.json'));for(const file of files){try{const data=await rF(file);S.survivalConfigs.set(file.name.replace('.json',''),data);}catch(err){toast('❌ '+file.name+': '+err.message,'error');}}if(S.survivalConfigs.size>0){$('surName').textContent='✅ '+S.survivalConfigs.size+'个';}updateUI();saveCache();inputEl.value='';});}
            let toastTimer = null;
$('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');});
            let lastToastMsg = '';
$('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();}});
            let toastQueue = [];


function esc(t){if(t==null)return'';return String(t).replace(/[&<>"]/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'})[m]);}
            function toast(m, t, force = false) {
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');const c=parseInt(p[0],10),r=parseInt(p[1],10);return{cols:(!isNaN(c)&&c>0&&c<=20)?c:9,rows:(!isNaN(r)&&r>0&&r<=10)?r:5};}return{cols:9,rows:5};}
                if (!m) return;
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};}
                if (!force && m === lastToastMsg) return;
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()};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||String(i.value));});});sa(S.optionsData.map).forEach(mp=>{if(mp&&mp.value!=null)m.map.set(String(mp.value),{label:mp.label||String(mp.value),size:mp.size||'9x5'});});return m;}
                toastQueue.push({ m, t, force });
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:1};return{totalWaves:tw,flagInterval:fi,flagCount:Math.floor((tw-1)/fi)+1};}
                if (toastQueue.length > 3) toastQueue.shift();
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;}
                processToastQueue();
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':'清除所有对象'};
const sTL={'Default':'正常冒出','Bungi':'蹦极放置'};
const cL={'White':'白卡','Gold':'金卡','Diamond':'钻卡','Colour':'彩卡','Star':'星卡','Original':'原卡+紫卡'};
function eDD(ev,m){if(!ev||typeof ev!=='object')return'未知事件';const n=ev.EventName||'';const v=ev.Value||{};switch(n){case'GravestoneCreateRandom':{const ns=Array.isArray(v.GravestoneNames)?v.GravestoneNames.map(x=>aL(x,m)).join('、'):'?';return`随机生成物体:${ns} ×${v.GravestoneNum||'?'}`;}case'GravestoneSpawnZombie':{const ns=Array.isArray(v.ZombieNames)?v.ZombieNames.map(x=>zL(x,m)).join('、'):'?';return`墓碑生成僵尸:${ns} ×${v.ZombieNum||'?'}`;}case'GridSpawnZombie':{const ns=Array.isArray(v.ZombieNames)?v.ZombieNames.map(x=>zL(x,m)).join('、'):'?';let d=`图格生成僵尸:${ns} ×${v.ZombieNum||'?'}`;if(v.SpawnPos)d+=`,区域:[${v.SpawnPos.x??'?'},${v.SpawnPos.y??'?'}]~[${v.SpawnPos.z??'?'},${v.SpawnPos.w??'?'}]`;if(v.SpawnType)d+=`,${sTL[v.SpawnType]||v.SpawnType}`;return d;}case'BungiSpawnZombie':{const ns=Array.isArray(v.ZombieNames)?v.ZombieNames.map(x=>zL(x,m)).join('、'):'?';let d=`蹦极放置僵尸:${ns} ×${v.ZombieNum||'?'}`;if(v.hypnoses)d+=',魅惑';return d;}case'CurrentMapUseWarningLine':return`设置警戒线:第${v.Row??'?'}列`;case'CurrentMapUseStripe':return`限制种植列(红线分隔),左侧列数:${v.Row??'?'}`;case'CreateProtal':return`创建传送门${v.Shape?',样式:'+esc(String(v.Shape)):''}`;case'TipsPlay':{const t=v.Text;const d=v.Duration;let ds='显示提示';if(t!==undefined&&t!==null&&String(t).trim()!=='')ds+=`:"${esc(String(t))}"`;if(d)ds+=`,持续${d}秒`;return ds;}case'SunCreate':{let d=`生成阳光 ×${v.Num||'?'}`;if(v.FromPacket)d+=`(继承卡牌×${v.FromPacketPercentage||1})`;if(v.ByCamp)d+=',跟随阵营';return d;}case'PacketCreate':{let d='创建角色卡牌';if(v.PacketName)d+=`:${aL(v.PacketName,m)}`;else if(v.PacketBankName){d+=`:从选卡范围[${v.PacketBankName}]随机`;if(v.CategoryName)d+=`,品质:${cL[v.CategoryName]||v.CategoryName}`;}if(v.AliveTime)d+=`,存在${v.AliveTime}秒`;return d;}case'PacketSpawn':{let d='生成角色';if(v.PacketName)d+=`:${aL(v.PacketName,m)}`;if(v.Percentage!=null&&v.Percentage<1)d+=`,概率${Math.round(v.Percentage*100)}%`;if(v.ByCamp)d+=',跟随阵营';return d;}case'SetGameMode':return`改变模式:${v.GameMode||'?'}`;case'CurrentMapCharacterClear':return'清除场景所有对象';case'CurrentMapFunctionExecute':{const fn=v.FunctionName||'';const fl=mFL[fn]||(fn||'未知行为');if(fn==='UseStripe'){const cn=Array.isArray(v.Value)?v.Value[0]:(v.Value??'?');return`地图行为:${fl},列数:${cn??'?'}`;}if(fn==='LineUseSet'){const rn=Array.isArray(v.Value)?v.Value[0]:'?';const en=Array.isArray(v.Value)&&v.Value.length>1?v.Value[1]:true;return`地图行为:${fl},行${rn}${en?'允许出怪':'禁止出怪'}`;}return`地图行为:${fl}`;}case'MapChange':{const mn=m.map.get(v.MapName)?.label||v.MapName||'?';return`切换地图:${mn}`;}case'AddPacket':return`传送带增加卡牌:${aL(v.Name,m)},权重${v.Weight||'?'}`;case'ChangePacket':{const pn=v.PacketName||'';return`改变卡牌属性:${pL(pn,m)||zL(pn,m)||pn||'?'}`;}case'Delete':return'删除卡牌';case'CraterCreate':return'创建弹坑';case'ChangeProtalPos':return'改变传送门位置';case'CheckCharacterNum':{const cn=v.CharacterName||'?';const mt=v.Method||'=';const nm=v.Num??'?';const oM={'=':'等于','>':'大于','<':'小于','>=':'大于等于','<=':'小于等于'};return`场上${aL(cn,m)}数量${oM[mt]||mt}${nm}`;}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,'开局');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 processToastQueue() {
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&&(ev.EventName==='BungiSpawnZombie'||ev.EventName==='GravestoneSpawnZombie'||ev.EventName==='GridSpawnZombie')&&Array.isArray(ev.Value?.ZombieNames))ev.Value.ZombieNames.forEach(z=>aZ(z,ev.Value?.ZombieNum||1));if(ev&&ev.EventName==='SpawnZombie'&&ev.Value?.ZombieName)aZ(ev.Value.ZombieName,ev.Value.ZombieNum||1);if(ev&&ev.EventName==='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;}
                if (toastQueue.length === 0) return;
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;}
                if (toastTimer) return;
function pUC(pb,m){if(!Array.isArray(pb?.Value)||pb.Value.length===0)return null;const cs=[];const MD=20;pb.Value.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;let cn=cur;let 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;}
                const { m, t } = toastQueue.shift();
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;}
                lastToastMsg = m;
function getVaseTypeClass(type){return type==='Plant'?'vase-plant':type==='Zombie'?'vase-zombie':'vase-normal';}
                const el = $('toast');
function getVaseTypeLabel(type){return type==='Plant'?'植物罐':type==='Zombie'?'僵尸罐':'普通罐';}
                if (!el) return;
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)}`;if(shuffled||cell.shuffled)h+=`<span class="shuffle-marker">↻</span>`;h+=`</span>`;}else{h+=`<span class="grid-cell">·</span>`;}}h+=`</div>`;}h+=`</div>`;return h;}
                el.textContent = m;
function parseGemMatch(gm,m){if(!gm?.Data)return'';const d=gm.Data;let h='';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;}
                el.className = 'toast ' + (t || '') + ' show';
                toastTimer = setTimeout(() => {
                    el.classList.remove('show');
                    toastTimer = null;
                    setTimeout(processToastQueue, 300);
                }, 2500);
            }


function pLvl(l,m){
            function saveCache() {
if(!l||!m)return'<span style="color:#c44;">❌ 无效数据</span>';
                try {
const isNF=!!l.Feature;let wm,mapName,bgmName,sunData,packetBank,reward,vaseManager,preSpawn,lookStar,finishMethod,gm=null;
                    const c = {
if(isNF){const wf=l.Feature.find(f=>f.Name==='Wave');const mf=l.Feature.find(f=>f.Name==='Map');const bf=l.Feature.find(f=>f.Name==='BGM');const sf=l.Feature.find(f=>f.Name==='Sun');const sef=l.Feature.find(f=>f.Name==='SeedBank');const pf=l.Feature.find(f=>f.Name==='Process');gm=l.Feature.find(f=>f.Name==='GemMatch');wm=wf?.Data;mapName=mf?.Data?.MapName;bgmName=bf?.Data?.BGMName;sunData=sf?.Data;packetBank=sef?.Data;finishMethod=pf?.Data?.Name||'Wave';reward=l.Reward;vaseManager=l.VaseManager;preSpawn=l.PreSpawn;lookStar=l.LookStarManager;}else{wm=l.WaveManager;mapName=l.Map;bgmName=l.BGM;sunData=l.SunManager;packetBank=l.PacketBank;finishMethod=l.FinishMethod||'Wave';reward=l.Reward;vaseManager=l.VaseManager;preSpawn=l.PreSpawn;lookStar=l.LookStarManager;}
                        opt: S.optionsData,
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 isSh=!isS&&!isV&&!isI&&packetBank?.Method==='Preset'&&Array.isArray(wm?.Wave)&&wm.Wave.length>0;const isSt=!!lookStar?.Open;const izS=l.IZMManager?.Shuffle;const isConveyor=packetBank?.Method==='Conveyor';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);
                        tpl: S.templatesData,
const{rows,cols}=gMS(mapName);const ml=m.map.get(mapName)?.label||mapName||'未知地图';const rw=pR(reward,m);const wi=gWI(l);
                        surv: Array.from(S.survivalConfigs.entries()),
let h=`<div style="font-weight:bold;font-size:1rem;">📋 ${esc(l.LevelName||l.Name||'未命名')}</div><div class="info-grid">`;
                        timestamp: Date.now()
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>`;
                    const j = JSON.stringify(c);
if(isS)h+=`<span class="info-badge survival">🔄生存${isE?'(无尽)':'(基础)'}</span>`;if(isE)h+=`<span class="info-badge endless">♾️无尽</span>`;
                    if (j.length > 8 * 1024 * 1024) {
if(isSh)h+=`<span class="info-badge shooting">🔫射击</span>`;if(isV)h+=`<span class="info-badge vase">🏺罐子</span>`;
                        const compressed = {
if(isI)h+=`<span class="info-badge izm">🧠${fin}</span>`;if(isSt)h+=`<span class="info-badge star">⭐观星</span>`;
                            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,
if(hDyn)h+=`<span class="info-badge dynamic">🎲动态出怪</span>`;if(isConveyor)h+=`<span class="info-badge conveyor">📦 传送带</span>`;
                            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,
if(izS!==undefined)h+=`<span class="info-badge ${izS?'shuffle-anim':'shuffle'}">🔀 植物${izS?'已打乱':'固定位置'}</span>`;
                            surv: Array.from(S.survivalConfigs.entries()),
h+=`</div><div class="reward-block"><strong>🎁 通关奖励:</strong>${rw}</div>`;
                            timestamp: Date.now()
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:0.2rem;">💬 ${esc(s.BroadCast.Text)}</div>`;});h+='</div>';}
                        const cj = JSON.stringify(compressed);
if(gm)h+=parseGemMatch(gm,m);
                        if (cj.length < 8 * 1024 * 1024) {
                            localStorage.setItem(S.CACHE_KEY, cj);
                        }
                    } else {
                        localStorage.setItem(S.CACHE_KEY, j);
                    }
                } catch (e) {}
            }


// 【6.3新增】传送带显示
            function loadCache() {
if(isConveyor&&packetBank?.ConveyorPreset){const cp=packetBank.ConveyorPreset;
                try {
h+='<div class="conveyor-section"><div class="section-title" style="border-bottom-color:#e67e22;">📦 传送带配置</div>';
                    const s = localStorage.getItem(S.CACHE_KEY);
h+=`<div style="font-size:0.6rem;color:#6b4f2c;">⏱ 初始间隔:${cp.Interval||0}秒 | 每${cp.IntervalIncreaseEvery||0}波缩短 | 倍率:${cp.IntervalMagnification||0}</div>`;
                    if (!s) return false;
if(Array.isArray(cp.Packet)&&cp.Packet.length>0){h+='<div style="margin-top:0.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:0.45rem;color:#ffda7a;">[权重:${p.Weight||'?'}]</span></span>`;});h+='</div>';}
                    const c = JSON.parse(s);
if(Array.isArray(cp.PacketPrioritySpawnList)&&cp.PacketPrioritySpawnList.length>0){h+='<div style="margin-top:0.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>';}
                    if (!c) return false;
h+='</div>';}
                    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();
if(isS&&wm?.Survival){const sk=wm.Survival;const 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:0.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>`;}}
                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(isSh&&packetBank){const cs=pUC(packetBank,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>';});}}
                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() {
if(packetBank?.Value?.length&&!isConveyor){if(packetBank.Method==='Preset'||isI){h+=`<div class="section-title">🎴 预设卡牌</div><div>`;packetBank.Value.forEach(p=>{if(!p)return;const n=typeof p==='string'?p:(p?.PacketName||'');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:0.45rem;color:#ffda7a;">(${pts.join('|')})</span>`;}if(isI)h+=`<span class="zombie-tag">🧟${esc(aL(n,m))}${ex}</span>`;else h+=`<span class="${m.plant.has(n)?'plant-tag':'zombie-tag'}">${m.plant.has(n)?'🌱':'🧟'}${esc(aL(n,m))}${ex}</span>`;});h+='</div>';}}
                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(isSt&&lookStar?.Check){const cs=bCFL(lookStar.Check,m,pL);if(cs.size>0)h+=renderGrid(mapName,cs,'lookstar','⭐ 观星布局');}
                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(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 (!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 => ({ '&': '&amp;', '<': '&lt;',
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);}
                    '>': '&gt;', '"': '&quot;' })[m]); }


// 僵尸
            function gMS(mv) {
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 (!S.optionsData?.map) return { cols: 9, rows: 5 };
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>';}}
                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])),
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>';}}
                    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 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>`;}
                const m = { plant: new Map(), zombie: new Map(), zombieExtra: new Map(), item: new Map(), map: new Map(),
return h;}
                    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;
            }


$('analyzeBtn')?.addEventListener('click',()=>{if(S.isAnalyzing)return;if(!S.optionsData||!S.templatesData||!S.levelData){toast('请上传所有文件','error');return;}S.isAnalyzing=true;const ab=$('analyzeBtn');if(ab){ab.disabled=true;ab.innerHTML='<span class="loading-spinner"></span>解析中...';}requestAnimationFrame(()=>{try{S.lastMaps=bM();const hc=pLvl(S.levelData,S.lastMaps);$('output').innerHTML=hc+'<div style="color:#5a7a3a;margin-top:0.4rem;">✅ 解析完成</div>';toast('解析完成','success');}catch(e){$('output').innerHTML='<span style="color:#c44;">❌ 解析失败:'+esc(e.message)+'</span>';toast('解析失败','error');}finally{S.isAnalyzing=false;if(ab)ab.innerHTML='🔍 解析关卡';updateUI();}});});
            function gWI(l) {
document.addEventListener('keydown',function(e){if(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA')return;const ab=$('analyzeBtn');if(e.ctrlKey&&e.key==='Enter'&&ab&&!ab.disabled){e.preventDefault();ab.click();}});
                const wm = l.WaveManager || l.Feature?.find(f => f.Name === 'Wave')?.Data;
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();}}}
                if (!wm?.Wave || !Array.isArray(wm.Wave) || wm.Wave.length === 0) return null;
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);else init();})();
                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>

2026年7月19日 (日) 04:25的最新版本