DARK ZONE
by RocketDolphin441457 lines126.1 KB🛠️ Three.js (3D graphics)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>DARK ZONE</title>
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@300;500;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
:root{--bg:#0a0908;--bg2:#13110f;--panel:#1a1714;--panel2:#221d18;--line:#3a3530;--line2:#524539;--fg:#e8d5b7;--muted:#8a7e6d;--accent:#d4a574;--accent2:#c8462f;--ok:#7a9b3a;--warn:#d49a3a}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--fg);font-family:'Rajdhani',sans-serif;user-select:none}
.mono{font-family:'JetBrains Mono',monospace}
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
.btn{padding:10px 22px;background:var(--accent2);color:#1a0a06;font-weight:700;letter-spacing:1px;border:1px solid #8a2a1c;clip-path:polygon(8px 0,100% 0,100% calc(100% - 8px),calc(100% - 8px) 100%,0 100%,0 8px)}
.btn:hover{background:#e0553a}
.btn.ghost{background:transparent;color:var(--accent);border:1px solid var(--accent)}
.btn:disabled{opacity:.4;cursor:not-allowed}
#base{position:fixed;inset:0;display:flex;flex-direction:column;background:linear-gradient(180deg,#0a0908,#13110f);z-index:10}
.base-header{display:flex;align-items:center;justify-content:space-between;padding:18px 40px;border-bottom:1px solid var(--line);background:rgba(0,0,0,.3)}
.logo h1{font-size:24px;letter-spacing:6px}
.logo p{font-size:11px;letter-spacing:3px;color:var(--muted)}
.base-stats{display:flex;gap:32px;align-items:center}
.stat-block{text-align:right}
.stat-block .label{font-size:10px;letter-spacing:2px;color:var(--muted)}
.stat-block .value{font-size:22px;font-weight:700;color:var(--accent)}
.money .value{color:var(--ok)}
.base-body{flex:1;display:grid;grid-template-columns:280px 1fr 360px;overflow:hidden}
.panel{background:var(--panel);border-right:1px solid var(--line);display:flex;flex-direction:column;overflow:hidden}
.panel:last-child{border-right:none;border-left:1px solid var(--line)}
.panel-head{padding:14px 20px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center}
.panel-head h2{font-size:13px;letter-spacing:3px;color:var(--accent)}
.panel-content{flex:1;overflow-y:auto;padding:12px}
.stash-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:6px}
.slot{aspect-ratio:1;background:var(--bg2);border:1px solid var(--line);display:flex;flex-direction:column;align-items:center;justify-content:center;position:relative;cursor:pointer;padding:4px}
.slot:hover{border-color:var(--accent);background:var(--panel2)}
.slot.empty{opacity:.4;cursor:default}
.slot .ic{font-size:22px;color:var(--accent)}
.slot .qty{position:absolute;bottom:2px;right:4px;font-size:11px;font-weight:700}
.shop-tabs{display:flex;border-bottom:1px solid var(--line)}
.shop-tab{flex:1;padding:10px;text-align:center;font-size:12px;letter-spacing:2px;color:var(--muted);cursor:pointer;border-bottom:2px solid transparent}
.shop-tab.active{color:var(--accent);border-bottom-color:var(--accent2)}
.shop-item{display:flex;align-items:center;gap:12px;padding:10px;background:var(--bg2);border:1px solid var(--line);margin-bottom:8px}
.shop-item:hover{border-color:var(--line2)}
.shop-item .ic{width:38px;height:38px;background:var(--panel);display:flex;align-items:center;justify-content:center;font-size:20px;border:1px solid var(--line);flex-shrink:0}
.shop-item .info{flex:1}
.shop-item .name{font-size:14px;font-weight:700}
.shop-item .stats{font-size:10px;color:var(--accent);margin-top:3px}
.mini-btn{padding:5px 10px;font-size:11px;font-weight:700;background:var(--accent2);color:#1a0a06;border:1px solid #8a2a1c}
.mini-btn.ghost{background:transparent;color:var(--accent);border:1px solid var(--accent2)}
.mini-btn:disabled{opacity:.3}
.loadout-box{background:var(--bg2);border:1px solid var(--line);padding:12px;margin-bottom:10px}
.loadout-box .lh{display:flex;justify-content:space-between;font-size:11px;color:var(--muted);margin-bottom:8px}
.loadout-slot{background:var(--panel);border:1px dashed var(--line2);padding:10px;display:flex;align-items:center;gap:10px;min-height:54px;cursor:pointer}
.loadout-slot:hover{border-color:var(--accent)}
.loadout-slot.filled{border-style:solid;border-color:var(--accent)}
.loadout-slot.locked{border-style:solid;border-color:var(--line2);cursor:not-allowed;background:rgba(0,0,0,.2)}
.loadout-slot .ic{font-size:22px;color:var(--accent)}
.loadout-slot .nfo{flex:1}
.loadout-slot .nm{font-size:13px;font-weight:700}
.loadout-slot .sub{font-size:10px;color:var(--muted)}
.raid-info{background:var(--bg2);border:1px solid var(--line);padding:14px;margin-bottom:10px}
.raid-info h3{font-size:12px;letter-spacing:3px;color:var(--accent2);margin-bottom:10px}
.diff-card{padding:10px;text-align:center;font-size:13px;letter-spacing:2px;background:var(--bg2);border:1px solid var(--line);cursor:pointer;font-weight:700}
.diff-card.active{border-color:var(--accent2);color:var(--accent);background:rgba(200,70,47,.1)}
.time-card{padding:8px;text-align:center;font-size:12px;letter-spacing:1px;background:var(--bg2);border:1px solid var(--line);cursor:pointer;font-weight:700}
.time-card.active{border-color:var(--accent2);color:var(--accent);background:rgba(200,70,47,.1)}
#deployBtn{width:100%;padding:14px;font-size:14px;letter-spacing:4px}
#game{position:fixed;inset:0;display:block;z-index:1}
#hud{position:fixed;inset:0;pointer-events:none;z-index:5;display:none}
.hud-bottom{position:absolute;bottom:24px;left:24px;right:24px;display:flex;justify-content:space-between;align-items:flex-end}
.hud-top{position:absolute;top:20px;left:50%;transform:translateX(-50%);background:rgba(10,9,8,.7);border:1px solid var(--line);padding:8px 24px;text-align:center}
.hud-msg{position:absolute;top:120px;left:50%;transform:translateX(-50%);pointer-events:none}
.toast{background:rgba(10,9,8,.85);border-left:3px solid var(--accent);padding:8px 16px;font-size:13px;margin-bottom:6px;animation:fadeOut 3s forwards}
.toast.warn{border-color:var(--accent2)}.toast.ok{border-color:var(--ok)}
@keyframes fadeOut{0%,70%{opacity:1}100%{opacity:0}}
#minimap{position:fixed;top:20px;left:20px;width:160px;height:160px;background:rgba(10,9,8,.7);border:1px solid var(--line);z-index:5}
#crosshair{position:fixed;top:50%;left:50%;width:24px;height:24px;transform:translate(-50%,-50%);pointer-events:none;z-index:5}
#crosshair::before,#crosshair::after{content:'';position:absolute;background:var(--accent)}
#crosshair::before{top:50%;left:0;right:0;height:1px;transform:translateY(-50%)}
#crosshair::after{left:50%;top:0;bottom:0;width:1px;transform:translateX(-50%)}
#hitmarker{position:fixed;top:50%;left:50%;width:0;height:0;transform:translate(-50%,-50%);pointer-events:none;z-index:7;opacity:0}
#hitmarker.show{animation:hmFlash 0.35s ease-out}
.hm{position:absolute;width:9px;height:2px;background:#fff;transform-origin:center;box-shadow:0 0 3px #000,0 0 2px #000}
.hm1{top:-11px;left:-11px;transform:rotate(45deg)}
.hm2{top:-11px;left:2px;transform:rotate(-45deg)}
.hm3{top:2px;left:-11px;transform:rotate(-45deg)}
.hm4{top:2px;left:2px;transform:rotate(45deg)}
@keyframes hmFlash{0%{opacity:1;transform:translate(-50%,-50%) scale(0.4)}25%{opacity:1;transform:translate(-50%,-50%) scale(1.3)}100%{opacity:0;transform:translate(-50%,-50%) scale(1)}}
#pauseOverlay{position:fixed;inset:0;background:rgba(0,0,0,.6);display:none;align-items:center;justify-content:center;z-index:15;pointer-events:none}
#pauseOverlay .text{font-size:28px;letter-spacing:6px;font-weight:700;text-align:center}
#lootUI,#resultUI,#loadingUI,#deathScreen,#extractUI{position:fixed;inset:0;display:none;align-items:center;justify-content:center;z-index:30}
#lootUI{background:rgba(0,0,0,.85);z-index:20}
.loot-panel{width:840px;max-width:95vw;background:var(--panel);border:1px solid var(--line2)}
.loot-body{display:grid;grid-template-columns:1fr 1fr;height:500px}
.loot-side{padding:14px;overflow-y:auto;border-right:1px solid var(--line)}
.loot-side:last-child{border-right:none}
.inv-grid{display:grid;gap:4px;position:relative}
.inv-item-grid{background:var(--panel2);border:1px solid var(--line);display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:grab;position:relative;padding:2px;z-index:1;overflow:hidden;min-height:40px;transition:transform 0.1s, opacity 0.1s}
.inv-item-grid:hover{border-color:var(--accent);z-index:1000;transform:scale(1.05);box-shadow:0 0 15px rgba(0,0,0,1)}
.inv-item-grid.dragging{opacity:0.3;transform:none;z-index:0;pointer-events:none}
.inv-item-grid .ic{font-size:20px;pointer-events:none}
.inv-item-grid .nm{font-size:9px;color:var(--muted);text-align:center;pointer-events:none;line-height:1.1;word-break:break-all;width:100%}
.inv-item-grid .qty{position:absolute;bottom:2px;right:4px;font-size:11px;font-weight:700;color:var(--accent);background:rgba(0,0,0,0.7);padding:0 4px;border-radius:3px}
.inv-item-grid .btns{position:absolute;top:2px;right:2px;display:flex;flex-direction:column;gap:2px}
.inv-item-grid .btns button{font-size:9px;padding:2px 4px;background:rgba(0,0,0,.8);color:#fff;border:1px solid var(--line);pointer-events:auto}
.empty-slot-grid{background:rgba(0,0,0,.3);border:1px dashed var(--line)}
.drag-ghost{position:fixed;pointer-events:none;z-index:200;opacity:0.8;display:flex;flex-direction:column;align-items:center;justify-content:center;background:var(--panel);border:1px solid var(--accent)}
#resultUI{background:rgba(0,0,0,.92)}
.result-panel{width:680px;max-width:95vw;background:var(--panel);border:1px solid var(--line2)}
.result-head{padding:24px;text-align:center;border-bottom:1px solid var(--line)}
.result-head .status{font-size:32px;letter-spacing:8px;font-weight:700}
#loadingUI{background:var(--bg);z-index:40}
#deathScreen{background:rgba(60,10,5,.6);z-index:25;pointer-events:none}
#replayUI{display:none;position:fixed;top:15%;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.85);padding:15px 30px;border:1px solid #c8462f;text-align:center;z-index:26;pointer-events:none}
#replayUI .t1{color:#c8462f;font-size:14px;letter-spacing:3px}
#replayUI .t2{font-size:22px;font-weight:700;margin-top:5px}
#replayUI .t3{font-size:12px;color:#d49a3a;margin-top:5px}
#extractUI{background:rgba(0,0,0,.4);z-index:18;pointer-events:none}
#burnUI{position:fixed;top:40%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,.85);border:2px solid #d49a3a;padding:20px 40px;display:none;z-index:19;text-align:center;pointer-events:none}
.controls-hint{position:fixed;top:20px;right:20px;background:rgba(10,9,8,.7);border:1px solid var(--line);padding:10px;font-size:11px;color:var(--muted);z-index:5}
.controls-hint kbd{background:var(--panel);border:1px solid var(--line2);padding:1px 6px;color:var(--accent);font-family:'JetBrains Mono',monospace;font-size:10px}
.tooltip{position:fixed;background:rgba(0,0,0,0.95);border:1px solid var(--accent);padding:8px 12px;font-size:12px;color:var(--fg);pointer-events:none;z-index:100;max-width:200px;display:none}
.tooltip.red{border-color:#c8462f;box-shadow:0 0 10px rgba(200,70,47,0.5)}
.tooltip.red b{font-size:14px}
.gear-ui{display:flex;gap:8px;margin-bottom:8px}
.gear-box{width:54px;height:54px;background:var(--panel);border:1px solid var(--line);position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;overflow:hidden}
.gear-box .ic{font-size:20px;line-height:1}
.gear-box .rn{font-size:11px;font-weight:700;color:var(--accent);line-height:1}
.gear-box .du{font-size:9px;color:var(--muted);line-height:1}
.gear-box .dmg-overlay{position:absolute;bottom:0;left:0;width:100%;height:0%;background:rgba(255,0,0,0.6);transition:height 0.2s;pointer-events:none}
#scopeUI{position:fixed;inset:0;display:none;pointer-events:none;z-index:4;background:rgba(0,0,0,0.7)}
.scope-circle{position:absolute;top:50%;left:50%;width:100vh;height:100vh;transform:translate(-50%,-50%);border-radius:50%;border:2px solid rgba(0,0,0,0.9);box-shadow:0 0 0 9999px rgba(0,0,0,0.85)}
.scope-cross-v{position:absolute;top:50%;left:50%;width:2px;height:60vh;transform:translate(-50%,-50%);background:rgba(0,0,0,0.5)}
.scope-cross-h{position:absolute;top:50%;left:50%;width:100vw;height:2px;transform:translate(-50%,-50%);background:rgba(0,0,0,0.5)}
.scope-dot{position:absolute;top:50%;left:50%;width:4px;height:4px;transform:translate(-50%,-50%);background:var(--accent2);border-radius:50%}
#scopeDist{position:absolute;top:56%;left:50%;transform:translate(-50%,0);color:var(--accent2);font-size:16px;letter-spacing:2px;text-shadow:0 0 4px #000}
#nightVisionUI{position:fixed;inset:0;pointer-events:none;z-index:3;background:rgba(0,0,0,0);mix-blend-mode:multiply;transition:background 0.3s}
#throwUI{position:fixed;top:55%;left:50%;transform:translate(-50%,-50%);pointer-events:none;z-index:5;display:none;text-align:center}
#throwIcon{font-size:48px;text-shadow:0 0 10px #000}
#throwTimer{font-size:32px;font-weight:700;color:#d49a3a;text-shadow:0 0 5px #000}
</style>
</head>
<body>
<div id="base">
<div class="base-header">
<div class="logo"><h1>DARK ZONE</h1><p>暗区撤离</p></div>
<div class="base-stats">
<div class="stat-block money"><div class="label">资金</div><div class="value mono" id="baseMoney">¥500,000</div></div>
<button class="btn ghost" id="helpBtn">操作说明</button>
</div>
</div>
<div class="base-body">
<div class="panel"><div class="panel-head"><h2>仓库</h2><span id="stashCount">0/50</span></div><div class="panel-content" id="stashContent"></div></div>
<div class="panel"><div class="panel-head"><h2>商人</h2></div>
<div class="shop-tabs"><div class="shop-tab active" data-cat="all">全部</div><div class="shop-tab" data-cat="weapon">武器</div><div class="shop-tab" data-cat="gear">装备</div><div class="shop-tab" data-cat="consumable">消耗品</div><div class="shop-tab" data-cat="valuable">高价值</div></div>
<div class="panel-content" id="shopContent"></div>
</div>
<div class="panel"><div class="panel-head"><h2>出击配置</h2></div><div class="panel-content">
<div class="raid-info"><h3>难度选择</h3>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px" id="diffSelector">
<div class="diff-card" data-diff="0">普通</div><div class="diff-card" data-diff="1">机密</div><div class="diff-card" data-diff="2">绝密</div>
</div>
<p id="mapDesc" style="font-size:11px;color:var(--muted);margin-top:8px"></p>
<h3 style="margin-top:12px">昼夜选择</h3>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px" id="timeSelector">
<div class="time-card active" data-time="day">白天</div><div class="time-card" data-time="night">永夜</div>
</div>
<p style="font-size:11px;color:var(--muted);margin-top:8px">永夜模式下视野极差,建议携带具备夜视功能的头盔。</p>
<div style="margin-top:8px;font-size:11px;color:var(--warn)">当前战备值: <span id="loadoutScore">0</span></div>
</div>
<div class="loadout-box"><div class="lh"><span>主武器1 (1键)</span><span id="loPrimaryTag">未装备</span></div><div class="loadout-slot" id="loPrimary"><span style="color:var(--muted)">点击仓库装备</span></div></div>
<div class="loadout-box"><div class="lh"><span>主武器2 (2键)</span><span id="loSecondaryTag">未装备</span></div><div class="loadout-slot" id="loSecondary"><span style="color:var(--muted)">点击仓库装备</span></div></div>
<div class="loadout-box"><div class="lh"><span>护甲</span><span id="loArmorTag">未装备</span></div><div class="loadout-slot" id="loArmor"><span style="color:var(--muted)">点击仓库装备</span></div></div>
<div class="loadout-box"><div class="lh"><span>头盔</span><span id="loHelmetTag">未装备</span></div><div class="loadout-slot" id="loHelmet"><span style="color:var(--muted)">点击仓库装备</span></div></div>
<div class="loadout-box"><div class="lh"><span>背包</span><span id="loBackpackTag">默认12格</span></div><div class="loadout-slot" id="loBackpack"><span style="color:var(--muted)">点击仓库装备</span></div></div>
<div class="loadout-box"><div class="lh"><span>战术物品 (点击仓库添加)</span><span id="loBagTag">0/12</span></div><div id="loBagContent" style="display:grid;grid-template-columns:repeat(5,1fr);gap:4px;min-height:60px;max-height:180px;overflow-y:auto"></div></div>
<button class="btn" id="deployBtn">进入战区</button>
</div></div>
</div>
</div>
<canvas id="game"></canvas>
<div id="nightVisionUI"></div>
<div id="hud">
<div class="hud-top"><div style="font-size:11px;color:var(--muted)">任务</div><div id="hudObjective" style="font-size:14px;color:var(--accent)">搜刮物资并撤离</div><div class="mono" id="hudTimer" style="font-size:11px;color:var(--warn)">10:00</div></div>
<div class="controls-hint"><kbd>WASD</kbd>移动 <kbd>SHIFT</kbd>奔跑 <kbd>Z</kbd>开镜 <kbd>X</kbd>扑灭 <kbd>N</kbd>夜视 <kbd>M</kbd>静音 <kbd>1</kbd>主武器1 <kbd>2</kbd>主武器2 <kbd>3</kbd>投掷物 <kbd>4</kbd>切换投掷 <kbd>R</kbd>换弹 <kbd>F</kbd>搜刮/拉闸 <kbd>H</kbd>治疗 <kbd>T</kbd>修甲 <kbd>G</kbd>修盔<br>投掷: 按<kbd>3</kbd>进入瞄准, <kbd>左键</kbd>蓄力并开始倒计时, 松开投出</div>
<div id="minimap"><canvas id="mmCanvas" width="160" height="160"></canvas></div>
<div id="crosshair"></div>
<div id="hitmarker">
<div class="hm hm1"></div><div class="hm hm2"></div><div class="hm hm3"></div><div class="hm hm4"></div>
</div>
<div class="hud-msg" id="hudMsg"></div>
<div class="hud-bottom">
<div style="background:rgba(10,9,8,.7);border:1px solid var(--line);padding:12px;min-width:240px">
<div class="gear-ui">
<div class="gear-box"><div class="ic" id="hudHelmetIcon">—</div><div class="rn mono" id="hudHelmetRoman">-</div><div class="du mono" id="hudHelmetDur">0/0</div><div class="dmg-overlay" id="hudHelmetDmg"></div></div>
<div class="gear-box"><div class="ic" id="hudArmorIcon">—</div><div class="rn mono" id="hudArmorRoman">-</div><div class="du mono" id="hudArmorDur">0/0</div><div class="dmg-overlay" id="hudArmorDmg"></div></div>
</div>
<div style="display:flex;align-items:center;gap:8px;margin:4px 0;font-size:11px"><span style="color:var(--muted);width:50px">生命</span><div style="flex:1;height:6px;background:var(--bg);border:1px solid var(--line)"><div id="hpFill" style="height:100%;background:#c8462f;width:100%"></div></div><span class="mono" id="hpVal">100</span></div>
<div style="display:flex;align-items:center;gap:8px;margin:4px 0;font-size:11px"><span style="color:var(--muted);width:50px">体力</span><div style="flex:1;height:6px;background:var(--bg);border:1px solid var(--line)"><div id="stFill" style="height:100%;background:#d49a3a;width:100%"></div></div><span class="mono" id="stVal">100</span></div>
<div style="display:flex;align-items:center;gap:8px;margin:4px 0;font-size:11px"><span style="color:var(--muted);width:50px">背包</span><div style="flex:1;height:6px;background:var(--bg);border:1px solid var(--line)"><div id="invFill" style="height:100%;background:#d4a574;width:0%"></div></div><span class="mono" id="invVal">0/12</span></div>
</div>
<div style="background:rgba(10,9,8,.7);border:1px solid var(--line);padding:12px;text-align:right">
<div id="wpnName" style="font-size:16px;color:var(--accent)">无武器</div>
<div style="font-size:24px;font-weight:700"><span class="mono" id="wpnAmmoCur">0</span> / <span class="mono" id="wpnAmmoMax" style="color:var(--muted);font-size:16px">0</span></div>
</div>
</div>
<div id="throwUI">
<div id="throwIcon"></div>
<div id="throwTimer"></div>
</div>
</div>
<div id="scopeUI">
<div class="scope-circle"></div>
<div class="scope-cross-v"></div>
<div class="scope-cross-h"></div>
<div class="scope-dot"></div>
<div id="scopeDist" class="mono">0m</div>
</div>
<div id="damageVignette" style="position:fixed;inset:0;pointer-events:none;box-shadow:inset 0 0 200px rgba(200,70,47,0);z-index:6"></div>
<div id="burnVignette" style="position:fixed;inset:0;pointer-events:none;box-shadow:inset 0 0 150px rgba(255,69,0,0);z-index:6;transition:box-shadow 0.1s"></div>
<div id="replayUI">
<div class="t1">你被击杀</div>
<div class="t2" id="replayKiller"></div>
<div class="t3" id="replayInfo"></div>
</div>
<div id="deathScreen"><div style="font-size:64px;letter-spacing:12px;color:var(--accent2);font-weight:700">YOU DIED</div></div>
<div id="pauseOverlay"><div class="text" id="pauseText">准备就绪</div><div style="text-align:center;color:#ccc;margin-top:10px">点击屏幕开始</div></div>
<div id="lootUI"><div class="loot-panel"><div style="padding:14px 20px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center"><h3 style="color:var(--accent)">搜刮</h3><span id="lootTotalValue" style="color:var(--ok)">背包价值:¥0</span><button class="mini-btn ghost" id="lootClose">关闭</button></div><div class="loot-body"><div class="loot-side"><h4 style="color:var(--muted);margin-bottom:10px">容器物品</h4><div id="lootContainer" class="inv-grid" style="grid-template-columns:repeat(5,1fr)"></div></div><div class="loot-side"><h4 style="color:var(--muted);margin-bottom:10px">你的背包 (拖拽整理,按 R 或 方向键 旋转)</h4><div id="lootInventory" class="inv-grid"></div></div></div></div></div>
<div id="extractUI"><div style="background:rgba(10,9,8,.9);border:2px solid var(--ok);padding:40px 60px;text-align:center"><div style="font-size:18px;color:var(--ok)" id="extractStatus">撤离中</div><div class="mono" id="extractTimer" style="font-size:56px;font-weight:700">15</div></div></div>
<div id="burnUI"><div style="font-size:18px;color:#d49a3a" id="burnStatus">扑灭中 (保持静止)</div><div class="mono" id="burnTimer" style="font-size:48px;font-weight:700;color:#fff">3</div></div>
<div id="resultUI"><div class="result-panel"><div class="result-head"><div class="status" id="resultStatus">生还</div><div id="resultSub" style="color:var(--muted);font-size:12px;margin-top:4px">EXTRACTION SUCCESSFUL</div></div><div style="padding:20px;max-height:60vh;overflow-y:auto"><div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:18px"><div style="background:var(--bg2);border:1px solid var(--line);padding:14px;text-align:center"><div style="font-size:10px;color:var(--muted)">击杀</div><div class="mono" id="rsKills" style="font-size:22px;font-weight:700">0</div></div><div style="background:var(--bg2);border:1px solid var(--line);padding:14px;text-align:center"><div style="font-size:10px;color:var(--muted)">搜刮价值</div><div class="mono" id="rsLoot" style="font-size:22px;font-weight:700;color:var(--ok)">¥0</div></div><div style="background:var(--bg2);border:1px solid var(--line);padding:14px;text-align:center"><div style="font-size:10px;color:var(--muted)" id="rsTotalLbl">总收益</div><div class="mono" id="rsTotal" style="font-size:22px;font-weight:700">¥0</div></div></div><div id="rsItems"></div></div><div style="padding:20px;border-top:1px solid var(--line);display:flex;justify-content:space-between;align-items:center"><div>余额<span class="mono" id="rsBalance" style="font-size:24px;font-weight:700;color:var(--ok);margin-left:10px">¥0</span></div><button class="btn" id="backToBase">返回基地</button></div></div></div>
<div id="loadingUI" style="flex-direction:column;gap:20px"><div style="font-size:32px;letter-spacing:8px;color:var(--accent);font-weight:700">DARK ZONE</div><div style="width:300px;height:3px;background:var(--line);overflow:hidden"><div style="width:30%;height:100%;background:var(--accent2);animation:loadbar 1.2s infinite"></div></div><div id="loadText" style="font-size:11px;color:var(--muted)">部署中...</div></div>
<div class="tooltip" id="tooltip"></div>
<div id="dragGhost" class="drag-ghost" style="display:none"></div>
<script src="https://unpkg.com/three@0.149.0/build/three.min.js"></script>
<script>
"use strict";
const $=id=>document.getElementById(id);
const fmt=n=>'¥'+(n||0).toLocaleString();
const rc=r=>({common:'#8a7e6d',uncommon:'#7a9b3a',blue:'#3a7a9b',purple:'#9b3a7a',gold:'#d49a3a',red:'#c8462f'}[r]||'#8a7e6d');
const toRoman=num=>{if(num===1)return 'I';if(num===2)return 'II';if(num===3)return 'III';if(num===4)return 'IV';if(num===5)return 'V';if(num===6)return 'VI';return '-'};
function sizeToWH(size){
let w,h;
switch(size){
case 1:w=1;h=1;break;
case 2:w=2;h=1;break;
case 3:w=3;h=1;break;
case 4:w=2;h=2;break;
case 6:w=3;h=2;break;
case 8:w=4;h=2;break;
case 9:w=3;h=3;break;
case 12:w=4;h=3;break;
default:w=1;h=1;
}
return {w,h};
}
const MAP_CFG={worldSize:120,groundColor:0x4a5a3a,lootCount:16};
const DIFFS=[
{name:'普通',enemyHpMul:0.8,enemyDmgMul:0.7,enemyCountMul:1.0,valWeight:150,redMul:0.3,goldMul:0.7,desc:'仅出现基础敌人。固定撤离点,1个空投。无战备要求。红色物资爆率极低,仅少量低价值红。',reqScore:0,enemyPool:['guard','shield'],airdropCount:1,extractType:'fixed',enemySenseMul:1.0,enemySpeedMul:1.0},
{name:'机密',enemyHpMul:1.2,enemyDmgMul:1.0,enemyCountMul:1.5,valWeight:230,redMul:0.7,goldMul:1.0,desc:'出现机枪兵与火箭兵。需拉闸撤离(10s),1个空投。需战备值 25。红色物资爆率中等,有概率出中价值红。',reqScore:25,enemyPool:['guard','shield','mg','rocket'],airdropCount:1,extractType:'activate',enemySenseMul:1.2,enemySpeedMul:1.1},
{name:'绝密',enemyHpMul:1.6,enemyDmgMul:1.4,enemyCountMul:2.0,valWeight:320,redMul:1.3,goldMul:1.4,desc:'全兵种出没且感知提升。需拉闸撤离且引敌包围,2个空投。需战备值 50。红色物资爆率最高,有概率出顶级大红。',reqScore:50,enemyPool:['guard','shield','mg','rocket','flame'],airdropCount:2,extractType:'activate_siege',enemySenseMul:1.5,enemySpeedMul:1.2}
];
const ET={
guard:{name:'卫队士兵',hp:80,speed:2.5,fireRate:700,dmg:8,range:40,color:0x3a3a3a,headColor:0xc8a070,scale:1},
shield:{name:'盾兵',hp:120,speed:1.8,fireRate:500,dmg:10,range:30,color:0x2a3a4a,headColor:0xc8a070,scale:0.95,hasShield:true},
mg:{name:'机枪兵',hp:350,speed:1.5,fireRate:120,dmg:14,range:60,color:0x4a2a2a,headColor:0x888888,scale:1.15,bodyResist:0.3,headMult:2.5},
rocket:{name:'火箭兵',hp:60,speed:2.0,fireRate:2500,dmg:55,range:70,color:0x4a4a2a,headColor:0xc8a070,scale:1,isRocket:true,keepDistance:35},
flame:{name:'喷火兵',hp:90,speed:2.8,fireRate:300,dmg:30,range:9,color:0x5a2a1a,headColor:0xc8a070,scale:1.05,isFlame:true,fuelTank:true}
};
const GRADE_REDUCE=[0,0.30,0.40,0.50,0.55,0.60,0.65];
const ITEMS={
knife:{name:'战术匕首',icon:'🔪',cat:'weapon',sub:'近战',buy:null,sell:800,size:1,dmg:200,fireRate:400,mag:0,range:2.5,auto:false,ammoType:'melee',rarity:'common',grade:1},
pistol_l:{name:'磨损手枪',icon:'🔫',cat:'weapon',sub:'手枪',buy:2000,sell:1000,size:1,dmg:18,fireRate:340,mag:10,range:45,auto:false,ammoType:'pistol',rarity:'common',grade:1},
pistol:{name:'P220手枪',icon:'🔫',cat:'weapon',sub:'手枪',buy:5000,sell:2500,size:1,dmg:24,fireRate:280,mag:12,range:60,auto:false,ammoType:'pistol',rarity:'common',grade:2},
smg_l:{name:'老旧冲锋枪',icon:'🔫',cat:'weapon',sub:'冲锋枪',buy:6000,sell:3000,size:1,dmg:14,fireRate:110,mag:25,range:40,auto:true,ammoType:'pistol',rarity:'common',grade:1},
smg:{name:'MP5冲锋枪',icon:'🔫',cat:'weapon',sub:'冲锋枪',buy:20000,sell:10000,size:1,dmg:18,fireRate:90,mag:30,range:50,auto:true,ammoType:'pistol',rarity:'uncommon',grade:2},
ump45:{name:'UMP45冲锋枪',icon:'🔫',cat:'weapon',sub:'冲锋枪',buy:25000,sell:12500,size:1,dmg:25,fireRate:100,mag:25,range:55,auto:true,ammoType:'pistol',rarity:'blue',grade:3},
vector:{name:'Vector冲锋枪',icon:'🔫',cat:'weapon',sub:'冲锋枪',buy:50000,sell:25000,size:1,dmg:22,fireRate:60,mag:20,range:50,auto:true,ammoType:'pistol',rarity:'purple',grade:4},
rifle_l:{name:'生锈步枪',icon:'🔫',cat:'weapon',sub:'步枪',buy:15000,sell:7500,size:1,dmg:32,fireRate:180,mag:20,range:90,auto:false,ammoType:'rifle',rarity:'common',grade:1},
rifle:{name:'AKM步枪',icon:'🔫',cat:'weapon',sub:'步枪',buy:40000,sell:20000,size:1,dmg:42,fireRate:140,mag:30,range:120,auto:true,ammoType:'rifle',rarity:'blue',grade:3},
m4a1:{name:'M4A1突击步枪',icon:'🔫',cat:'weapon',sub:'步枪',buy:60000,sell:30000,size:1,dmg:38,fireRate:120,mag:30,range:110,auto:true,ammoType:'rifle',rarity:'blue',grade:3},
scarl:{name:'SCAR-L步枪',icon:'🔫',cat:'weapon',sub:'步枪',buy:80000,sell:40000,size:1,dmg:40,fireRate:130,mag:30,range:120,auto:true,ammoType:'rifle',rarity:'purple',grade:4},
fal:{name:'FAL自动步枪',icon:'🔫',cat:'weapon',sub:'步枪',buy:90000,sell:45000,size:1,dmg:45,fireRate:160,mag:20,range:130,auto:false,ammoType:'rifle',rarity:'purple',grade:4},
shotgun:{name:'M870霰弹枪',icon:'🔫',cat:'weapon',sub:'霰弹枪',buy:25000,sell:12500,size:1,dmg:90,fireRate:700,mag:6,range:25,auto:false,ammoType:'shell',rarity:'uncommon',grade:2},
s12k:{name:'S12K霰弹枪',icon:'🔫',cat:'weapon',sub:'霰弹枪',buy:40000,sell:20000,size:1,dmg:70,fireRate:300,mag:8,range:30,auto:true,ammoType:'shell',rarity:'blue',grade:3},
sniper:{name:'M24狙击枪',icon:'🔫',cat:'weapon',sub:'狙击枪',buy:120000,sell:60000,size:1,dmg:120,fireRate:1500,mag:5,range:200,auto:false,ammoType:'rifle',rarity:'purple',grade:4,zoom:true},
svd:{name:'SVD狙击步枪',icon:'🔫',cat:'weapon',sub:'狙击枪',buy:150000,sell:75000,size:1,dmg:100,fireRate:800,mag:10,range:250,auto:false,ammoType:'rifle',rarity:'purple',grade:4,zoom:true},
awm:{name:'AWM狙击步枪',icon:'🔫',cat:'weapon',sub:'狙击枪',buy:300000,sell:150000,size:1,dmg:200,fireRate:2000,mag:5,range:300,auto:false,ammoType:'rifle',rarity:'gold',grade:5,zoom:true},
ammo_pistol:{name:'9mm手枪弹',icon:'■',cat:'consumable',sub:'弹药',buy:30,sell:15,size:1,stack:60,ammoType:'pistol',rarity:'common',grade:1},
ammo_rifle:{name:'7.62mm步枪弹',icon:'■',cat:'consumable',sub:'弹药',buy:80,sell:40,size:1,stack:30,ammoType:'rifle',rarity:'common',grade:1},
ammo_shell:{name:'12号霰弹',icon:'■',cat:'consumable',sub:'弹药',buy:120,sell:60,size:1,stack:6,ammoType:'shell',rarity:'common',grade:1},
grenade:{name:'破片手雷',icon:'💣',cat:'consumable',sub:'投掷物',buy:1000,sell:500,size:1,stack:3,dmg:150,radius:8,fuse:3.0,rarity:'uncommon',grade:1},
molotov:{name:'燃烧瓶',icon:'🍶',cat:'consumable',sub:'投掷物',buy:1500,sell:750,size:1,stack:2,dmg:30,radius:5,duration:6.0,rarity:'uncommon',grade:1},
smoke_grenade:{name:'烟雾弹',icon:'💨',cat:'consumable',sub:'投掷物',buy:1200,sell:600,size:1,stack:2,radius:6,duration:12.0,rarity:'uncommon',grade:1},
armor_scrap:{name:'破损护甲',icon:'🛡',cat:'gear',sub:'护甲',buy:1000,sell:500,size:1,armor:30,maxDur:60,rarity:'common',grade:1},
armor_light:{name:'轻型护甲',icon:'🛡',cat:'gear',sub:'护甲',buy:3000,sell:1500,size:1,armor:65,maxDur:100,rarity:'uncommon',grade:2},
armor_tg_h:{name:'TG-H防弹衣',icon:'🦺',cat:'gear',sub:'护甲',buy:15000,sell:7500,size:1,armor:70,maxDur:120,rarity:'blue',grade:3},
armor_shooter:{name:'射手战术背心',icon:'🦺',cat:'gear',sub:'护甲',buy:20000,sell:10000,size:1,armor:85,maxDur:130,rarity:'blue',grade:3},
armor_mk2:{name:'MK-2战术背心',icon:'🦺',cat:'gear',sub:'护甲',buy:50000,sell:25000,size:1,armor:90,maxDur:150,rarity:'purple',grade:4},
armor_hmp:{name:'HMP特勤防弹衣',icon:'🦺',cat:'gear',sub:'护甲',buy:80000,sell:40000,size:1,armor:110,maxDur:160,rarity:'purple',grade:4},
armor_elite:{name:'精英防弹背心',icon:'🦺',cat:'gear',sub:'护甲',buy:150000,sell:75000,size:1,armor:100,maxDur:180,rarity:'gold',grade:5},
armor_fs:{name:'FS复合防弹衣',icon:'🦺',cat:'gear',sub:'护甲',buy:200000,sell:100000,size:1,armor:120,maxDur:200,rarity:'gold',grade:5},
armor_heavy:{name:'重型突击背心',icon:'🦺',cat:'gear',sub:'护甲',buy:250000,sell:125000,size:1,armor:130,maxDur:220,rarity:'gold',grade:5},
armor_mas2:{name:'特里克MAS2.0装甲',icon:'🦺',cat:'gear',sub:'护甲',buy:1000000,sell:500000,size:1,armor:120,maxDur:250,rarity:'red',grade:6},
armor_titan:{name:'泰坦防弹装甲',icon:'🦺',cat:'gear',sub:'护甲',buy:1500000,sell:750000,size:1,armor:140,maxDur:280,rarity:'red',grade:6},
armor_ha2:{name:'HA-2重型防弹衣',icon:'🦺',cat:'gear',sub:'护甲',buy:2000000,sell:1000000,size:1,armor:150,maxDur:300,rarity:'red',grade:6},
armor_repair:{name:'护甲维修包',icon:'🔧',cat:'consumable',sub:'维修',buy:1000,sell:500,size:1,repair:60,rarity:'common',grade:1},
helmet_repair:{name:'头盔维修包',icon:'🔧',cat:'consumable',sub:'头盔维修',buy:1500,sell:750,size:1,repair:60,rarity:'common',grade:1},
helmet_cap:{name:'户外棒球帽',icon:'🪖',cat:'gear',sub:'头盔',buy:10000,sell:5000,size:1,armor:5,maxDur:10,rarity:'common',grade:1,hearing:10,aimPenalty:0,movePenalty:0},
helmet_steel:{name:'老式钢盔',icon:'🪖',cat:'gear',sub:'头盔',buy:2000,sell:1000,size:1,armor:10,maxDur:20,rarity:'common',grade:1,hearing:0,aimPenalty:0,movePenalty:0},
helmet_security:{name:'安保头盔',icon:'🪖',cat:'gear',sub:'头盔',buy:2500,sell:1200,size:1,armor:10,maxDur:20,rarity:'common',grade:1,hearing:0,aimPenalty:0,movePenalty:0},
helmet_bonnie:{name:'奔尼帽',icon:'🪖',cat:'gear',sub:'头盔',buy:9000,sell:4500,size:1,armor:5,maxDur:15,rarity:'common',grade:1,hearing:50,aimPenalty:0,movePenalty:0},
helmet_lvl2:{name:'二级通用头盔',icon:'🪖',cat:'gear',sub:'头盔',buy:5000,sell:2500,size:1,armor:15,maxDur:30,rarity:'uncommon',grade:2,hearing:0,aimPenalty:0,movePenalty:0},
helmet_mc201:{name:'MC201防弹头盔',icon:'🪖',cat:'gear',sub:'头盔',buy:30000,sell:15000,size:1,armor:25,maxDur:34,rarity:'blue',grade:3,hearing:20,aimPenalty:1,movePenalty:1},
helmet_das:{name:'DAS防弹头盔',icon:'🪖',cat:'gear',sub:'头盔',buy:30000,sell:15000,size:1,armor:30,maxDur:40,rarity:'blue',grade:3,hearing:0,aimPenalty:0,movePenalty:0},
helmet_anti_riot:{name:'防爆头盔',icon:'🪖',cat:'gear',sub:'头盔',buy:20000,sell:10000,size:1,armor:25,maxDur:35,rarity:'blue',grade:3,hearing:10,aimPenalty:2,movePenalty:2},
helmet_d6:{name:'D6战术头盔',icon:'🪖',cat:'gear',sub:'头盔',buy:10000,sell:5000,size:1,armor:40,maxDur:55,rarity:'purple',grade:4,hearing:-10,aimPenalty:8,movePenalty:4},
helmet_mhs:{name:'MHS战术头盔',icon:'🪖',cat:'gear',sub:'头盔',buy:70000,sell:35000,size:1,armor:35,maxDur:25,rarity:'purple',grade:4,hearing:30,aimPenalty:3,movePenalty:1},
helmet_dich:{name:'DICH训练头盔',icon:'🪖',cat:'gear',sub:'头盔',buy:20000,sell:10000,size:1,armor:35,maxDur:35,rarity:'purple',grade:4,hearing:50,aimPenalty:0,movePenalty:2},
helmet_gt1:{name:'GT1战术头盔',icon:'🪖',cat:'gear',sub:'头盔',buy:15000,sell:7500,size:1,armor:40,maxDur:48,rarity:'purple',grade:4,hearing:20,aimPenalty:4,movePenalty:4},
helmet_mask1:{name:'Mask-1铁臂头盔',icon:'🪖',cat:'gear',sub:'头盔',buy:150000,sell:75000,size:1,armor:60,maxDur:75,rarity:'gold',grade:5,hearing:-15,aimPenalty:6,movePenalty:5},
helmet_h09:{name:'H09防爆头盔',icon:'🪖',cat:'gear',sub:'头盔',buy:80000,sell:40000,size:1,armor:50,maxDur:45,rarity:'gold',grade:5,hearing:20,aimPenalty:4,movePenalty:3},
helmet_gn_heavy:{name:'GN重型头盔',icon:'🪖',cat:'gear',sub:'头盔',buy:100000,sell:50000,size:1,armor:50,maxDur:50,rarity:'gold',grade:5,hearing:10,aimPenalty:4,movePenalty:3},
helmet_dich1:{name:'DICH-1战术头盔',icon:'🪖',cat:'gear',sub:'头盔',buy:300000,sell:150000,size:1,armor:45,maxDur:40,rarity:'gold',grade:5,hearing:40,aimPenalty:1,movePenalty:1},
helmet_gn_nv:{name:'GN重型夜视头盔',icon:'🪖',cat:'gear',sub:'头盔',buy:200000,sell:100000,size:1,armor:50,maxDur:50,rarity:'gold',grade:5,hearing:10,aimPenalty:4,movePenalty:3,nv:true},
helmet_gn_nv2:{name:'GN久战重型夜视头盔',icon:'🪖',cat:'gear',sub:'头盔',buy:250000,sell:125000,size:1,armor:50,maxDur:50,rarity:'gold',grade:5,hearing:10,aimPenalty:4,movePenalty:3,nv:true},
helmet_h70:{name:'H70精英头盔',icon:'🪖',cat:'gear',sub:'头盔',buy:4000000,sell:2000000,size:1,armor:80,maxDur:55,rarity:'red',grade:6,hearing:0,aimPenalty:4,movePenalty:3,nv:true},
helmet_gt5:{name:'GT5指挥官头盔',icon:'🪖',cat:'gear',sub:'头盔',buy:5000000,sell:2500000,size:1,armor:90,maxDur:60,rarity:'red',grade:6,hearing:20,aimPenalty:8,movePenalty:3,nv:true},
helmet_dich9:{name:'DICH-9重型头盔',icon:'🪖',cat:'gear',sub:'头盔',buy:5000000,sell:2500000,size:1,armor:85,maxDur:50,rarity:'red',grade:6,hearing:40,aimPenalty:0,movePenalty:0,nv:true},
bp_dg_sport:{name:'DG运动背包',icon:'🎒',cat:'gear',sub:'背包',buy:800,sell:400,slots:12,gridCols:3,rarity:'common',grade:1},
bp_sport:{name:'运动背包',icon:'🎒',cat:'gear',sub:'背包',buy:400,sell:200,slots:8,gridCols:2,rarity:'common',grade:1},
bp_camp:{name:'露营背包',icon:'🎒',cat:'gear',sub:'背包',buy:2000,sell:1000,slots:15,gridCols:5,rarity:'uncommon',grade:2},
bp_jungle:{name:'雨林猎手背包',icon:'🎒',cat:'gear',sub:'背包',buy:8000,sell:4000,slots:21,gridCols:7,rarity:'blue',grade:3},
bp_survival:{name:'生存战术背包',icon:'🎒',cat:'gear',sub:'背包',buy:20000,sell:10000,slots:28,gridCols:7,rarity:'purple',grade:4},
bp_gt5:{name:'GT5野战背包',icon:'🎒',cat:'gear',sub:'背包',buy:50000,sell:25000,slots:30,gridCols:6,rarity:'gold',grade:5},
bp_gto:{name:'GTO重型战术包',icon:'🎒',cat:'gear',sub:'背包',buy:150000,sell:75000,slots:45,gridCols:9,rarity:'red',grade:6},
bandage:{name:'绷带',icon:'✚',cat:'consumable',sub:'治疗',buy:300,sell:150,size:1,heal:30,rarity:'common',grade:1},
medkit:{name:'医疗包',icon:'✚',cat:'consumable',sub:'治疗',buy:1500,sell:750,size:1,heal:80,rarity:'uncommon',grade:1},
ocean_tear:{name:'海洋之泪',icon:'🔵',cat:'valuable',sub:'极品矿物',buy:null,sell:20000000,size:1,rarity:'red'},
africa_heart:{name:'非洲之心',icon:'💎',cat:'valuable',sub:'极品钻石',buy:null,sell:10000000,size:1,rarity:'red'},
respirator:{name:'复苏呼吸机',icon:'🫁',cat:'valuable',sub:'高级医疗',buy:null,sell:7110000,size:9,rarity:'red'},
disk_array:{name:'高速磁盘阵列',icon:'💾',cat:'valuable',sub:'电子设备',buy:null,sell:3310000,size:12,rarity:'red'},
flight_recorder:{name:'飞行记录仪',icon:'📡',cat:'valuable',sub:'航空设备',buy:null,sell:3070000,size:4,rarity:'red'},
crown:{name:'万金泪冠',icon:'👑',cat:'valuable',sub:'皇室珍宝',buy:null,sell:2300000,size:4,rarity:'red'},
micro_reactor:{name:'微型反应炉',icon:'☢️',cat:'valuable',sub:'能源核心',buy:null,sell:2160000,size:9,rarity:'red'},
secret_server:{name:'绝密服务器',icon:'🖥️',cat:'valuable',sub:'电子设备',buy:null,sell:2110000,size:9,rarity:'red'},
white_vase:{name:'白瓷净瓶',icon:'🏺',cat:'valuable',sub:'古董文物',buy:null,sell:1600000,size:8,rarity:'red'},
gold_crocodile:{name:'黄金鳄鱼头雕像',icon:'🐊',cat:'valuable',sub:'黄金制品',buy:null,sell:550000,size:4,rarity:'red'},
gas_canister:{name:'高能瓦斯罐',icon:'🛢️',cat:'valuable',sub:'能源物资',buy:null,sell:550000,size:4,rarity:'red'},
military_terminal:{name:'军用控制终端',icon:'⌨️',cat:'valuable',sub:'军用设备',buy:null,sell:400000,size:6,rarity:'red'},
graphics_card:{name:'显卡',icon:'🎮',cat:'valuable',sub:'电子元件',buy:null,sell:330000,size:2,rarity:'red'},
champagne:{name:'奥莉薇娅香槟',icon:'🍾',cat:'valuable',sub:'奢侈品',buy:null,sell:330000,size:2,rarity:'red'},
pure_gold_bar:{name:'万足金条',icon:'🟨',cat:'valuable',sub:'贵金属',buy:null,sell:300000,size:2,rarity:'red'},
programmable_cpu:{name:'可编程处理器',icon:'🖥️',cat:'valuable',sub:'电子元件',buy:null,sell:169800,size:2,rarity:'gold'},
hydraulic_ram:{name:'液压破门器',icon:'🛠️',cat:'valuable',sub:'战术装备',buy:null,sell:90000,size:2,rarity:'gold'},
space_badge:{name:'航天纪念徽章',icon:'🏅',cat:'valuable',sub:'纪念品',buy:null,sell:50000,size:1,rarity:'gold'},
gold_cup:{name:'金杯',icon:'🏆',cat:'valuable',sub:'黄金制品',buy:null,sell:30000,size:2,rarity:'gold'},
thermal_imager:{name:'已损坏的热像仪',icon:'📷',cat:'valuable',sub:'军用设备',buy:null,sell:47700,size:2,rarity:'purple'},
ssd:{name:'固态硬盘',icon:'💽',cat:'valuable',sub:'电子元件',buy:null,sell:34200,size:2,rarity:'purple'},
gs5_controller:{name:'GS5手柄',icon:'🎮',cat:'valuable',sub:'电子产品',buy:null,sell:29000,size:2,rarity:'purple'},
knee_joint:{name:'人工膝关节',icon:'🦴',cat:'valuable',sub:'医疗植入',buy:null,sell:12200,size:1,rarity:'purple'},
radio:{name:'收音机',icon:'📻',cat:'valuable',sub:'电子产品',buy:null,sell:12200,size:2,rarity:'purple'},
survival_kit:{name:'便携生存套组',icon:'🎒',cat:'valuable',sub:'战术装备',buy:null,sell:12100,size:2,rarity:'purple'},
encrypted_router:{name:'加密路由器',icon:'🔌',cat:'valuable',sub:'电子设备',buy:null,sell:12000,size:2,rarity:'purple'},
damaged_file:{name:'残缺的档案',icon:'📁',cat:'valuable',sub:'情报',buy:null,sell:11700,size:1,rarity:'purple'},
gingerbread:{name:'姜饼人',icon:'🍪',cat:'valuable',sub:'奢侈品',buy:null,sell:11500,size:1,rarity:'purple'},
gold_medallion:{name:'黄金饰章',icon:'🎖️',cat:'valuable',sub:'黄金制品',buy:null,sell:11000,size:1,rarity:'purple'},
coffee_machine:{name:'咖啡机',icon:'☕',cat:'valuable',sub:'家用电器',buy:null,sell:9858,size:4,rarity:'purple'},
blood_pressure_monitor:{name:'血压仪',icon:'🩺',cat:'valuable',sub:'医疗设备',buy:null,sell:9785,size:2,rarity:'purple'},
special_steel:{name:'特种钢',icon:'⛓️',cat:'valuable',sub:'工业材料',buy:null,sell:9481,size:2,rarity:'purple'},
ox_horn:{name:'牛角',icon:'🐂',cat:'valuable',sub:'自然物品',buy:null,sell:8674,size:1,rarity:'purple'},
thermometer:{name:'额温枪',icon:'🌡️',cat:'valuable',sub:'医疗设备',buy:null,sell:30200,size:1,rarity:'blue'},
solar_panel:{name:'太阳能板',icon:'🔆',cat:'valuable',sub:'电子设备',buy:null,sell:27000,size:2,rarity:'blue'},
military_canned:{name:'军用罐头',icon:'🥫',cat:'valuable',sub:'军需物资',buy:null,sell:17500,size:1,rarity:'blue'},
digital_caliper:{name:'高精数显卡尺',icon:'📏',cat:'valuable',sub:'工业工具',buy:null,sell:15700,size:1,rarity:'blue'},
portable_drill:{name:'无线便携电钻',icon:'🪛',cat:'valuable',sub:'电动工具',buy:null,sell:15700,size:2,rarity:'blue'},
low_grade_fuel:{name:'低级燃料',icon:'⛽',cat:'valuable',sub:'能源物资',buy:null,sell:14400,size:2,rarity:'blue'},
tea_bags:{name:'英式袋泡茶',icon:'🫖',cat:'valuable',sub:'生活物资',buy:null,sell:13800,size:1,rarity:'blue'},
cola:{name:'可乐',icon:'🥤',cat:'valuable',sub:'生活物资',buy:null,sell:12700,size:1,rarity:'blue'},
fried_noodles:{name:'香喷喷炒面',icon:'🍜',cat:'valuable',sub:'生活物资',buy:null,sell:12100,size:1,rarity:'blue'},
adapter:{name:'转换插座',icon:'🔌',cat:'valuable',sub:'电子配件',buy:null,sell:12100,size:1,rarity:'blue'},
webcam:{name:'摄像头',icon:'📹',cat:'valuable',sub:'电子设备',buy:null,sell:11900,size:2,rarity:'blue'},
memory_card:{name:'存储卡',icon:'💾',cat:'valuable',sub:'电子配件',buy:null,sell:7561,size:1,rarity:'blue'}
};
for(const id in ITEMS){if(ITEMS[id].sub==='头盔'){ITEMS[id].maxDur=Math.floor(ITEMS[id].maxDur*4)}}
const RED_LOWS=Object.keys(ITEMS).filter(id=>ITEMS[id].rarity==='red'&&ITEMS[id].cat==='valuable'&&ITEMS[id].sell<500000);
const RED_MIDS=Object.keys(ITEMS).filter(id=>ITEMS[id].rarity==='red'&&ITEMS[id].cat==='valuable'&&ITEMS[id].sell>=500000&&ITEMS[id].sell<1500000);
const RED_HIGHS=Object.keys(ITEMS).filter(id=>ITEMS[id].rarity==='red'&&ITEMS[id].cat==='valuable'&&ITEMS[id].sell>=1500000);
const G={money:500000,stash:{},loadout:{primary:null,secondary:'knife',armor:null,armorDur:0,helmet:null,helmetDur:0,backpack:null,consumables:[]},raid:null,selectedDiff:0,isNight:false,isMuted:false};
G.stash.pistol=1;G.stash.ammo_pistol=120;G.stash.bandage=4;G.stash.bp_dg_sport=1;G.stash.helmet_steel=1;G.stash.armor_light=1;G.stash.grenade=3;G.stash.molotov=2;G.stash.smoke_grenade=2;
G.loadout.primary='pistol';G.loadout.backpack='bp_dg_sport';G.loadout.helmet='helmet_steel';G.loadout.helmetDur=ITEMS.helmet_steel.maxDur;G.loadout.armor='armor_light';G.loadout.armorDur=ITEMS.armor_light.maxDur;
G.stash.ammo_pistol-=90;G.loadout.consumables.push({id:'ammo_pistol', count:90});
G.stash.bandage-=2;G.loadout.consumables.push({id:'bandage', count:2});
G.stash.grenade-=1;G.loadout.consumables.push({id:'grenade', count:1});
G.stash.molotov-=1;G.loadout.consumables.push({id:'molotov', count:1});
if(G.stash.ammo_pistol<=0)delete G.stash.ammo_pistol;
if(G.stash.bandage<=0)delete G.stash.bandage;
if(G.stash.grenade<=0)delete G.stash.grenade;
if(G.stash.molotov<=0)delete G.stash.molotov;
function toast(msg,type){const el=document.createElement('div');el.className='toast '+(type||'');el.textContent=msg;$('hudMsg').appendChild(el);setTimeout(()=>el.remove(),3000)}
function showHitmarker(){
const hm = $('hitmarker');
hm.classList.remove('show');
void hm.offsetWidth;
hm.classList.add('show');
}
function getLoadoutScore(){let s=0;if(G.loadout.primary)s+=(ITEMS[G.loadout.primary].grade||1)*10;if(G.loadout.secondary&&G.loadout.secondary!=='knife')s+=(ITEMS[G.loadout.secondary].grade||1)*10;if(G.loadout.armor)s+=(ITEMS[G.loadout.armor].grade||1)*5;if(G.loadout.helmet)s+=(ITEMS[G.loadout.helmet].grade||1)*5;if(G.loadout.backpack)s+=(ITEMS[G.loadout.backpack].grade||1)*5;return s;}
let currentShopCat='all';
function renderBase(){$('baseMoney').textContent=fmt(G.money);$('loadoutScore').textContent=getLoadoutScore();renderStash();renderShop();renderLoadout()}
function renderStash(){
const c=$('stashContent');c.innerHTML='';
const e=Object.entries(G.stash).filter(([k,v])=>v>0);
$('stashCount').textContent=e.length+'/50';
const slotsCount=Math.max(25,Math.ceil(e.length/5)*5);
for(let i=0;i<slotsCount;i++){
const s=document.createElement('div');s.className='slot';
if(i<e.length){
const[id,q]=e[i],it=ITEMS[id];
s.innerHTML='<div class="ic">'+it.icon+'</div><div class="qty mono">'+q+'</div>';
s.style.borderTop='2px solid '+rc(it.rarity);
s.title=it.name+'\n卖出: '+(it.sell?fmt(it.sell):'无')+'\n信息: '+it.sub;
s.dataset.id=id;
}else{s.className='slot empty'}
c.appendChild(s);
}
}
function renderShop(){
const c=$('shopContent');c.innerHTML='';
Object.entries(ITEMS).filter(([id,it])=>{
if(currentShopCat==='all')return it.buy!==null;
if(currentShopCat==='valuable')return it.cat==='valuable';
return it.cat===currentShopCat&&it.buy!==null
}).forEach(([id,it])=>{
const o=G.stash[id]||0;
const r=document.createElement('div');r.className='shop-item';
let st='';
if(it.cat==='weapon')st='伤害'+it.dmg+' 射程'+it.range+'m 弹匣'+it.mag;
else if(it.sub==='护甲')st='护甲'+it.armor+' 耐久'+it.maxDur;
else if(it.sub==='头盔')st='护甲'+it.armor+' 耐久'+it.maxDur+' 听力'+(it.hearing||0)+'%'+(it.nv?' [夜视]':'');
else if(it.sub==='背包')st='容量'+it.slots+'格';
else if(it.sub==='治疗')st='治疗'+it.heal+'HP';
else if(it.sub==='弹药')st='每份×'+it.stack;
else if(it.sub==='投掷物')st='伤害'+(it.dmg||0)+' 半径'+it.radius+'m'+(it.fuse?(' 引信'+it.fuse+'s'):'');
else if(it.cat==='valuable')st='卖出'+fmt(it.sell)+' 占'+it.size+'格';
r.innerHTML='<div class="ic" style="border-left:3px solid '+rc(it.rarity)+'">'+it.icon+'</div><div class="info"><div class="name">'+it.name+'</div><div class="stats">'+st+'</div></div><div style="display:flex;gap:6px;align-items:center">'+(it.buy?'<button class="mini-btn" data-act="buy" data-id="'+id+'" '+(G.money<it.buy?'disabled':'')+'>买入'+fmt(it.buy)+'</button>':'')+(it.sell&&o>0?'<button class="mini-btn ghost" data-act="sell" data-id="'+id+'">卖出</button>':'')+'<span style="font-size:10px;color:var(--muted)">'+(o>0?'×'+o:'')+'</span></div>';
c.appendChild(r);
});
}
function buyItem(id){
const it=ITEMS[id];
if(G.money<it.buy)return;
const currentSlots=Object.keys(G.stash).filter(k=>G.stash[k]>0).length;
if(currentSlots>=50&&!(G.stash[id]>0)){toast('仓库已满 (50/50)!','warn');return}
G.money-=it.buy;
G.stash[id]=(G.stash[id]||0)+(it.stack||1);
renderBase();
}
function sellItem(id){
const it=ITEMS[id];
if(!G.stash[id]||G.stash[id]<=0)return;
G.money+=it.sell;
G.stash[id]--;
if(G.stash[id]<=0)delete G.stash[id];
renderBase();
}
function tryEquip(id){
const it=ITEMS[id];if(!G.stash[id])return;
if(it.cat==='weapon'){
if(id==='knife'){toast('匕首已常驻','warn');return}
if(!G.loadout.primary){
G.loadout.primary=id;G.stash[id]--;if(G.stash[id]<=0)delete G.stash[id];toast('已装备至主武器1 '+it.name,'ok');
}else if(!G.loadout.secondary || G.loadout.secondary==='knife'){
G.loadout.secondary=id;G.stash[id]--;if(G.stash[id]<=0)delete G.stash[id];toast('已装备至主武器2 '+it.name,'ok');
}else{
if(G.loadout.primary)G.stash[G.loadout.primary]=(G.stash[G.loadout.primary]||0)+1;
G.loadout.primary=id;G.stash[id]--;if(G.stash[id]<=0)delete G.stash[id];toast('已替换主武器1 '+it.name,'ok');
}
}else if(it.sub==='护甲'){
if(G.loadout.armor)G.stash[G.loadout.armor]=(G.stash[G.loadout.armor]||0)+1;G.loadout.armor=id;G.loadout.armorDur=it.maxDur;G.stash[id]--;if(G.stash[id]<=0)delete G.stash[id];toast('已装备'+it.name,'ok');
}else if(it.sub==='头盔'){
if(G.loadout.helmet)G.stash[G.loadout.helmet]=(G.stash[G.loadout.helmet]||0)+1;G.loadout.helmet=id;G.loadout.helmetDur=it.maxDur;G.stash[id]--;if(G.stash[id]<=0)delete G.stash[id];toast('已装备'+it.name,'ok');
}else if(it.sub==='背包'){
if(G.loadout.backpack)G.stash[G.loadout.backpack]=(G.stash[G.loadout.backpack]||0)+1;G.loadout.backpack=id;G.stash[id]--;if(G.stash[id]<=0)delete G.stash[id];toast('已装备'+it.name,'ok');
}else if(it.cat==='consumable' || it.cat==='valuable'){
const tempRaid = { inventory: [], inventoryMax: G.loadout.backpack ? ITEMS[G.loadout.backpack].slots : 12, inventoryCols: G.loadout.backpack ? ITEMS[G.loadout.backpack].gridCols : 3 };
const oldRaid = G.raid; G.raid = tempRaid;
G.loadout.consumables.forEach(c => addItemToInv(c.id, c.count, true));
const addedCount = addItemToInv(id, G.stash[id], true);
G.raid = oldRaid;
if(addedCount > 0){
const exist = G.loadout.consumables.find(c => c.id === id);
if(exist) exist.count += addedCount;
else G.loadout.consumables.push({id, count: addedCount});
G.stash[id] -= addedCount;
if(G.stash[id] <= 0) delete G.stash[id];
toast('已放入背包 '+it.name + (addedCount>1?' x'+addedCount:''),'ok');
} else {
toast('背包容量不足','warn');
}
}else{toast('无法装备','warn')}renderBase();
}
function renderLoadout(){
const lp=$('loPrimary');if(G.loadout.primary){const it=ITEMS[G.loadout.primary];lp.className='loadout-slot filled';lp.innerHTML='<div class="ic">'+it.icon+'</div><div class="nfo"><div class="nm">'+it.name+'</div><div class="sub">伤害'+it.dmg+' 射程'+it.range+'m</div></div>';$('loPrimaryTag').textContent='已装备';lp.onclick=()=>{G.stash[G.loadout.primary]=(G.stash[G.loadout.primary]||0)+1;G.loadout.primary=null;renderBase()}}else{lp.className='loadout-slot';lp.innerHTML='<span style="color:var(--muted)">点击仓库装备</span>';$('loPrimaryTag').textContent='未装备'}
const ls=$('loSecondary');
if(G.loadout.secondary && G.loadout.secondary !== 'knife'){
const it=ITEMS[G.loadout.secondary];
ls.className='loadout-slot filled';
ls.innerHTML='<div class="ic">'+it.icon+'</div><div class="nfo"><div class="nm">'+it.name+'</div><div class="sub">伤害'+it.dmg+' 射程'+it.range+'m</div></div>';
$('loSecondaryTag').textContent='已装备';
ls.onclick=()=>{G.stash[G.loadout.secondary]=(G.stash[G.loadout.secondary]||0)+1;G.loadout.secondary='knife';renderBase()};
}else{
G.loadout.secondary='knife';
const it=ITEMS.knife;
ls.className='loadout-slot filled locked';
ls.innerHTML='<div class="ic">'+it.icon+'</div><div class="nfo"><div class="nm">'+it.name+'</div><div class="sub">一刀必杀</div></div>';
$('loSecondaryTag').textContent='常驻';
ls.onclick=null;
}
const la=$('loArmor');if(G.loadout.armor){const it=ITEMS[G.loadout.armor];la.className='loadout-slot filled';la.innerHTML='<div class="ic">'+it.icon+'</div><div class="nfo"><div class="nm">'+it.name+'</div><div class="sub">耐久'+G.loadout.armorDur+'/'+it.maxDur+'</div></div>';$('loArmorTag').textContent=G.loadout.armorDur+'%';la.onclick=()=>{G.stash[G.loadout.armor]=(G.stash[G.loadout.armor]||0)+1;G.loadout.armor=null;G.loadout.armorDur=0;renderBase()}}else{la.className='loadout-slot';la.innerHTML='<span style="color:var(--muted)">点击仓库装备</span>';$('loArmorTag').textContent='未装备'}
const lh=$('loHelmet');if(G.loadout.helmet){const it=ITEMS[G.loadout.helmet];lh.className='loadout-slot filled';lh.innerHTML='<div class="ic">'+it.icon+'</div><div class="nfo"><div class="nm">'+it.name+'</div><div class="sub">耐久'+G.loadout.helmetDur+'/'+it.maxDur+' 移速-'+(it.movePenalty||0)+'%'+(it.nv?' [夜视]':'')+'</div></div>';$('loHelmetTag').textContent=G.loadout.helmetDur+'%';lh.onclick=()=>{G.stash[G.loadout.helmet]=(G.stash[G.loadout.helmet]||0)+1;G.loadout.helmet=null;G.loadout.helmetDur=0;renderBase()}}else{lh.className='loadout-slot';lh.innerHTML='<span style="color:var(--muted)">点击仓库装备</span>';$('loHelmetTag').textContent='未装备'}
const lb=$('loBackpack');if(G.loadout.backpack){const it=ITEMS[G.loadout.backpack];lb.className='loadout-slot filled';lb.innerHTML='<div class="ic">'+it.icon+'</div><div class="nfo"><div class="nm">'+it.name+'</div><div class="sub">'+it.slots+'格</div></div>';$('loBackpackTag').textContent=it.slots+'格';lb.onclick=()=>{G.stash[G.loadout.backpack]=(G.stash[G.loadout.backpack]||0)+1;G.loadout.backpack=null;renderBase()}}else{lb.className='loadout-slot';lb.innerHTML='<span style="color:var(--muted)">默认12格</span>';$('loBackpackTag').textContent='默认12格'}
const maxSlots = G.loadout.backpack ? ITEMS[G.loadout.backpack].slots : 12;
const tempRaidCalc = { inventory: [], inventoryMax: maxSlots, inventoryCols: G.loadout.backpack ? ITEMS[G.loadout.backpack].gridCols : 3 };
const oldRaidCalc = G.raid; G.raid = tempRaidCalc;
G.loadout.consumables.forEach(c => addItemToInv(c.id, c.count, true));
const usedSlots = tempRaidCalc.inventory.reduce((s,i)=>s+i.w*i.h, 0);
G.raid = oldRaidCalc;
$('loBagTag').textContent = usedSlots + '/' + maxSlots;
const bagC = $('loBagContent'); bagC.innerHTML = '';
G.loadout.consumables.forEach((c, idx) => {
const it = ITEMS[c.id];
const el = document.createElement('div');
el.className = 'loadout-slot filled';
el.style.flexDirection = 'column'; el.style.alignItems = 'center'; el.style.justifyContent = 'center'; el.style.textAlign = 'center'; el.style.padding = '4px';
el.innerHTML = '<div class="ic">'+it.icon+'</div><div class="nm" style="font-size:10px">'+it.name+'</div><div style="font-size:10px;color:var(--accent)">×'+c.count+'</div>';
el.onclick = () => { G.stash[c.id] = (G.stash[c.id]||0) + c.count; G.loadout.consumables.splice(idx, 1); renderBase(); };
bagC.appendChild(el);
});
}
document.querySelectorAll('.shop-tab').forEach(t=>t.onclick=()=>{document.querySelectorAll('.shop-tab').forEach(x=>x.classList.remove('active'));t.classList.add('active');currentShopCat=t.dataset.cat;renderShop()});
document.querySelectorAll('.time-card').forEach(t=>t.onclick=()=>{document.querySelectorAll('.time-card').forEach(x=>x.classList.remove('active'));t.classList.add('active');G.isNight=t.dataset.time==='night'});
$('helpBtn').onclick=()=>alert('操作说明:\n点击仓库物品装备\n商人处购买物品\nWASD移动 SHIFT奔跑 Z开镜 X扑灭 N夜视 M静音\n1主武器1 2主武器2 3投掷物 4切换投掷物 R换弹 F搜刮/拉闸 H治疗 T修甲 G修盔\n投掷操作: 按3进入瞄准状态, 鼠标左键按住蓄力, 松开投出!\n拖拽时按 R 或 方向键 旋转物品');
$('shopContent').addEventListener('click', e => {
const btn = e.target.closest('button[data-act]');
if(!btn) return;
if(btn.dataset.act === 'buy') buyItem(btn.dataset.id);
else if(btn.dataset.act === 'sell') sellItem(btn.dataset.id);
});
$('stashContent').addEventListener('click', e => {
const slot = e.target.closest('.slot');
if(!slot || slot.classList.contains('empty')) return;
const id = slot.dataset.id;
if(id) tryEquip(id);
});
let scene,camera,renderer,clock,enemies=[],lootCrates=[],extractPoint=null,particles=[],obstacles=[],fires=[],smokes=[],worldSize=120,ambientLight=null,sunLight=null;
const input={keys:{},mouseDown:false,yaw:0,pitch:0,locked:false};
let isReloading=false,isAiming=false,audioCtx=null,isThrowing=false,isCharging=false,throwStartTime=0,throwFuse=0,throwTrajectoryLine=null,currentThrowable='grenade';
function initAudio(){if(!audioCtx){try{audioCtx=new(window.AudioContext||window.webkitAudioContext)()}catch(e){}}}
function playSnd(){if(!audioCtx||G.isMuted)return;const w=G.raid.weapon;if(w&&w.ammoType==='melee'){const o=audioCtx.createOscillator(),g=audioCtx.createGain();o.connect(g);g.connect(audioCtx.destination);o.type='sawtooth';o.frequency.setValueAtTime(800,audioCtx.currentTime);o.frequency.exponentialRampToValueAtTime(200,audioCtx.currentTime+0.1);g.gain.setValueAtTime(0.2,audioCtx.currentTime);g.gain.exponentialRampToValueAtTime(0.01,audioCtx.currentTime+0.12);o.start();o.stop(audioCtx.currentTime+0.12);return}const vol=0.4,bs=audioCtx.sampleRate*0.2,buf=audioCtx.createBuffer(1,bs,audioCtx.sampleRate),d=buf.getChannelData(0);for(let i=0;i<bs;i++)d[i]=(Math.random()*2-1)*Math.pow(1-i/bs,3);const n=audioCtx.createBufferSource();n.buffer=buf;const ng=audioCtx.createGain();ng.gain.setValueAtTime(vol,audioCtx.currentTime);n.connect(ng);ng.connect(audioCtx.destination);n.start();const o=audioCtx.createOscillator(),og=audioCtx.createGain();o.connect(og);og.connect(audioCtx.destination);o.type='sine';o.frequency.setValueAtTime(80,audioCtx.currentTime);o.frequency.exponentialRampToValueAtTime(20,audioCtx.currentTime+0.15);og.gain.setValueAtTime(vol*1.5,audioCtx.currentTime);og.gain.exponentialRampToValueAtTime(0.01,audioCtx.currentTime+0.15);o.start();o.stop(audioCtx.currentTime+0.15);}
function canPlace(inv, it, x, y, ig){
const c=G.raid.inventoryCols, r=Math.ceil(G.raid.inventoryMax/c);
if(x<0||y<0||x+it.w>c||y+it.h>r) return false;
for(let dy=0; dy<it.h; dy++){
for(let dx=0; dx<it.w; dx++){
const cx=x+dx, cy=y+dy;
if(cy*c+cx>=G.raid.inventoryMax) return false;
for(const o of inv){
if(o===ig) continue;
if(cx>=o.x && cx<o.x+o.w && cy>=o.y && cy<o.y+o.h) return false;
}
}
}
return true;
}
function findSlot(inv, it){
const c=G.raid.inventoryCols, r=Math.ceil(G.raid.inventoryMax/c);
for(let y=0;y<r;y++) for(let x=0;x<c;x++) if(canPlace(inv, it, x, y, it)){it.x=x;it.y=y;return true}
if(it.w !== it.h){
const tempW = it.w, tempH = it.h; it.w = tempH; it.h = tempW;
for(let y=0;y<r;y++) for(let x=0;x<c;x++) if(canPlace(inv, it, x, y, it)){it.x=x;it.y=y;return true}
it.w = tempW; it.h = tempH;
}
return false;
}
function addItemToInv(id, count, silent = false){
const it = ITEMS[id];
const addedItems = [];
let rem = count;
if(it.sub === '弹药'){
const stack = it.stack;
for(let i=0; i<G.raid.inventory.length && rem > 0; i++){
let item = G.raid.inventory[i];
if(item.id === id && item.count < stack){
const canAdd = Math.min(rem, stack - item.count);
item.count += canAdd;
rem -= canAdd;
}
}
while(rem > 0){
const c = Math.min(rem, stack);
const ni = {id, w:1, h:1, x:0, y:0, count:c};
if(findSlot(G.raid.inventory, ni)){
G.raid.inventory.push(ni);
addedItems.push(ni);
rem -= c;
} else {
break;
}
}
if(rem > 0 && count === rem){
if(!silent) toast('空间不足或形状不匹配','warn');
} else if (rem > 0){
if(!silent) toast('空间不足,部分弹药未能放入','warn');
}
return count - rem;
} else {
for(let i=0; i<count; i++){
const ni = {id, w:sizeToWH(it.size).w, h:sizeToWH(it.size).h, x:0, y:0, count:1};
if(findSlot(G.raid.inventory, ni)){
G.raid.inventory.push(ni);
addedItems.push(ni);
} else {
break;
}
}
if(addedItems.length === 0){
if(!silent) toast('空间不足或物品形状无法匹配','warn');
} else if (addedItems.length < count){
if(!silent) toast('空间不足,已放入'+addedItems.length+'个','warn');
}
return addedItems.length;
}
}
function initRaid(){
const d=DIFFS[G.selectedDiff];G.raidDiff=d;worldSize=MAP_CFG.worldSize;
const l=JSON.parse(JSON.stringify(G.loadout));
const wp=l.primary?ITEMS[l.primary]:null;const helm=l.helmet?ITEMS[l.helmet]:null;const arm=l.armor?ITEMS[l.armor]:null;
const sp=l.secondary&&l.secondary!=='knife'?ITEMS[l.secondary]:null;
G.raid={health:100,armorId:l.armor||null,armor:arm?arm.armor:0,armorDur:l.armorDur||0,maxArmorDur:arm?arm.maxDur:0,helmetId:l.helmet||null,helmetDur:l.helmetDur||0,maxHelmetDur:helm?helm.maxDur:0,movePenalty:helm?(helm.movePenalty||0):0,stamina:100,primary:wp?{...wp,id:l.primary,ammoInMag:0}:null,secondary:sp?{...sp,id:l.secondary,ammoInMag:0}:{...ITEMS.knife,id:'knife'},activeSlot:wp?'primary':'secondary',inventory:[],inventoryMax:l.backpack?ITEMS[l.backpack].slots:12,inventoryCols:l.backpack?ITEMS[l.backpack].gridCols:3,kills:0,lootValue:0,extracting:false,extractState:'none',raidTime:600,alive:true,state:'intro',introTime:3,helicopter:null,rockets:[],grenades:[],airdropTriggered:false,airdropCount:d.airdropCount,burning:false,burnDur:0,burnTick:0,extinguishing:false,extinguishTimer:0,nvOn: G.isNight && helm && helm.nv ? true : false, replay:false, replayTimer:0, replayCenter:null, replayRadius:0, replayAngle:0, replayHeight:0, deathPos:null, nearLever:false, _leverHintShown:false, _extractLockedToast:false};
G.raid.weapon=G.raid.activeSlot==='primary'?G.raid.primary:G.raid.secondary;if(!G.raid.weapon){G.raid.weapon=G.raid.secondary;G.raid.activeSlot='secondary'}
l.consumables.forEach(item => {
if(addItemToInv(item.id, item.count) === 0){
toast('背包已满,'+ITEMS[item.id].name+'未能带入', 'warn');
}
});
scene=new THREE.Scene();scene.background=new THREE.Color(G.isNight?0x000000:0x87ceeb);scene.fog=new THREE.Fog(G.isNight?0x000000:0x87ceeb,G.isNight?5:100,G.isNight?15:300);
camera=new THREE.PerspectiveCamera(75,innerWidth/innerHeight,0.1,400);camera.position.set(0,30,5);
if(!renderer){renderer=new THREE.WebGLRenderer({canvas:$('game'),antialias:true});renderer.shadowMap.enabled=true}
renderer.setSize(innerWidth,innerHeight);renderer.setPixelRatio(Math.min(devicePixelRatio,1.5));
clock=new THREE.Clock();isReloading=false;isAiming=false;input.mouseDown=false;input.keys={};
buildWorld();const baseEnemyCount=6;spawnEnemies(Math.round(baseEnemyCount*d.enemyCountMul),d);spawnLoot(MAP_CFG.lootCount,d);spawnExtract(d.extractType);
$('hud').style.display='block';updateNightVision();updateHUD();toast('进入战区 ['+d.name+']','ok');
const heli=new THREE.Group();const bm=new THREE.MeshStandardMaterial({color:0x333333,roughness:0.6});heli.add(new THREE.Mesh(new THREE.BoxGeometry(3,2,8),bm));const rotor=new THREE.Mesh(new THREE.BoxGeometry(9,0.1,0.5),new THREE.MeshStandardMaterial({color:0x111111}));rotor.position.set(0,1.5,0);heli.add(rotor);heli.userData.rotor=rotor;heli.position.set(0,36,5);scene.add(heli);G.raid.helicopter=heli;animate();
}
function buildWorld(){
ambientLight=new THREE.AmbientLight(0xffffff,G.isNight?0.15:1);scene.add(ambientLight);
sunLight=new THREE.DirectionalLight(0xffffff,G.isNight?0.2:1.5);sunLight.position.set(50,80,30);sunLight.castShadow=true;sunLight.shadow.mapSize.set(1024,1024);scene.add(sunLight);
const g=new THREE.Mesh(new THREE.PlaneGeometry(worldSize*2.5,worldSize*2.5),new THREE.MeshStandardMaterial({color:MAP_CFG.groundColor,roughness:1}));g.rotation.x=-Math.PI/2;g.receiveShadow=true;scene.add(g);
const wm=new THREE.MeshStandardMaterial({color:0x7a7a6a,roughness:1});for(let i=0;i<4;i++){const w=new THREE.Mesh(new THREE.BoxGeometry(worldSize*2.5,10,1),wm);const wx=Math.sin(i*Math.PI/2)*worldSize,wz=Math.cos(i*Math.PI/2)*worldSize;w.position.set(wx,5,wz);w.rotation.y=i*Math.PI/2;scene.add(w);obstacles.push({x:wx,z:wz,w:i%2?1:worldSize*2.5,d:i%2?worldSize*2.5:1,h:10})}
const bldm=new THREE.MeshStandardMaterial({color:0x8a7a6a,roughness:1});[{x:-40,z:-30,w:24,h:12,d:18},{x:30,z:-35,w:18,h:10,d:22},{x:45,z:25,w:28,h:15,d:16},{x:-35,z:40,w:22,h:11,d:18}].forEach(b=>{const m=new THREE.Mesh(new THREE.BoxGeometry(b.w,b.h,b.d),bldm);m.position.set(b.x,b.h/2,b.z);scene.add(m);obstacles.push({x:b.x,z:b.z,w:b.w,d:b.d,h:b.h})});
const cm=new THREE.MeshStandardMaterial({color:0x9a8a7a,roughness:0.8});for(let i=0;i<40;i++){const w=1.5+Math.random()*2,h=1+Math.random()*1.5,d=1.5+Math.random()*2,x=(Math.random()-0.5)*worldSize*1.8,z=(Math.random()-0.5)*worldSize*1.8;if(Math.abs(x)<8&&Math.abs(z)<8)continue;const m=new THREE.Mesh(new THREE.BoxGeometry(w,h,d),cm);m.position.set(x,h/2,z);m.rotation.y=Math.random()*Math.PI;scene.add(m);obstacles.push({x,z,w,d,h})}
}
function spawnEnemies(n,d){for(let i=0;i<n;i++){const tid=d.enemyPool[Math.floor(Math.random()*d.enemyPool.length)],c=ET[tid];const g=new THREE.Group();const body=new THREE.Mesh(new THREE.CapsuleGeometry(0.4*c.scale,1*c.scale,4,8),new THREE.MeshStandardMaterial({color:c.color,roughness:0.8}));body.position.y=1*c.scale;g.add(body);const head=new THREE.Mesh(new THREE.SphereGeometry(0.28*c.scale,8,8),new THREE.MeshStandardMaterial({color:c.headColor,roughness:0.6}));head.position.y=1.85*c.scale;head.userData.isHead=true;g.add(head);if(c.hasShield){const s=new THREE.Mesh(new THREE.BoxGeometry(1,1.4,0.08),new THREE.MeshStandardMaterial({color:0x556677,metalness:0.7}));s.position.set(0,1,0.55);s.userData.isShield=true;g.add(s)}if(c.bodyResist){const h=new THREE.Mesh(new THREE.SphereGeometry(0.32*c.scale,8,8,0,Math.PI*2,0,Math.PI/2),new THREE.MeshStandardMaterial({color:0x333333,metalness:0.6}));h.position.y=1.88*c.scale;g.add(h)}if(c.isRocket){const r=new THREE.Mesh(new THREE.BoxGeometry(0.25,0.25,1.2),new THREE.MeshStandardMaterial({color:0x3a5a2a}));r.position.set(0.3,1.3,0.2);g.add(r)}if(c.fuelTank){const t=new THREE.Mesh(new THREE.CylinderGeometry(0.25,0.25,0.7,8),new THREE.MeshStandardMaterial({color:0xc8462f,emissive:0x661a0a,emissiveIntensity:0.3}));t.position.set(0,1.1,-0.35);t.userData.isFuelTank=true;g.add(t)}let x,z;do{x=(Math.random()-0.5)*worldSize*1.8;z=(Math.random()-0.5)*worldSize*1.8}while((Math.abs(x)<40&&Math.abs(z)<40)||checkCol(x,z,0.6));g.position.set(x,0,z);scene.add(g);enemies.push({mesh:g,pos:g.position,cfg:c,health:c.hp*d.enemyHpMul,state:'patrol',target:new THREE.Vector3(x,0,z),patrolTimer:0,fireTimer:0,lastSeen:0,alertLevel:0,senseRange:60*d.enemySenseMul,moveSpeed:c.speed*d.enemySpeedMul})}}
function getValTable(d){const m=r=>d?(r==='red'?d.redMul:(r==='gold'?d.goldMul:1)):1;return[{id:'memory_card',w:100},{id:'webcam',w:90},{id:'adapter',w:90},{id:'fried_noodles',w:85},{id:'cola',w:85},{id:'tea_bags',w:80},{id:'low_grade_fuel',w:75},{id:'portable_drill',w:70},{id:'digital_caliper',w:70},{id:'military_canned',w:65},{id:'solar_panel',w:60},{id:'thermometer',w:55},{id:'ox_horn',w:40},{id:'special_steel',w:38},{id:'blood_pressure_monitor',w:35},{id:'coffee_machine',w:35},{id:'gold_medallion',w:32},{id:'gingerbread',w:30},{id:'damaged_file',w:28},{id:'encrypted_router',w:25},{id:'survival_kit',w:22},{id:'radio',w:20},{id:'knee_joint',w:18},{id:'gs5_controller',w:15},{id:'ssd',w:12},{id:'thermal_imager',w:10},{id:'gold_cup',w:8*m('gold')},{id:'space_badge',w:6*m('gold')},{id:'hydraulic_ram',w:4*m('gold')},{id:'programmable_cpu',w:2.5*m('gold')},{id:'pure_gold_bar',w:5*m('red')},{id:'champagne',w:5*m('red')},{id:'graphics_card',w:5*m('red')},{id:'military_terminal',w:2*m('red')},{id:'gas_canister',w:2*m('red')},{id:'gold_crocodile',w:2*m('red')},{id:'white_vase',w:0.5*m('red')},{id:'secret_server',w:0.5*m('red')},{id:'micro_reactor',w:0.5*m('red')},{id:'crown',w:0.5*m('red')},{id:'flight_recorder',w:0.5*m('red')},{id:'disk_array',w:0.5*m('red')},{id:'respirator',w:0.2*m('red')},{id:'africa_heart',w:0.2*m('red')},{id:'ocean_tear',w:0.2*m('red')}]}
function spawnLoot(n,d){
const lt=[{id:'bandage',w:25},{id:'medkit',w:10},{id:'ammo_pistol',w:20},{id:'ammo_rifle',w:15},{id:'armor_repair',w:8},{id:'helmet_repair',w:5},{id:'armor_scrap',w:6},{id:'pistol_l',w:6},{id:'helmet_steel',w:3},{id:'helmet_security',w:3},{id:'helmet_lvl2',w:2},{id:'grenade',w:4},{id:'molotov',w:3},{id:'smoke_grenade',w:3}];
const tw=lt.reduce((s,l)=>s+l.w,0)+d.valWeight;
const vt=getValTable(d);
const redLowChance = d.reqScore === 0 ? 0.04 : (d.reqScore === 25 ? 0.10 : 0.18);
const redMidChance = d.reqScore === 0 ? 0.008 : (d.reqScore === 25 ? 0.025 : 0.055);
const redHighChance = d.reqScore === 0 ? 0.0005 : (d.reqScore === 25 ? 0.003 : 0.01);
for(let i=0;i<n;i++){
const g=new THREE.Group();const cm=new THREE.MeshStandardMaterial({color:0x3a4a2a,roughness:0.7});
g.add(new THREE.Mesh(new THREE.BoxGeometry(1.2,1,0.8),cm));
let x, z, attempts = 0;
do {
x = (Math.random() - 0.5) * worldSize * 1.8;
z = (Math.random() - 0.5) * worldSize * 1.8;
attempts++;
} while (
((Math.abs(x) < 4 && Math.abs(z) < 4) ||
checkCol(x, z, 1.2) ||
lootCrates.some(c => c.pos.distanceTo(new THREE.Vector3(x, 0, z)) < 3)) &&
attempts < 50
);
g.position.set(x,0,z);scene.add(g);
const items=[],ni=2+Math.floor(Math.random()*3);
for(let j=0;j<ni;j++){
let r=Math.random()*tw,f=false;
for(const l of lt){r-=l.w;if(r<=0){items.push({id:l.id, count:1});f=true;break}}
if(!f){for(const l of vt){r-=l.w;if(r<=0){items.push({id:l.id, count:1});break}}}
}
if(Math.random()<redLowChance){items.push({id:RED_LOWS[Math.floor(Math.random()*RED_LOWS.length)], count:1})}
if(Math.random()<redMidChance){items.push({id:RED_MIDS[Math.floor(Math.random()*RED_MIDS.length)], count:1})}
if(Math.random()<redHighChance){items.push({id:RED_HIGHS[Math.floor(Math.random()*RED_HIGHS.length)], count:1})}
lootCrates.push({mesh:g,pos:g.position.clone(),items,opened:false,isAirdrop:false});
}
}
function spawnAirdrop(){
const g=new THREE.Group();const m=new THREE.Mesh(new THREE.BoxGeometry(1.5,1.5,1.5),new THREE.MeshStandardMaterial({color:0xc8462f,emissive:0x441510}));m.position.y=0.75;g.add(m);const chute=new THREE.Mesh(new THREE.ConeGeometry(2,2,8),new THREE.MeshBasicMaterial({color:0xffffff,side:THREE.DoubleSide}));chute.position.y=2.5;g.add(chute);g.userData.chute=chute;
let x, z, attempts = 0;
do {
x = (Math.random() - 0.5) * worldSize * 1.8;
z = (Math.random() - 0.5) * worldSize * 1.8;
attempts++;
} while (
((Math.abs(x) < 4 && Math.abs(z) < 4) || checkCol(x, z, 1.5)) &&
attempts < 50
);
g.position.set(x,50,z);scene.add(g);
const items=[];const vt=getValTable(G.raidDiff);
const score = G.raidDiff.reqScore;
if(score === 0){
for(let j=0;j<4;j++){
if(Math.random() < 0.6) items.push({id:'programmable_cpu', count:1});
else items.push({id:vt[Math.floor(Math.random()*vt.length)].id, count:1});
}
if(Math.random() < 0.12) items.push({id:RED_LOWS[Math.floor(Math.random()*RED_LOWS.length)], count:1});
if(Math.random() < 0.02) items.push({id:RED_MIDS[Math.floor(Math.random()*RED_MIDS.length)], count:1});
} else if(score === 25){
for(let j=0;j<4;j++) items.push({id:vt[Math.floor(Math.random()*vt.length)].id, count:1});
items.push({id:RED_LOWS[Math.floor(Math.random()*RED_LOWS.length)], count:1});
if(Math.random() < 0.25) items.push({id:RED_MIDS[Math.floor(Math.random()*RED_MIDS.length)], count:1});
if(Math.random() < 0.03) items.push({id:RED_HIGHS[Math.floor(Math.random()*RED_HIGHS.length)], count:1});
} else if(score === 50){
for(let j=0;j<4;j++) items.push({id:vt[Math.floor(Math.random()*vt.length)].id, count:1});
items.push({id:RED_MIDS[Math.floor(Math.random()*RED_MIDS.length)], count:1});
if(Math.random() < 0.5) items.push({id:RED_MIDS[Math.floor(Math.random()*RED_MIDS.length)], count:1});
if(Math.random() < 0.12) items.push({id:RED_HIGHS[Math.floor(Math.random()*RED_HIGHS.length)], count:1});
if(Math.random() < 0.04) items.push({id:RED_HIGHS[Math.floor(Math.random()*RED_HIGHS.length)], count:1});
}
lootCrates.push({mesh:g,pos:new THREE.Vector3(x,0,z),items,opened:false,isAirdrop:true,falling:true});toast('侦测到空投正在落下!', 'warn')
}
function spawnExtract(type){
const x=worldSize*0.9,z=worldSize*0.9;
const g=new THREE.Group();
const b=new THREE.Mesh(new THREE.CylinderGeometry(3,3,40,16,1,true),new THREE.MeshBasicMaterial({color:type==='fixed'?0x3a9b3a:0x9b3a3a,transparent:true,opacity:0.3,side:THREE.DoubleSide}));
b.position.y=20;g.add(b);
g.position.set(x,0,z);scene.add(g);
extractPoint={pos:new THREE.Vector3(x,0,z),radius:4,type:type,activated:type==='fixed',smoke:null,lever:null};
if(type==='activate' || type==='activate_siege'){
const lx=-worldSize*0.8, lz=-worldSize*0.8;
const leverGroup=new THREE.Group();
const base=new THREE.Mesh(new THREE.BoxGeometry(0.6,1.5,0.4),new THREE.MeshStandardMaterial({color:0x553333,roughness:0.8}));
base.position.y=0.75;leverGroup.add(base);
const lever=new THREE.Mesh(new THREE.BoxGeometry(0.15,0.8,0.15),new THREE.MeshStandardMaterial({color:0xc8462f,emissive:0x661a0a,emissiveIntensity:0.5}));
lever.position.y=1.3;leverGroup.add(lever);
const light=new THREE.Mesh(new THREE.SphereGeometry(0.12,8,8),new THREE.MeshStandardMaterial({color:0xff0000,emissive:0xff0000,emissiveIntensity:1}));
light.position.set(0,1.7,0);leverGroup.add(light);
leverGroup.position.set(lx,0,lz);scene.add(leverGroup);
extractPoint.lever={pos:new THREE.Vector3(lx,0,lz),mesh:leverGroup,lever:lever,light:light,pulled:false};
}
}
function pullLever(){
if(!extractPoint||!extractPoint.lever||extractPoint.lever.pulled)return;
extractPoint.lever.pulled=true;
extractPoint.lever.lever.material.color.setHex(0x7a9b3a);
extractPoint.lever.lever.material.emissive.setHex(0x3a5a1a);
extractPoint.lever.lever.rotation.x=Math.PI/2.5;
extractPoint.lever.lever.position.y=1.0;
extractPoint.lever.light.material.color.setHex(0x00ff00);
extractPoint.lever.light.material.emissive.setHex(0x00ff00);
extractPoint.activated=true;
toast('闸门已拉下!撤离点已解锁(10秒撤离)','ok');
if(extractPoint.type==='activate_siege'){
const s=new THREE.Mesh(new THREE.CylinderGeometry(1.5,1.5,20,8),new THREE.MeshBasicMaterial({color:0xff0000,transparent:true,opacity:0.5}));
s.position.copy(extractPoint.pos).setY(10);scene.add(s);extractPoint.smoke=s;
enemies.forEach(e=>{if(e.state!=='dead'){e.alertLevel=100;e.state='attack';e.target=extractPoint.pos.clone();e.lastSeen=performance.now()}});
toast('红烟已点燃!敌人正在包围撤离点!','warn');
}
}
function checkCol(x,z,r){for(const o of obstacles){if(Math.abs(x-o.x)<o.w/2+r&&Math.abs(z-o.z)<o.d/2+r)return true}return Math.abs(x)>worldSize-1||Math.abs(z)>worldSize-1}
function hasLOS(f,t){for(const o of obstacles){if(!o.h||o.h<2)continue;const dx=t.x-f.x,dz=t.z-f.z,l2=dx*dx+dz*dz;if(l2<0.01)continue;const p=Math.max(0,Math.min(1,((o.x-f.x)*dx+(o.z-f.z)*dz)/l2)),cx=f.x+p*dx,cz=f.z+p*dz;if(Math.abs(cx-o.x)<o.w/2+0.3&&Math.abs(cz-o.z)<o.d/2+0.3)return false}for(const s of smokes){const dx=t.x-f.x,dz=t.z-f.z,l2=dx*dx+dz*dz;if(l2<0.01)continue;const p=Math.max(0,Math.min(1,((s.pos.x-f.x)*dx+(s.pos.z-f.z)*dz)/l2)),cx=f.x+p*dx,cz=f.z+p*dz,cy=p*(t.y-f.y)+f.y;if(Math.sqrt((cx-s.pos.x)**2+(cz-s.pos.z)**2+((cy-s.pos.y)**2)/2)<s.radius)return false}return true}
function switchWeapon(slot){if(isThrowing)cancelThrow();if(!G.raid[slot]){toast(slot==='primary'?'未装备主武器1':'未装备主武器2','warn');if(slot==='primary')switchWeapon('secondary');return}G.raid.activeSlot=slot;G.raid.weapon=G.raid[slot];isReloading=false;isAiming=false;$('scopeUI').style.display='none';toast('切换至 '+G.raid.weapon.name,'ok');updateHUD()}
let lastFire=0;
function spawnTracer(start,end){const geo=new THREE.BufferGeometry().setFromPoints([start,end]);const mat=new THREE.LineBasicMaterial({color:0xffee88,transparent:true,opacity:0.9});const line=new THREE.Line(geo,mat);scene.add(line);particles.push({mesh:line,life:0.08,isTracer:true})}
function fire(){if(isThrowing)return;const now=performance.now(),w=G.raid.weapon;if(!w||isReloading)return;const m=w.ammoType==='melee';if(m){if(now-lastFire<w.fireRate)return}else{if(now-lastFire<w.fireRate||w.ammoInMag<=0){if(w.ammoInMag<=0&&input.mouseDown)reload();return}}lastFire=now;if(!m)w.ammoInMag--;playSnd();const o=new THREE.Vector3();camera.getWorldPosition(o);const dir=new THREE.Vector3();camera.getWorldDirection(dir);enemies.forEach(e=>{if(e.state!=='dead'&&e.pos.distanceTo(o)<40){e.alertLevel=100;e.state='attack';e.lastSeen=now}});const sp=isAiming?0.003:0.01;dir.x+=(Math.random()-0.5)*sp;dir.y+=(Math.random()-0.5)*sp;dir.z+=(Math.random()-0.5)*sp;dir.normalize();const ray=new THREE.Raycaster(o,dir,0,w.range);const meshes=[];scene.traverse(ob=>{if(ob.isMesh&&ob.geometry&&ob.geometry.type==='BoxGeometry')meshes.push(ob)});enemies.forEach(e=>{if(e.health>0)e.mesh.traverse(ob=>{if(ob.isMesh)meshes.push(ob)})});const hits=ray.intersectObjects(meshes,false);const endPos=o.clone().add(dir.clone().multiplyScalar(w.range));if(hits.length>0){const h=hits[0];endPos.copy(h.point);let he=null;for(const e of enemies){if(e.health<=0)continue;if(e.mesh.children.includes(h.object)||h.object.parent===e.mesh){he=e;break}}if(he){const c=he.cfg;let dmg=w.dmg;if(h.object.userData.isHead)dmg*=(c.headMult||2);else if(h.object.userData.isShield){toast('盾牌格挡!','warn');spawnTracer(o,h.point);return}else if(h.object.userData.isFuelTank&&!he.fuelExploded){he.fuelExploded=true;he.health=0;for(let k=0;k<20;k++)spawnParticle(he.pos.clone().setY(1),0xff6600,0.2);if(he.pos.distanceTo(camera.position)<6)applyDmg(40, false, false, he);killEnemy(he);spawnTracer(o,h.point);showHitmarker();return}else if(c.bodyResist)dmg*=c.bodyResist;he.health-=dmg;showHitmarker();for(let k=0;k<4;k++)spawnParticle(h.point,0xc8462f,0.1);if(he.health<=0)killEnemy(he);else{he.state='attack';he.alertLevel=100;he.lastSeen=now}}}if(!m){spawnTracer(o,endPos);input.pitch+=0.01+Math.random()*0.005;input.yaw+=(Math.random()-0.5)*0.01}updateHUD()}
function spawnParticle(p,c,s){const m=new THREE.Mesh(new THREE.SphereGeometry(s||0.1,4,4),new THREE.MeshBasicMaterial({color:c}));m.position.copy(p);m.userData.vel=new THREE.Vector3((Math.random()-0.5)*3,Math.random()*2,(Math.random()-0.5)*3);scene.add(m);particles.push({mesh:m,life:0.4})}
function killEnemy(e){
G.raid.kills++;e.mesh.rotation.z=Math.PI/2;e.mesh.position.y=0.4;e.health=0;e.state='dead';
const drops=[],nd=2+Math.floor(Math.random()*3);
const dt=['pistol_l','smg_l','rifle_l','ammo_pistol','ammo_rifle','bandage','medkit','armor_repair','helmet_repair','armor_scrap','helmet_steel','helmet_security','helmet_lvl2','grenade','molotov','smoke_grenade'];
const vt=getValTable(G.raidDiff),vw=vt.reduce((s,l)=>s+l.w,0);
for(let i=0;i<nd;i++){
if(Math.random()<0.6)drops.push({id:dt[Math.floor(Math.random()*dt.length)], count:1});
else{
let r=Math.random()*vw;
for(const l of vt){r-=l.w;if(r<=0){drops.push({id:l.id, count:1});break}}
}
}
const bg=new THREE.Group();
bg.add(new THREE.Mesh(new THREE.BoxGeometry(0.8,0.3,0.5),new THREE.MeshStandardMaterial({color:0x3a2a1a})));
let dropX = e.pos.x, dropZ = e.pos.z;
if (checkCol(dropX, dropZ, 0.5)) {
for (let r = 0.5; r < 5; r += 0.5) {
for (let a = 0; a < Math.PI * 2; a += Math.PI / 4) {
const tx = e.pos.x + Math.cos(a) * r;
const tz = e.pos.z + Math.sin(a) * r;
if (!checkCol(tx, tz, 0.5)) {
dropX = tx; dropZ = tz; r = 99; break;
}
}
}
}
bg.position.set(dropX, 0, dropZ);
scene.add(bg);
lootCrates.push({mesh:bg, pos:bg.position.clone(), items:drops, opened:false});
toast('击杀'+e.cfg.name,'ok');
}
function reload(){
const w=G.raid.weapon;if(!w||w.ammoType==='melee')return;if(w.ammoInMag>=w.mag||isReloading)return;
const ammoId = 'ammo_' + w.ammoType;
const idx = G.raid.inventory.findIndex(x => x.id === ammoId);
if(idx === -1){ toast('没有备用弹药','warn'); return; }
isReloading=true;toast('换弹中...');
setTimeout(()=>{
if(!G.raid||!G.raid.alive)return;
const idx2 = G.raid.inventory.findIndex(x => x.id === ammoId);
if(idx2 !== -1){
const ammoItem = G.raid.inventory[idx2];
const need = w.mag - w.ammoInMag;
const get = Math.min(need, ammoItem.count);
w.ammoInMag += get;
ammoItem.count -= get;
if(ammoItem.count <= 0){
G.raid.inventory.splice(idx2, 1);
}
toast('换弹完成','ok');
} else {
toast('换弹失败','warn');
}
isReloading=false;
if($('lootUI').style.display==='flex') renderLoot();
updateHUD();
},1500);
}
function useHeal(){
if(G.raid.health>=100){toast('生命已满','warn');return}
const i = G.raid.inventory.findIndex(x => ITEMS[x.id].sub === '治疗');
if(i !== -1){
const it = ITEMS[G.raid.inventory[i].id];
G.raid.health = Math.min(100, G.raid.health + it.heal);
G.raid.inventory.splice(i, 1);
if($('lootUI').style.display==='flex') renderLoot();
toast('使用'+it.name+' +'+it.heal+'HP','ok');
updateHUD();
} else {
toast('没有医疗物品','warn');
}
}
function useRepair(type){
const sub = type === 'armor' ? '维修' : '头盔维修';
const id = type === 'armor' ? 'armorId' : 'helmetId';
const dur = type === 'armor' ? 'armorDur' : 'helmetDur';
const max = type === 'armor' ? 'maxArmorDur' : 'maxHelmetDur';
if(!G.raid[id]){toast('未装备'+(type==='armor'?'护甲':'头盔'),'warn');return}
if(G.raid[dur]>=G.raid[max]){toast((type==='armor'?'护甲':'头盔')+'耐久已满','warn');return}
const i = G.raid.inventory.findIndex(x => ITEMS[x.id].sub === sub);
if(i !== -1){
const it = ITEMS[G.raid.inventory[i].id];
G.raid[dur] = Math.min(G.raid[max], G.raid[dur] + it.repair);
G.raid.inventory.splice(i, 1);
if($('lootUI').style.display==='flex') renderLoot();
toast((type==='armor'?'护甲':'头盔')+'维修+'+it.repair,'ok');
updateHUD();
} else {
toast('没有'+(type==='armor'?'护甲':'头盔')+'维修包','warn');
}
}
function startThrow(){
if(isThrowing){ cancelThrow(); return; }
const idx = G.raid.inventory.findIndex(x => x.id === currentThrowable);
if(idx === -1){
const types = ['grenade', 'molotov', 'smoke_grenade'];
let found = false;
for(let i=0; i<types.length; i++){
if(G.raid.inventory.some(x => x.id === types[i])){
currentThrowable = types[i];
found = true;
break;
}
}
if(!found){ toast('没有可用的投掷物','warn'); return; }
}
isThrowing = true; isCharging = false; throwFuse = 0;
$('throwUI').style.display = 'block';
const it = ITEMS[currentThrowable];
$('throwIcon').textContent = it.icon;
$('throwTimer').textContent = '';
if(!throwTrajectoryLine){
const mat = new THREE.LineDashedMaterial({color:0xffff00, dashSize:0.5, gapSize:0.3, transparent:true, opacity:0.8});
const geo = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(), new THREE.Vector3()]);
throwTrajectoryLine = new THREE.Line(geo, mat); scene.add(throwTrajectoryLine);
}
throwTrajectoryLine.visible = true;
toast('准备投掷 '+it.name, 'ok');
}
function cancelThrow(){ isThrowing = false; isCharging = false; throwFuse = 0; $('throwUI').style.display = 'none'; if(throwTrajectoryLine) throwTrajectoryLine.visible = false; }
function startCharge(){ if(!isThrowing || isCharging) return; isCharging = true; throwStartTime = performance.now(); const it = ITEMS[currentThrowable]; if(it && it.fuse){ throwFuse = it.fuse; } else { throwFuse = 0; } }
function releaseThrow(){
if(!isThrowing || !isCharging) return; isCharging = false;
const idx = G.raid.inventory.findIndex(x => x.id === currentThrowable);
if(idx !== -1){
G.raid.inventory.splice(idx, 1);
if($('lootUI').style.display==='flex') renderLoot();
updateHUD();
const o = new THREE.Vector3(); camera.getWorldPosition(o);
const dir = new THREE.Vector3(); camera.getWorldDirection(dir);
const start = o.clone().add(dir.clone().multiplyScalar(1)); start.y -= 0.5;
const power = Math.min(1, (performance.now() - throwStartTime) / 1500);
const speed = 10 + power * 20;
const g = new THREE.Mesh(new THREE.SphereGeometry(0.15, 8, 8), new THREE.MeshStandardMaterial({color:0x3a5a2a}));
g.position.copy(start); scene.add(g);
const vel = dir.clone().multiplyScalar(speed); vel.y += 5;
G.raid.grenades.push({mesh:g, pos:g.position, vel:vel, timer:0, type:currentThrowable, exploded:false});
}
cancelThrow();
}
function explodeGrenade(gr){
scene.remove(gr.mesh);
const it = ITEMS[gr.type];
const exp = new THREE.Mesh(new THREE.SphereGeometry(1, 16, 16), new THREE.MeshBasicMaterial({color:0xff6600, transparent:true, opacity:0.8}));
exp.position.copy(gr.pos); scene.add(exp);
particles.push({mesh:exp, life:0.5, isExplosion:true, scale:1});
for(let k=0; k<20; k++) spawnParticle(gr.pos, 0xff4400, 0.3);
for(let k=0; k<10; k++) spawnParticle(gr.pos, 0x222222, 0.2);
if(gr.type === 'grenade'){
const radius = it.radius;
enemies.forEach(e => {
if(e.state !== 'dead'){
const d = e.pos.distanceTo(gr.pos);
if(d < radius){
if(hasLOS(gr.pos.clone().setY(0.5), e.pos.clone().setY(1))){
const dmg = it.dmg * (1 - d / radius);
e.health -= dmg; if(e.health <= 0) killEnemy(e);
}
}
}
});
const pd = camera.position.distanceTo(gr.pos);
if(pd < radius){
if(hasLOS(gr.pos.clone().setY(0.5), camera.position.clone().setY(1))){
applyDmg(it.dmg * (1 - pd / radius) * 0.5, false, false, null);
}
}
} else if(gr.type === 'molotov'){
const f = new THREE.Mesh(new THREE.CircleGeometry(it.radius, 16), new THREE.MeshBasicMaterial({color:0xff4400, transparent:true, opacity:0.6}));
f.rotation.x = -Math.PI/2; f.position.copy(gr.pos); f.position.y = 0.1; scene.add(f);
fires.push({mesh:f, pos:gr.pos.clone(), radius:it.radius, duration:it.duration, tick:0});
} else if(gr.type === 'smoke_grenade'){
const s = new THREE.Mesh(new THREE.SphereGeometry(it.radius, 16, 16), new THREE.MeshBasicMaterial({color:0xaaaaaa, transparent:true, opacity:0.6}));
s.position.copy(gr.pos); s.position.y = 1.5; scene.add(s);
smokes.push({mesh:s, pos:s.position.clone(), radius:it.radius, duration:it.duration});
}
$('damageVignette').style.boxShadow = 'inset 0 0 200px rgba(255,100,0,0.8)';
setTimeout(() => $('damageVignette').style.boxShadow = 'inset 0 0 200px rgba(200,70,47,0)', 150);
}
function toggleThrowable(){
const types = ['grenade', 'molotov', 'smoke_grenade'];
for(let i=0; i<3; i++){
const next = types[(types.indexOf(currentThrowable)+1)%3];
if(G.raid.inventory.some(x => x.id === next)){
currentThrowable = next; toast('切换为 '+ITEMS[next].name, 'ok'); return;
}
}
toast('没有其他投掷物','warn');
}
function toggleExtinguish(){
if(!G.raid.burning){toast('身上没有火苗','warn');return}
if(!G.raid.extinguishing){G.raid.extinguishing=true;G.raid.extinguishTimer=3;$('burnUI').style.display='block';$('burnStatus').textContent='扑灭中 (保持静止)';toast('正在扑灭火苗...','ok')}else{G.raid.extinguishing=false;$('burnUI').style.display='none';toast('取消扑灭','warn')}
}
function toggleMute(){ G.isMuted = !G.isMuted; toast(G.isMuted?'已静音':'已开启声音','ok'); }
function toggleNV(){
if(!G.raid.helmetId || !ITEMS[G.raid.helmetId].nv){toast('当前头盔不支持夜视仪','warn');return}
G.raid.nvOn = !G.raid.nvOn; updateNightVision(); toast(G.raid.nvOn?'夜视仪开启':'夜视仪关闭','ok');
}
function updateNightVision(){
const nvUI = $('nightVisionUI');
if(G.isNight && G.raid && G.raid.alive){
if(G.raid.nvOn){
nvUI.style.background = 'rgba(0, 60, 0, 0.2)'; nvUI.style.mixBlendMode = 'screen';
if(ambientLight) ambientLight.intensity = 1.0; if(sunLight) sunLight.intensity = 1.5;
if(scene.fog){ scene.fog.near = 100; scene.fog.far = 250; }
} else {
nvUI.style.background = 'rgba(0, 0, 0, 0.7)'; nvUI.style.mixBlendMode = 'multiply';
if(ambientLight) ambientLight.intensity = 0.15; if(sunLight) sunLight.intensity = 0.2;
if(scene.fog){ scene.fog.near = 5; scene.fog.far = 15; }
}
} else {
nvUI.style.background = 'rgba(0,0,0,0)'; nvUI.style.mixBlendMode = 'normal';
if(ambientLight) ambientLight.intensity = 1; if(sunLight) sunLight.intensity = 1.5;
if(scene.fog){ scene.fog.near = 100; scene.fog.far = 300; }
}
}
function toggleLoot(){
if(isThrowing)cancelThrow();
if(G.raid.nearLever&&extractPoint&&extractPoint.lever&&!extractPoint.lever.pulled){ pullLever();return; }
if(G.raid.nearLoot){ $('lootUI').style.display==='flex'?closeLoot():openLoot(G.raid.nearLoot) }
}
function openLoot(c){
G.raid.activeCrate=c;c.opened = true;
if(c.mesh && c.mesh.children[0]){c.mesh.children[0].material.color.setHex(0x2a2a2a);if(c.mesh.children[0].material.emissive) c.mesh.children[0].material.emissive.setHex(0x000000);c.mesh.children[0].material.needsUpdate = true;}
renderLoot();$('lootUI').style.display='flex';document.exitPointerLock();input.locked=false;
}
function closeLoot(){$('lootUI').style.display='none';G.raid.activeCrate=null;$('pauseOverlay').style.display='flex';$('pauseText').textContent='搜索结束'}
$('lootClose').onclick=closeLoot;
function getUsed(){return G.raid.inventory.reduce((s,i)=>s+i.w*i.h,0)}
const tooltip=$('tooltip');
document.addEventListener('mousemove',e=>{
if(e.target && e.target.classList && e.target.classList.contains('inv-item-grid') && !dragData.active){
tooltip.style.display='block';tooltip.style.left=(e.clientX+15)+'px';tooltip.style.top=(e.clientY+15)+'px';tooltip.innerHTML=e.target.dataset.tip;
tooltip.classList.toggle('red', e.target.dataset.rarity === 'red');
} else {tooltip.style.display='none';tooltip.classList.remove('red');}
});
const dragData={active:false,type:null,idx:-1,item:null,ghost:null,startX:0,startY:0};
const ghost=$('dragGhost');
function startDrag(e,type,idx){
e.preventDefault(); dragData.startX = e.clientX; dragData.startY = e.clientY;
let itemData,w,h;
if(type==='inv'){ itemData={...G.raid.inventory[idx]};w=itemData.w;h=itemData.h; e.currentTarget.classList.add('dragging'); }
else{ const it=ITEMS[G.raid.activeCrate.items[idx].id];let s=sizeToWH(it.size);w=s.w;h=s.h;itemData={id:it.id,w,h,x:0,y:0,count:1}; }
dragData.active=true;dragData.type=type;dragData.idx=idx;dragData.item=itemData;
ghost.style.display='flex';ghost.style.width=(w*40)+'px';ghost.style.height=(h*40)+'px';
ghost.innerHTML='<div class="ic">'+ITEMS[itemData.id].icon+'</div><div class="nm">'+ITEMS[itemData.id].name+'</div>';
moveGhost(e);
}
function moveGhost(e){ghost.style.left=(e.clientX+5)+'px';ghost.style.top=(e.clientY+5)+'px'}
document.addEventListener('mousemove',e=>{if(!dragData.active)return;moveGhost(e)});
document.addEventListener('mouseup',e=>{
if(dragData.active){
const moved = Math.abs(e.clientX - dragData.startX) > 5 || Math.abs(e.clientY - dragData.startY) > 5;
if(moved){
const invRect=$('lootInventory').getBoundingClientRect();
if(e.clientX>=invRect.left&&e.clientX<=invRect.right&&e.clientY>=invRect.top&&e.clientY<=invRect.bottom){
const cellW=invRect.width/G.raid.inventoryCols;const cellH=invRect.height/Math.ceil(G.raid.inventoryMax/G.raid.inventoryCols);
let gx=Math.floor((e.clientX-invRect.left)/cellW);let gy=Math.floor((e.clientY-invRect.top)/cellH);
let tx=gx-Math.floor(dragData.item.w/2);let ty=gy-Math.floor(dragData.item.h/2);
tx=Math.max(0,tx);ty=Math.max(0,ty);
if(dragData.type==='inv'){
if(canPlace(G.raid.inventory, dragData.item, tx, ty, G.raid.inventory[dragData.idx])){G.raid.inventory[dragData.idx].x=tx;G.raid.inventory[dragData.idx].y=ty;G.raid.inventory[dragData.idx].w=dragData.item.w;G.raid.inventory[dragData.idx].h=dragData.item.h;renderLoot();updateHUD();}
else{toast('此处无法放置或空间不足','warn');renderLoot();}
}else{
const tempItem={...dragData.item,x:tx,y:ty};
if(canPlace(G.raid.inventory, tempItem, tx, ty, null)){
G.raid.inventory.push(tempItem);
G.raid.lootValue += (ITEMS[tempItem.id].sell||0) * (tempItem.count||1);
G.raid.activeCrate.items.splice(dragData.idx,1);
renderLoot();updateHUD();
}
else{toast('此处无法放置或空间不足','warn');renderLoot();}
}
} else { if(dragData.type==='inv') renderLoot(); }
} else {
if(dragData.type === 'container'){
const idx = dragData.idx; const data = G.raid.activeCrate.items[idx];
const id = data.id; const count = data.count || 1; const it = ITEMS[id]; const c = G.raid.activeCrate;
const added = addItemToInv(id, count);
if(added > 0){
if(it.sub !== '弹药'){ G.raid.lootValue += (it.sell||0) * added; }
if(data.count > added){ data.count -= added; } else { c.items.splice(idx,1); }
renderLoot();updateHUD();
}
} else if(dragData.type === 'inv') { renderLoot(); }
}
dragData.active=false;ghost.style.display='none';dragData.item=null;
}
});
function renderLoot(){
const c=G.raid.activeCrate;if(!c)return;let tv=0;G.raid.inventory.forEach(i=>{if(ITEMS[i.id].sell)tv+=ITEMS[i.id].sell*i.count});$('lootTotalValue').textContent='背包价值:'+fmt(tv);
const cc=$('lootContainer');cc.innerHTML='';
c.items.forEach((data, idx)=>{
const id = data.id; const count = data.count || 1;
const it=ITEMS[id],el=document.createElement('div');el.className='inv-item-grid';
let {w,h}=sizeToWH(it.size);
el.style.gridColumn='span '+w;el.style.gridRow='span '+h;el.style.aspectRatio=w+' / '+h;el.style.borderColor=rc(it.rarity);
let tipHtml = `<b style="color:${rc(it.rarity)}">${it.name}</b><br>价格: ${it.sell?fmt(it.sell):'不可出售'}<br>体积: ${w}x${h} (${w*h}格)<br>类型: ${it.sub}`;
if(it.cat === 'weapon' && id !== 'knife') tipHtml += `<br>射程: ${it.range}m`;
if(it.rarity === 'red' && it.cat === 'valuable'){tipHtml = `🔴 <b style="color:${rc(it.rarity)}; text-shadow: 0 0 5px ${rc(it.rarity)}">${it.name}</b> 🔴<br><span style="color:#c8462f; font-weight:700;">极品物资!</span><br>价格: ${fmt(it.sell)}<br>体积: ${w}x${h} (${w*h}格)<br>类型: ${it.sub}`;el.dataset.isRedVal = '1';}
el.dataset.tip = tipHtml;el.dataset.rarity = it.rarity;
let equipBtn = (it.cat==='weapon'&&id!=='knife'||it.sub==='护甲'||it.sub==='头盔'||it.sub==='背包') ? '<div class="btns"><button class="btn-equip">装</button></div>' : '';
el.innerHTML='<div class="ic">'+it.icon+'</div><div class="nm">'+it.name+'</div>' + (count>1?'<div class="qty mono">×'+count+'</div>':'') + equipBtn;
el.onmousedown=e=>{ if(e.target.closest('.btns')) return; startDrag(e,'container',idx) };
const equipBtnEl = el.querySelector('.btn-equip');
if(equipBtnEl){
equipBtnEl.onclick = (ev) => {
ev.stopPropagation(); const it = ITEMS[id]; const c = G.raid.activeCrate;
if(it.cat==='weapon'&&id!=='knife'){
let targetSlot = 'primary';
if(!G.raid.primary){ targetSlot = 'primary'; }
else if(!G.raid.secondary || G.raid.secondary.id === 'knife'){ targetSlot = 'secondary'; }
else { if(G.raid.primary){addItemToInv(G.raid.primary.id,1);G.raid.lootValue+=ITEMS[G.raid.primary.id].sell||0;} targetSlot = 'primary'; }
G.raid[targetSlot] = {...it, id, ammoInMag: it.mag};
G.raid.activeSlot = targetSlot; G.raid.weapon = G.raid[targetSlot];
c.items.splice(idx,1);renderLoot();updateHUD();
} else if(it.sub==='护甲'){
if(G.raid.armorId)addItemToInv(G.raid.armorId,1);G.raid.armorId=id;G.raid.armor=it.armor;G.raid.maxArmorDur=it.maxDur;G.raid.armorDur=it.maxDur;c.items.splice(idx,1);renderLoot();updateHUD();
} else if(it.sub==='头盔'){
if(G.raid.helmetId)addItemToInv(G.raid.helmetId,1);G.raid.helmetId=id;G.raid.maxHelmetDur=it.maxDur;G.raid.helmetDur=it.maxDur;G.raid.movePenalty=it.movePenalty||0;
if(G.isNight){if(it.nv){G.raid.nvOn=true;toast('已自动开启夜视仪','ok')}else{G.raid.nvOn=false}updateNightVision();}
c.items.splice(idx,1);renderLoot();updateHUD();
}
};
}
cc.appendChild(el);
});
const ic=$('lootInventory');const cols=G.raid.inventoryCols,rows=Math.ceil(G.raid.inventoryMax/cols);
ic.style.gridTemplateColumns='repeat('+cols+',1fr)';ic.style.gridTemplateRows='repeat('+rows+',1fr)';ic.style.gap='4px';ic.innerHTML='';
for(let i=0;i<G.raid.inventoryMax;i++){const e=document.createElement('div');e.className='empty-slot-grid';ic.appendChild(e)}
G.raid.inventory.forEach((item,idx)=>{
const it=ITEMS[item.id],el=document.createElement('div');el.className='inv-item-grid';
if(item.dragging) el.classList.add('dragging');
el.style.gridColumn=(item.x+1)+' / span '+item.w;el.style.gridRow=(item.y+1)+' / span '+item.h;el.style.borderColor=rc(it.rarity);
let tipHtml = `<b style="color:${rc(it.rarity)}">${it.name}</b><br>价格: ${it.sell?fmt(it.sell):'不可出售'}<br>体积: ${item.w}x${item.h}<br>类型: ${it.sub}`;
if(it.rarity === 'red' && it.cat === 'valuable'){tipHtml = `🔴 <b style="color:${rc(it.rarity)}; text-shadow: 0 0 5px ${rc(it.rarity)}">${it.name}</b> 🔴<br><span style="color:#c8462f; font-weight:700;">极品物资!</span><br>价格: ${fmt(it.sell)}<br>体积: ${item.w}x${item.h}<br>类型: ${it.sub}`;}
el.dataset.tip = tipHtml;el.dataset.rarity = it.rarity;
let btns='<div class="btns">';
if(it.sub==='维修'||it.sub==='头盔维修'||it.sub==='治疗'||it.sub==='投掷物')btns+='<button class="btn-use">用</button>';
btns+='<button class="btn-drop">丢</button></div>';
el.innerHTML='<div class="ic">'+it.icon+'</div><div class="nm">'+it.name+'</div>'+(item.count>1?'<div class="qty mono">×'+item.count+'</div>':'')+btns;
el.onmousedown=e=>{ if(e.target.closest('.btns')) return; startDrag(e,'inv',idx) };
const useBtn = el.querySelector('.btn-use');
if(useBtn){
useBtn.onclick = (ev) => {
ev.stopPropagation(); const it = ITEMS[item.id];
if(it.sub==='维修') useRepair('armor');
else if(it.sub==='头盔维修') useRepair('helmet');
else if(it.sub==='治疗') useHeal();
else if(it.sub==='投掷物'){ currentThrowable=item.id; toast('准备投掷 '+it.name,'ok'); }
};
}
const dropBtn = el.querySelector('.btn-drop');
if(dropBtn){ dropBtn.onclick = (ev) => { ev.stopPropagation(); dropItem(idx); }; }
ic.appendChild(el);
});
}
function dropItem(idx){
const item=G.raid.inventory[idx];if(!item)return;const it=ITEMS[item.id];
G.raid.inventory.splice(idx,1);
G.raid.lootValue -= (it.sell || 0) * (item.count || 1);
const col=parseInt(rc(it.rarity).slice(1),16);
const g=new THREE.Group();const box=new THREE.Mesh(new THREE.BoxGeometry(0.5,0.3,0.5),new THREE.MeshStandardMaterial({color:col,roughness:0.7,emissive:col,emissiveIntensity:0.15}));box.position.y=0.15;g.add(box);
const fx=-Math.sin(input.yaw),fz=-Math.cos(input.yaw);let px=camera.position.x+fx*0.8,pz=camera.position.z+fz*0.8;
if(checkCol(px,pz,0.3)){px=camera.position.x;pz=camera.position.z}
g.position.set(px,0,pz);scene.add(g);
lootCrates.push({mesh:g,pos:g.position.clone(),items:[{id:item.id, count:item.count||1}],opened:false,isAirdrop:false});
renderLoot();updateHUD();toast('已丢弃 '+it.name,'warn');
}
function updateHUD(){
$('hpFill').style.width=G.raid.health+'%';$('hpVal').textContent=Math.ceil(G.raid.health);
$('stFill').style.width=G.raid.stamina+'%';$('stVal').textContent=Math.ceil(G.raid.stamina);
$('invFill').style.width=(getUsed()/G.raid.inventoryMax*100)+'%';$('invVal').textContent=getUsed()+'/'+G.raid.inventoryMax;
if(G.raid.weapon){
const m=G.raid.weapon.ammoType==='melee';
const rangeInfo = m ? '' : ` <span style="font-size:10px;color:var(--muted)">射程:${G.raid.weapon.range}m</span>`;
$('wpnName').innerHTML=G.raid.weapon.name+(isAiming?' [开镜]':'')+rangeInfo;
let reserveAmmo = 0;
if(!m){
const ammoId = 'ammo_' + G.raid.weapon.ammoType;
G.raid.inventory.forEach(x => { if(x.id === ammoId) reserveAmmo += x.count; });
}
$('wpnAmmoCur').textContent=m?'∞':G.raid.weapon.ammoInMag;
$('wpnAmmoMax').textContent=m?'∞':reserveAmmo;
}
const t=Math.max(0,G.raid.raidTime);$('hudTimer').textContent=Math.floor(t/60)+':'+Math.floor(t%60).toString().padStart(2,'0');
if(G.raid.helmetId){const it=ITEMS[G.raid.helmetId];$('hudHelmetIcon').textContent=it.icon;$('hudHelmetRoman').textContent=toRoman(it.grade);$('hudHelmetDur').textContent=G.raid.helmetDur+'/'+G.raid.maxHelmetDur;$('hudHelmetDmg').style.height=(100 - G.raid.helmetDur/G.raid.maxHelmetDur*100)+'%'}else{$('hudHelmetIcon').textContent='—';$('hudHelmetRoman').textContent='-';$('hudHelmetDur').textContent='0/0';$('hudHelmetDmg').style.height='0%'}
if(G.raid.armorId){const it=ITEMS[G.raid.armorId];$('hudArmorIcon').textContent=it.icon;$('hudArmorRoman').textContent=toRoman(it.grade);$('hudArmorDur').textContent=G.raid.armorDur+'/'+G.raid.maxArmorDur;$('hudArmorDmg').style.height=(100 - G.raid.armorDur/G.raid.maxArmorDur*100)+'%'}else{$('hudArmorIcon').textContent='—';$('hudArmorRoman').textContent='-';$('hudArmorDur').textContent='0/0';$('hudArmorDmg').style.height='0%'}
}
function updateEnemies(dt){const p=camera.position;for(const e of enemies){if(e.state==='dead')continue;const c=e.cfg,d=e.pos.distanceTo(p),see=d<e.senseRange&&hasLOS(e.pos,p);if(see){e.alertLevel=100;e.lastSeen=performance.now();if(e.state!=='attack')e.state='attack'}else if(performance.now()-e.lastSeen>5000){e.alertLevel=Math.max(0,e.alertLevel-dt*30);if(e.alertLevel<=0)e.state='patrol'}if(e.state==='patrol'){e.patrolTimer-=dt;if(e.patrolTimer<=0){e.patrolTimer=3+Math.random()*4;e.target.set(e.pos.x+(Math.random()-0.5)*40,0,e.pos.z+(Math.random()-0.5)*40)}moveTo(e,e.target,dt,e.moveSpeed)}else if(e.state==='attack'){if(c.keepDistance){if(d<c.keepDistance-5)moveTo(e,{x:e.pos.x*2-p.x,z:e.pos.z*2-p.z},dt,e.moveSpeed);else if(d>c.keepDistance+10)moveTo(e,p,dt,e.moveSpeed)}else if(d>18)moveTo(e,p,dt,e.moveSpeed);else if(d<8&&!c.isFlame)moveTo(e,{x:e.pos.x*2-p.x,z:e.pos.z*2-p.z},dt,e.moveSpeed);if(c.isFlame&&d>3)moveTo(e,p,dt,e.moveSpeed);e.mesh.rotation.y=Math.atan2(p.x-e.pos.x,p.z-e.pos.z);e.fireTimer-=dt*1000;if(e.fireTimer<=0&&d<c.range&&see){e.fireTimer=c.fireRate+200+Math.random()*300;enemyFire(e)}else if(e.fireTimer<=0)e.fireTimer=500}}}
function moveTo(e,t,dt,s){const dx=t.x-e.pos.x,dz=t.z-e.pos.z,d=Math.sqrt(dx*dx+dz*dz);if(d<0.5)return;const nx=e.pos.x+dx/d*s*dt,nz=e.pos.z+dz/d*s*dt;if(!checkCol(nx,e.pos.z,0.5))e.pos.x=nx;if(!checkCol(e.pos.x,nz,0.5))e.pos.z=nz}
function enemyFire(e){
const c=e.cfg, d=e.pos.distanceTo(camera.position), m=G.raidDiff.enemyDmgMul;
if(!hasLOS(e.pos.clone().setY(1.3), camera.position.clone().setY(1.3))){ e.fireTimer = 300; return; }
if(c.isRocket){
const s=e.pos.clone().setY(1.3); const r=new THREE.Mesh(new THREE.SphereGeometry(0.15,6,6),new THREE.MeshBasicMaterial({color:0xff4400}));
r.position.copy(s); const dir=new THREE.Vector3().subVectors(camera.position,s).normalize();
r.userData={vel:dir.multiplyScalar(30),dmg:c.dmg*m, shooter:e}; scene.add(r); G.raid.rockets.push({mesh:r,life:3}); return;
}
if(c.isFlame){
if(d<c.range){
applyDmg(c.dmg*m, false, true, e); G.raid.burning=true;G.raid.burnDur=6;G.raid.burnTick=0.5;
if(!G.raid.extinguishing)toast('你着火了!按 X 扑灭','warn');
for(let i=0;i<5;i++)spawnParticle(camera.position.clone().add(new THREE.Vector3((Math.random()-0.5)*1,(Math.random()-0.5)*1,(Math.random()-0.5)*1)),0xff5500,0.2);
} return;
}
if(Math.random()<Math.max(0.2,0.7-d*0.012)){
const isHead = Math.random() < 0.35; let dmg = c.dmg * m; if(c.bodyResist) dmg *= 0.8;
applyDmg(dmg, isHead, false, e); if(isHead) toast('⚠ 爆头!','warn');
}
}
function updateRockets(dt){for(let i=G.raid.rockets.length-1;i>=0;i--){const r=G.raid.rockets[i];r.mesh.position.add(r.mesh.userData.vel.clone().multiplyScalar(dt));r.life-=dt;let hit=false;if(r.mesh.position.distanceTo(camera.position)<1.5)hit=true;else{for(const o of obstacles){if(o.h<2)continue;if(Math.abs(r.mesh.position.x-o.x)<o.w/2&&Math.abs(r.mesh.position.z-o.z)<o.d/2&&r.mesh.position.y<o.h){hit=true;break}}}if(hit||r.life<=0){if(r.mesh.position.distanceTo(camera.position)<5){if(hasLOS(r.mesh.position.clone(), camera.position.clone().setY(1))){applyDmg(r.mesh.userData.dmg, false, false, r.mesh.userData.shooter);}}for(let k=0;k<15;k++)spawnParticle(r.mesh.position.clone(),0xff6600,0.2);scene.remove(r.mesh);G.raid.rockets.splice(i,1)}}}
function applyDmg(d, isHead, isFire = false, source = null){
if(!G.raid.alive)return;
let baseDmg=d, finalDmg=d;
if(isFire){
let totalReduce = 0;
if(G.raid.armorDur > 0 && G.raid.armorId){ const grade = ITEMS[G.raid.armorId].grade || 1; totalReduce += (GRADE_REDUCE[grade] || 0.30) * 0.5; }
if(G.raid.helmetDur > 0 && G.raid.helmetId){ const grade = ITEMS[G.raid.helmetId].grade || 1; totalReduce += (GRADE_REDUCE[grade] || 0.30) * 0.5; }
finalDmg = d * (1 - Math.min(0.8, totalReduce));
if(G.raid.armorDur > 0 && G.raid.armorId){ G.raid.armorDur -= Math.ceil(d * 0.5); if(G.raid.armorDur <= 0){G.raid.armorDur = 0;toast('护甲已破损!','warn')} }
if(G.raid.helmetDur > 0 && G.raid.helmetId){ G.raid.helmetDur -= Math.ceil(d * 0.3); if(G.raid.helmetDur <= 0){G.raid.helmetDur = 0;toast('头盔已破损!','warn')} }
} else if(isHead){
finalDmg = d * 2.0;
if(G.raid.helmetDur > 0 && G.raid.helmetId){
const grade = ITEMS[G.raid.helmetId].grade || 1; const reduce = GRADE_REDUCE[grade] || 0.30;
const absorbed = finalDmg * reduce; finalDmg -= absorbed;
G.raid.helmetDur -= Math.ceil(absorbed); if(G.raid.helmetDur <= 0){G.raid.helmetDur = 0;toast('头盔已破损!','warn')}
}
} else {
if(G.raid.armorDur > 0 && G.raid.armorId){
const grade = ITEMS[G.raid.armorId].grade || 1; const reduce = GRADE_REDUCE[grade] || 0.30;
const absorbed = finalDmg * reduce; finalDmg -= absorbed;
G.raid.armorDur -= Math.ceil(baseDmg); if(G.raid.armorDur <= 0){G.raid.armorDur = 0;toast('护甲已破损!','warn')}
}
}
G.raid.health -= finalDmg;
$('damageVignette').style.boxShadow='inset 0 0 200px rgba(200,70,47,0.8)';
setTimeout(()=>$('damageVignette').style.boxShadow='inset 0 0 200px rgba(200,70,47,0)',150);
if(G.raid.health<=0){ G.raid.health=0; die(source); }
updateHUD();
}
function die(killer){
if(G.raid._ended) return; G.raid.alive=false; G.raid.state='dead'; document.exitPointerLock();
G.raid.deathPos = camera.position.clone();
if(killer){
G.raid.replay = true; G.raid.replayTimer = 3.5;
const killerPos = killer.pos.clone().setY(1.5); const playerPos = G.raid.deathPos.clone().setY(0.5);
const dist = killerPos.distanceTo(playerPos); const dir = new THREE.Vector3().subVectors(killerPos, playerPos).normalize();
const sideOffset = new THREE.Vector3(-dir.z, 0, dir.x).multiplyScalar(dist * 0.5);
camera.position.copy(playerPos).add(sideOffset).add(new THREE.Vector3(0, dist*0.3+2, 0));
G.raid.replayCenter = playerPos; G.raid.replayRadius = dist * 0.8;
G.raid.replayAngle = Math.atan2(camera.position.x - playerPos.x, camera.position.z - playerPos.z);
G.raid.replayHeight = dist * 0.3 + 2;
$('replayUI').style.display = 'block'; $('replayKiller').textContent = killer.cfg.name;
$('replayInfo').textContent = '距离 ' + dist.toFixed(1) + 'm';
} else { $('deathScreen').style.display='flex'; }
setTimeout(()=>{ if(G.raid) G.raid.replay = false; $('replayUI').style.display='none'; $('deathScreen').style.display='none'; endRaid(false); }, 3500);
}
function checkExtract(dt){
if(!extractPoint)return; const d=camera.position.distanceTo(extractPoint.pos); G.raid.nearExtract=d<extractPoint.radius;
if(extractPoint.type==='fixed'){
if(G.raid.nearExtract&&!G.raid.extracting){ G.raid.extracting=true;G.raid.extractTimer=15;$('extractUI').style.display='flex';$('extractStatus').textContent='撤离中';toast('撤离倒计时','ok') }
if(G.raid.extracting){
if(d>extractPoint.radius+1){ G.raid.extracting=false;$('extractUI').style.display='none';toast('离开撤离区','warn') }
else { G.raid.extractTimer-=dt;$('extractTimer').textContent=Math.ceil(G.raid.extractTimer);if(G.raid.extractTimer<=0){endRaid(true)} }
}
} else {
if(!extractPoint.activated){
if(G.raid.nearExtract&&!G.raid._extractLockedToast){ toast('撤离点未解锁,请先前往拉闸点拉下闸门','warn'); G.raid._extractLockedToast=true; }
if(!G.raid.nearExtract)G.raid._extractLockedToast=false;
} else {
if(G.raid.nearExtract){
if(!G.raid.extracting){ G.raid.extracting=true;G.raid.extractTimer=10; $('extractUI').style.display='flex';$('extractStatus').textContent='撤离中'; }
G.raid.extractTimer-=dt;$('extractTimer').textContent=Math.ceil(G.raid.extractTimer); if(G.raid.extractTimer<=0){endRaid(true)}
} else {
if(G.raid.extracting){ G.raid.extracting=false;$('extractUI').style.display='none';toast('离开撤离区,倒计时暂停','warn'); }
}
}
}
}
function endRaid(survived){
if(G.raid._ended)return; G.raid._ended=true;
let gain=0,lost=0; const brought={},lost2={};
if(survived){
G.raid.inventory.forEach(i=>{brought[i.id]=(brought[i.id]||0)+i.count;if(ITEMS[i.id].sell)gain+=ITEMS[i.id].sell*i.count});
Object.entries(brought).forEach(([id,q])=>{G.stash[id]=(G.stash[id]||0)+q});
G.money+=gain;
if(G.raid.primary){
G.loadout.primary=G.raid.primary.id;
const ammoId = 'ammo_' + G.raid.primary.ammoType;
if(G.raid.primary.ammoInMag > 0){ G.stash[ammoId] = (G.stash[ammoId]||0) + G.raid.primary.ammoInMag; }
} else { G.loadout.primary = null; }
if(G.raid.secondary && G.raid.secondary.id !== 'knife'){
G.loadout.secondary = G.raid.secondary.id;
const ammoId = 'ammo_' + G.raid.secondary.ammoType;
if(G.raid.secondary.ammoInMag > 0){ G.stash[ammoId] = (G.stash[ammoId]||0) + G.raid.secondary.ammoInMag; }
} else { G.loadout.secondary = 'knife'; }
G.loadout.armorDur=G.raid.armorDur; G.loadout.helmetDur=G.raid.helmetDur;
G.loadout.consumables = [];
} else {
G.raid.inventory.forEach(i=>{lost2[i.id]=(lost2[i.id]||0)+i.count;if(ITEMS[i.id].sell)lost+=ITEMS[i.id].sell*i.count});
if(G.raid.primary&&ITEMS[G.raid.primary.id].sell)lost+=ITEMS[G.raid.primary.id].sell;
if(G.raid.secondary&&G.raid.secondary.id!=='knife'&&ITEMS[G.raid.secondary.id].sell)lost+=ITEMS[G.raid.secondary.id].sell;
G.loadout.primary=null;G.loadout.secondary='knife';G.loadout.armor=null;G.loadout.armorDur=0;
G.loadout.helmet=null;G.loadout.helmetDur=0;G.loadout.backpack=null;G.loadout.consumables=[];
}
$('resultStatus').textContent=survived?'生还':'阵亡'; $('resultStatus').style.color=survived?'var(--ok)':'var(--accent2)';
$('resultSub').textContent=survived?'EXTRACTION SUCCESSFUL':'KILLED IN ACTION';
$('rsKills').textContent=G.raid.kills; $('rsLoot').textContent=fmt(survived?G.raid.lootValue:0);
$('rsTotalLbl').textContent=survived?'总收益':'损失价值';
$('rsTotal').textContent=(survived?'':'-')+fmt(survived?gain:lost); $('rsTotal').style.color=survived?'var(--ok)':'var(--accent2)';
const di=survived?brought:lost2;
$('rsItems').innerHTML=Object.keys(di).length===0?'<div style="color:var(--muted);text-align:center;padding:20px">无</div>':Object.entries(di).map(([id,q])=>{const it=ITEMS[id];return'<div style="display:flex;align-items:center;gap:10px;padding:8px;background:var(--bg2);border:1px solid var(--line);margin-bottom:4px"><span>'+it.icon+'</span><span style="flex:1">'+it.name+'×'+q+'</span><span style="color:var(--ok)">'+(it.sell?fmt(it.sell*q):'装备')+'</span></div>'}).join('');
$('rsBalance').textContent=fmt(G.money); cleanup(); setTimeout(()=>{$('resultUI').style.display='flex'},500);
}
function cleanup(){if(renderer)renderer.dispose();renderer=null;scene=null;enemies=[];lootCrates=[];particles=[];obstacles=[];fires=[];smokes=[];extractPoint=null;$('hud').style.display='none';$('deathScreen').style.display='none';$('extractUI').style.display='none';$('lootUI').style.display='none';$('burnUI').style.display='none';$('scopeUI').style.display='none';$('nightVisionUI').style.background='rgba(0,0,0,0)';$('burnVignette').style.boxShadow='inset 0 0 150px rgba(255,69,0,0)';$('pauseOverlay').style.display='none';$('replayUI').style.display='none';G.raid=null}
$('backToBase').onclick=()=>{$('resultUI').style.display='none';$('base').style.display='flex';renderBase()};
function updateScopeDistance(){
if(!isAiming) return; const o = new THREE.Vector3(); camera.getWorldPosition(o);
const dir = new THREE.Vector3(); camera.getWorldDirection(dir);
const ray = new THREE.Raycaster(o, dir, 0, 300); const meshes = []; scene.traverse(ob => { if(ob.isMesh) meshes.push(ob); });
const hits = ray.intersectObjects(meshes, false);
if(hits.length > 0){ $('scopeDist').textContent = hits[0].distance.toFixed(1) + 'm'; } else { $('scopeDist').textContent = '∞'; }
}
function updateThrowTrajectory(){
if(!isThrowing || !throwTrajectoryLine) return;
const power = isCharging ? Math.min(1, (performance.now() - throwStartTime) / 1500) : 0;
const speed = 10 + power * 20; const o = new THREE.Vector3(); camera.getWorldPosition(o);
const dir = new THREE.Vector3(); camera.getWorldDirection(dir);
const start = o.clone().add(dir.clone().multiplyScalar(1)); start.y -= 0.5;
const vel = dir.clone().multiplyScalar(speed); vel.y += 5;
const points = []; let p = start.clone(); let v = vel.clone(); const dt = 0.05;
for(let i=0; i<60; i++){ points.push(p.clone()); p.add(v.clone().multiplyScalar(dt)); v.y -= 15 * dt; if(p.y < 0) break; }
const geo = new THREE.BufferGeometry().setFromPoints(points);
throwTrajectoryLine.geometry.dispose(); throwTrajectoryLine.geometry = geo; throwTrajectoryLine.computeLineDistances();
}
function animate(){if(!G.raid)return;requestAnimationFrame(animate);const dt=Math.min(clock.getDelta(),0.1);lootCrates.forEach(c=>{if(c.falling){c.mesh.position.y-=dt*10;if(c.mesh.position.y<=0){c.mesh.position.y=0;c.falling=false;if(c.mesh.userData.chute)c.mesh.remove(c.mesh.userData.chute)}}});if(G.raid.state==='intro'){G.raid.introTime-=dt;const p=1-G.raid.introTime/3;camera.position.set(0,30-28.3*p,5);camera.rotation.set(-0.2+0.2*p,0,0);if(G.raid.helicopter){G.raid.helicopter.position.y=camera.position.y+6;G.raid.helicopter.userData.rotor.rotation.y+=dt*30}if(G.raid.introTime<=0){G.raid.state='playing';scene.remove(G.raid.helicopter);$('pauseOverlay').style.display='flex';$('pauseText').textContent='准备就绪'}renderer.render(scene,camera);return}
const targetFov=isAiming?(G.raid.weapon.zoom?20:45):75;if(Math.abs(camera.fov-targetFov)>0.1){camera.fov+=(targetFov-camera.fov)*0.2;camera.updateProjectionMatrix()}
$('scopeUI').style.display = isAiming ? 'block' : 'none';
if(isAiming) updateScopeDistance();
if(G.raid.state === 'dead' && G.raid.replay){
G.raid.replayAngle += dt * 0.3;
const x = G.raid.replayCenter.x + Math.sin(G.raid.replayAngle) * G.raid.replayRadius;
const z = G.raid.replayCenter.z + Math.cos(G.raid.replayAngle) * G.raid.replayRadius;
const y = G.raid.replayCenter.y + G.raid.replayHeight;
camera.position.lerp(new THREE.Vector3(x, y, z), 0.1); camera.lookAt(G.raid.replayCenter);
renderer.render(scene, camera); return;
}
const paused=!input.locked&&$('lootUI').style.display!=='flex'&&$('pauseOverlay').style.display!=='flex';if(G.raid.alive&&G.raid.state==='playing'&&!paused){updatePlayer(dt);updateEnemies(dt);checkExtract(dt);updateRockets(dt);if(input.mouseDown&&input.locked&&!isThrowing)fire()}if(isThrowing){updateThrowTrajectory();if(isCharging && throwFuse > 0){throwFuse-=dt;$('throwTimer').textContent=throwFuse.toFixed(1)+'s';if(throwFuse<=0){const o=new THREE.Vector3();camera.getWorldPosition(o);explodeGrenade({pos:o, type:currentThrowable});cancelThrow();die(null);}}else if(!isCharging){$('throwTimer').textContent='';}}for(let i=G.raid.grenades.length-1;i>=0;i--){const gr=G.raid.grenades[i];gr.vel.y-=15*dt;const next=gr.pos.clone().add(gr.vel.clone().multiplyScalar(dt));let bounced=false;if(next.y<0.1){next.y=0.1;gr.vel.y*=-0.4;gr.vel.x*=0.6;gr.vel.z*=0.6;bounced=true}for(const o of obstacles){if(Math.abs(next.x-o.x)<o.w/2+0.1&&Math.abs(next.z-o.z)<o.d/2+0.1&&next.y<o.h){gr.vel.x*=-0.5;gr.vel.z*=-0.5;next.x=gr.pos.x;next.z=gr.pos.z;bounced=true}}gr.pos.copy(next);gr.mesh.position.copy(gr.pos);if(gr.type === 'grenade' && bounced && gr.timer <= 0){gr.timer = ITEMS.grenade.fuse || 3;} if(gr.timer>0){gr.timer-=dt;if(gr.timer<=0){explodeGrenade(gr);G.raid.grenades.splice(i,1)}}else if(bounced && gr.type !== 'grenade'){explodeGrenade(gr);G.raid.grenades.splice(i,1)}}for(let i=fires.length-1;i>=0;i--){const f=fires[i];f.duration-=dt;f.tick-=dt;f.mesh.material.opacity=0.4+Math.sin(performance.now()*0.01)*0.2;if(f.tick<=0){f.tick=0.5;if(camera.position.distanceTo(f.pos)<f.radius){if(hasLOS(f.pos.clone().setY(0.5), camera.position.clone().setY(1))){applyDmg(10,false,true,null);G.raid.burning=true;G.raid.burnDur=3;G.raid.burnTick=0.5;}}enemies.forEach(e=>{if(e.state!=='dead'&&e.pos.distanceTo(f.pos)<f.radius){if(hasLOS(f.pos.clone().setY(0.5), e.pos.clone().setY(1))){e.health-=20;if(e.health<=0)killEnemy(e);}}});}if(f.duration<=0){scene.remove(f.mesh);fires.splice(i,1);}}for(let i=smokes.length-1;i>=0;i--){const s=smokes[i];s.duration-=dt;s.mesh.material.opacity=0.4+Math.sin(performance.now()*0.005)*0.2;if(s.duration<=0){scene.remove(s.mesh);smokes.splice(i,1)}}for(let i=particles.length-1;i>=0;i--){const p=particles[i];if(p.isTracer){p.life-=dt;p.mesh.material.opacity=p.life/0.08*0.9;if(p.life<=0){scene.remove(p.mesh);p.mesh.geometry.dispose();p.mesh.material.dispose();particles.splice(i,1)}}else if(p.isExplosion){p.life-=dt;p.scale+=dt*40;p.mesh.scale.set(p.scale,p.scale,p.scale);p.mesh.material.opacity=Math.max(0,p.life/0.5*0.8);if(p.life<=0){scene.remove(p.mesh);p.mesh.geometry.dispose();p.mesh.material.dispose();particles.splice(i,1)}}else{p.mesh.position.add(p.mesh.userData.vel.clone().multiplyScalar(dt));p.mesh.userData.vel.y-=9.8*dt;p.life-=dt;if(p.life<=0){scene.remove(p.mesh);particles.splice(i,1)}}}updateMinimap();renderer.render(scene,camera)}
function updatePlayer(dt){
const mv=input.keys.KeyW||input.keys.KeyS||input.keys.KeyA||input.keys.KeyD;
const run=input.keys.ShiftLeft&&mv&&!isAiming;
if(run&&!G.raid.staminaExhausted){G.raid.stamina-=dt*10;if(G.raid.stamina<=0){G.raid.stamina=0;G.raid.staminaExhausted=true;toast('体力耗尽','warn')}}else{G.raid.stamina=Math.min(100,G.raid.stamina+dt*8);if(G.raid.staminaExhausted&&G.raid.stamina>=30)G.raid.staminaExhausted=false}
const canRun=run&&G.raid.stamina>0&&!G.raid.staminaExhausted;
const spd=(canRun?8:4.5)*(1-G.raid.movePenalty/100)*dt;
if(G.raid.burning && G.raid.alive){
G.raid.burnDur -= dt;G.raid.burnTick -= dt;
$('burnVignette').style.boxShadow = 'inset 0 0 150px rgba(255,69,0,0.6)';
if(G.raid.burnTick <= 0){applyDmg(5, false, true, null);G.raid.burnTick = 0.5;spawnParticle(camera.position.clone().add(new THREE.Vector3((Math.random()-0.5)*0.5,1+Math.random()*0.5,(Math.random()-0.5)*0.5)),0xff6600,0.15);}
if(G.raid.burnDur <= 0){G.raid.burning = false;$('burnVignette').style.boxShadow = 'inset 0 0 150px rgba(255,69,0,0)';if(!G.raid.extinguishing) toast('火苗熄灭','ok');}
} else {$('burnVignette').style.boxShadow = 'inset 0 0 150px rgba(255,69,0,0)';}
if(G.raid.extinguishing){
if(mv || input.mouseDown){G.raid.extinguishing = false;$('burnUI').style.display = 'none';toast('动作中断,扑灭失败','warn');}
else {
G.raid.extinguishTimer -= dt;$('burnTimer').textContent = Math.ceil(G.raid.extinguishTimer);
if(G.raid.extinguishTimer <= 0){G.raid.burning = false;G.raid.extinguishing = false;G.raid.burnDur = 0;$('burnUI').style.display = 'none';$('burnVignette').style.boxShadow = 'inset 0 0 150px rgba(255,69,0,0)';toast('已扑灭火苗','ok');}
return;
}
}
const f=new THREE.Vector3(-Math.sin(input.yaw),0,-Math.cos(input.yaw)),r=new THREE.Vector3(Math.cos(input.yaw),0,-Math.sin(input.yaw));let mx=0,mz=0;
if(input.keys.KeyW){mx+=f.x;mz+=f.z}if(input.keys.KeyS){mx-=f.x;mz-=f.z}if(input.keys.KeyA){mx-=r.x;mz-=r.z}if(input.keys.KeyD){mx+=r.x;mz+=r.z}
const l=Math.sqrt(mx*mx+mz*mz);if(l>0){mx=mx/l*spd;mz=mz/l*spd;if(!checkCol(camera.position.x+mx,camera.position.z,0.4))camera.position.x+=mx;if(!checkCol(camera.position.x,camera.position.z+mz,0.4))camera.position.z+=mz}
camera.rotation.order='YXZ';camera.rotation.y=input.yaw;camera.rotation.x=input.pitch;
G.raid.nearLoot=null;for(const c of lootCrates){if(c.items.length>0&&camera.position.distanceTo(c.pos)<2.5){G.raid.nearLoot=c;break}}
G.raid.nearLever=false;
if(extractPoint&&extractPoint.lever&&!extractPoint.lever.pulled){
if(camera.position.distanceTo(extractPoint.lever.pos)<2.5){
G.raid.nearLever=true;
if(!G.raid._leverHintShown){ toast('按 F 拉下闸门解锁撤离点','ok'); G.raid._leverHintShown=true; }
} else { G.raid._leverHintShown=false; }
}
G.raid.raidTime-=dt;if(!G.raid.airdropTriggered&&G.raid.raidTime<=420){G.raid.airdropTriggered=true;for(let i=0;i<G.raid.airdropCount;i++){spawnAirdrop()}}if(G.raid.raidTime<=0){die(null)}updateHUD();
}
const mmCtx=$('mmCanvas').getContext('2d');function updateMinimap(){const ctx=mmCtx;ctx.clearRect(0,0,160,160);ctx.fillStyle='rgba(10,9,8,0.8)';ctx.fillRect(0,0,160,160);const sc=160/(worldSize*2),cx=80,cy=80;ctx.fillStyle='#3a2e22';obstacles.forEach(o=>{if(!o.h||o.h<2)return;ctx.fillRect(cx+o.x*sc-o.w*sc/2,cy+o.z*sc-o.d*sc/2,o.w*sc,o.d*sc)});lootCrates.forEach(c=>{
if(c.isAirdrop){ctx.fillStyle = c.items.length > 0 ? '#00ffff' : '#005566';ctx.fillRect(cx+c.pos.x*sc-2.5,cy+c.pos.z*sc-2.5,5,5);}
else {if(c.items.length > 0){ctx.fillStyle = c.opened ? '#d49a3a' : '#7a9b3a';} else {ctx.fillStyle = '#333333';}ctx.fillRect(cx+c.pos.x*sc-1.5,cy+c.pos.z*sc-1.5,3,3);}
});ctx.fillStyle='#c8462f';enemies.forEach(e=>{if(e.state!=='dead'){ctx.beginPath();ctx.arc(cx+e.pos.x*sc,cy+e.pos.z*sc,2.5,0,Math.PI*2);ctx.fill()}});if(extractPoint){ctx.fillStyle=extractPoint.activated?'#7a9b3a':'#d49a3a';ctx.beginPath();ctx.arc(cx+extractPoint.pos.x*sc,cy+extractPoint.pos.z*sc,5,0,Math.PI*2);ctx.fill()}
if(extractPoint&&extractPoint.lever){
ctx.fillStyle=extractPoint.lever.pulled?'#7a9b3a':'#ff4400';
ctx.beginPath(); ctx.arc(cx+extractPoint.lever.pos.x*sc,cy+extractPoint.lever.pos.z*sc,4,0,Math.PI*2); ctx.fill();
ctx.fillStyle='#fff'; ctx.font='8px monospace'; ctx.fillText('L',cx+extractPoint.lever.pos.x*sc-2,cy+extractPoint.lever.pos.z*sc+3);
}
ctx.fillStyle='#d4a574';ctx.beginPath();ctx.arc(cx+camera.position.x*sc,cy+camera.position.z*sc,3,0,Math.PI*2);ctx.fill()}
window.onresize=()=>{if(camera&&renderer){camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();renderer.setSize(innerWidth,innerHeight)}};
document.addEventListener('pointerlockchange',()=>{input.locked=document.pointerLockElement===$('game');if(!input.locked){if(G.raid&&G.raid.alive&&G.raid.state==='playing'&&$('lootUI').style.display!=='flex'){$('pauseText').textContent='已暂停';$('pauseOverlay').style.display='flex'}}else{$('pauseOverlay').style.display='none';if(clock)clock.getDelta()}});
document.addEventListener('click',()=>{initAudio();if(!input.locked&&G.raid&&G.raid.alive&&G.raid.state==='playing'&&$('lootUI').style.display!=='flex'&&$('resultUI').style.display!=='flex'){$('game').requestPointerLock()}});
document.addEventListener('mousemove',e=>{if(!input.locked||!G.raid||G.raid.state!=='playing')return;const s=isAiming?0.0012:0.0022;input.yaw-=e.movementX*s;input.pitch-=e.movementY*s;input.pitch=Math.max(-1.5,Math.min(1.5,input.pitch))});
document.addEventListener('mousedown',e=>{
if(e.button===0 && input.locked && G.raid.state==='playing'){
if(isThrowing){ startCharge(); } else { input.mouseDown=true; fire(); }
}
});
document.addEventListener('mouseup',e=>{
if(e.button===0){
if(isThrowing && isCharging){ releaseThrow(); }
input.mouseDown=false;
}
});
document.addEventListener('contextmenu',e=>e.preventDefault());
window.addEventListener('keydown',e=>{
if(dragData.active && (e.code==='KeyR'||e.code==='ArrowUp'||e.code==='ArrowDown'||e.code==='ArrowLeft'||e.code==='ArrowRight')){
const temp = dragData.item.w;dragData.item.w = dragData.item.h;dragData.item.h = temp;
ghost.style.width = (dragData.item.w * 40) + 'px';ghost.style.height = (dragData.item.h * 40) + 'px';
e.preventDefault();return;
}
input.keys[e.code]=true;
if(e.code==='KeyM'&&!e.repeat)toggleMute();
if(G.raid&&G.raid.alive&&G.raid.state==='playing'){
if(e.code==='KeyR')reload();
if(e.code==='KeyF')toggleLoot();
if(e.code==='KeyH')useHeal();
if(e.code==='KeyT')useRepair('armor');
if(e.code==='KeyG')useRepair('helmet');
if(e.code==='Digit3'&&!e.repeat)startThrow();
if(e.code==='Digit4'&&!e.repeat)toggleThrowable();
if(e.code==='KeyN'&&!e.repeat)toggleNV();
if(e.code==='KeyX'&&!e.repeat)toggleExtinguish();
if(e.code==='KeyZ'&&!e.repeat){isAiming=!isAiming;toast(isAiming?'开镜':'关镜','')}
if(e.code==='Digit1')switchWeapon('primary');
if(e.code==='Digit2')switchWeapon('secondary');
}
});
window.addEventListener('keyup',e=>{ input.keys[e.code]=false; });
document.querySelectorAll('.diff-card').forEach(c=>c.onclick=()=>{document.querySelectorAll('.diff-card').forEach(x=>x.classList.remove('active'));c.classList.add('active');G.selectedDiff=parseInt(c.dataset.diff);$('mapDesc').textContent=DIFFS[G.selectedDiff].desc});
document.querySelector('.diff-card[data-diff="0"]').classList.add('active');$('mapDesc').textContent=DIFFS[0].desc;
$('deployBtn').onclick=()=>{const score=getLoadoutScore();if(score<DIFFS[G.selectedDiff].reqScore){toast('战备值不足!当前 '+score+',需要 '+DIFFS[G.selectedDiff].reqScore,'warn');return}$('base').style.display='none';$('loadingUI').style.display='flex';setTimeout(()=>{$('loadingUI').style.display='none';initRaid()},1500)};
renderBase();
</script>
</body>
</html>
Game Source: DARK ZONE
Creator: RocketDolphin44
Libraries: three
Complexity: complex (1457 lines, 126.1 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: dark-zone-rocketdolphin44" to link back to the original. Then publish at arcadelab.ai/publish.