LETHAL 2D - 致命公司2D版
by SolarWolf302221 lines74.6 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LETHAL 2D - 致命公司2D版</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0a0a0a;
color: #fff;
font-family: 'Courier New', monospace;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
user-select: none;
}
#gameContainer {
position: relative;
width: 100vw;
height: 100vh;
}
canvas {
display: block;
background: #000;
cursor: crosshair;
}
#ui {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none;
z-index: 10;
}
#topBar {
position: absolute;
top: 10px; left: 50%;
transform: translateX(-50%);
display: flex;
gap: 20px;
background: rgba(0,0,0,0.7);
padding: 8px 20px;
border: 1px solid #444;
border-radius: 4px;
font-size: 14px;
}
#topBar span { color: #ffd700; }
#inventory {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 6px;
}
.slot {
width: 56px; height: 56px;
background: rgba(0,0,0,0.75);
border: 2px solid #555;
border-radius: 4px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 11px;
position: relative;
color: #ccc;
}
.slot.active { border-color: #ffd700; box-shadow: 0 0 10px rgba(255,215,0,0.5); }
.slot .key { position: absolute; top: 2px; left: 4px; font-size: 10px; color: #888; }
.slot .item-icon { font-size: 22px; }
.slot .item-name { font-size: 9px; text-align: center; margin-top: 2px; }
.slot.tool { border-color: #4488ff; }
.slot.tool .item-icon { color: #66aaff; }
#healthBar {
position: absolute;
bottom: 90px;
left: 50%;
transform: translateX(-50%);
width: 200px;
height: 16px;
background: rgba(0,0,0,0.7);
border: 1px solid #666;
border-radius: 3px;
overflow: hidden;
}
#healthFill {
height: 100%;
background: linear-gradient(90deg, #ff4444, #ff6666);
transition: width 0.3s;
}
#flashlightBar {
position: absolute;
bottom: 115px;
left: 50%;
transform: translateX(-50%);
width: 150px;
height: 10px;
background: rgba(0,0,0,0.7);
border: 1px solid #666;
border-radius: 3px;
overflow: hidden;
}
#flashlightFill {
height: 100%;
background: linear-gradient(90deg, #ffff88, #ffdd44);
transition: width 0.2s;
}
#message {
position: absolute;
top: 80px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.8);
padding: 10px 20px;
border-radius: 4px;
font-size: 14px;
display: none;
text-align: center;
max-width: 60%;
}
#shopUI, #shipInvUI, #menuUI, #gameOverUI {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0,0,0,0.92);
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 100;
pointer-events: auto;
}
.panel {
background: #1a1a1a;
border: 2px solid #444;
border-radius: 8px;
padding: 30px;
max-width: 700px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.panel h2 { color: #ffd700; margin-bottom: 15px; text-align: center; }
.exit-btn {
position: absolute;
top: 15px;
right: 15px;
background: #c0392b;
color: #fff;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
font-size: 14px;
}
.exit-btn:hover { background: #e74c3c; }
.shop-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin: 15px 0;
}
.shop-item {
background: #222;
border: 1px solid #555;
border-radius: 4px;
padding: 10px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.shop-item:hover { border-color: #ffd700; background: #2a2a2a; }
.shop-item .icon { font-size: 28px; }
.shop-item .name { font-size: 12px; margin: 5px 0; }
.shop-item .price { color: #ffd700; font-size: 13px; font-weight: bold; }
.shop-item.owned { opacity: 0.5; cursor: not-allowed; }
.page-controls {
display: flex;
justify-content: center;
gap: 15px;
margin-top: 10px;
}
.page-controls button {
background: #333;
color: #fff;
border: 1px solid #666;
padding: 6px 14px;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
}
.page-controls button:hover { background: #444; }
.page-controls button:disabled { opacity: 0.3; cursor: not-allowed; }
.ship-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
margin: 15px 0;
}
.ship-slot {
width: 70px; height: 70px;
background: #222;
border: 2px solid #555;
border-radius: 4px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
font-size: 11px;
}
.ship-slot:hover { border-color: #ffd700; }
.ship-slot .icon { font-size: 24px; }
.menu-btn {
display: block;
width: 250px;
margin: 10px auto;
padding: 14px;
background: #2c3e50;
color: #fff;
border: 2px solid #3498db;
border-radius: 6px;
cursor: pointer;
font-family: inherit;
font-size: 16px;
transition: all 0.2s;
}
.menu-btn:hover { background: #34495e; border-color: #5dade2; }
.menu-title {
font-size: 48px;
color: #ffd700;
text-align: center;
margin-bottom: 10px;
text-shadow: 0 0 20px rgba(255,215,0,0.5);
letter-spacing: 4px;
}
.menu-sub { text-align: center; color: #888; margin-bottom: 30px; font-size: 14px; }
.controls-info {
background: rgba(0,0,0,0.5);
padding: 15px;
border-radius: 6px;
margin-top: 20px;
font-size: 12px;
color: #aaa;
line-height: 1.8;
}
.controls-info b { color: #ffd700; }
#hint {
position: absolute;
bottom: 160px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.7);
padding: 6px 14px;
border-radius: 4px;
font-size: 12px;
color: #aaa;
display: none;
}
#radioPlaying {
position: absolute;
top: 50px;
left: 50%;
transform: translateX(-50%);
background: rgba(100,0,100,0.8);
padding: 8px 20px;
border-radius: 4px;
font-size: 13px;
display: none;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
</style>
</head>
<body>
<div id="gameContainer">
<canvas id="game"></canvas>
<div id="ui">
<div id="topBar">
<div>任务金额: <span id="taskAmount">$0</span></div>
<div>已收集: <span id="collectedAmount">$0</span></div>
<div>时间: <span id="timer">30:00</span></div>
<div>地图: <span id="mapName">-</span></div>
</div>
<div id="healthBar"><div id="healthFill" style="width:100%"></div></div>
<div id="flashlightBar"><div id="flashlightFill" style="width:100%"></div></div>
<div id="inventory"></div>
<div id="hint"></div>
<div id="message"></div>
<div id="radioPlaying">📻 BL STUDIO HARDTEKK (Slowed) 播放中...</div>
</div>
<div id="menuUI">
<div class="menu-title">LETHAL 2D</div>
<div class="menu-sub">致命公司 2D 版 — 进入大门,捡取物资,活着回来</div>
<button class="menu-btn" onclick="Game.startGame()">开始游戏</button>
<button class="menu-btn" onclick="Game.showControls()">操作说明</button>
<div id="controlsDetail" class="controls-info" style="display:none;max-width:500px;margin:20px auto;">
<b>WASD</b> - 移动 <b>鼠标</b> - 转视野<br>
<b>1-4</b> - 切换物品栏 <b>5</b> - 切换小包<br>
<b>F</b> - 开关手电筒 <b>E</b> - 拾取/购买<br>
<b>Q</b> - 上交物资 <b>T</b> - 丢弃物品<br>
<b>O</b> - 飞船储物 <b>Y</b> - 上车/出发<br>
<b>空格</b> - 使用道具(收音机/枪) <b>ESC</b> - 暂停
</div>
</div>
<div id="shopUI">
<div class="panel" style="position:relative;">
<button class="exit-btn" onclick="Game.closeShop()">退出商店</button>
<h2>🛒 公司商店</h2>
<div style="text-align:center;color:#ffd700;margin-bottom:10px;">余额: $<span id="shopMoney">0</span></div>
<div class="shop-grid" id="shopGrid"></div>
<div class="page-controls" id="pageControls" style="display:none;">
<button onclick="Game.shopPage(-1)" id="prevPage">上一页</button>
<span id="pageNum">1/1</span>
<button onclick="Game.shopPage(1)" id="nextPage">下一页</button>
</div>
<div style="text-align:center;margin-top:15px;font-size:12px;color:#888;">
走到车子旁按 <b style="color:#ffd700;">Y</b> 上车出发
</div>
</div>
</div>
<div id="shipInvUI">
<div class="panel" style="position:relative;">
<button class="exit-btn" onclick="Game.closeShipInv()">关闭</button>
<h2>🚀 飞船储物柜</h2>
<div style="text-align:center;color:#888;font-size:12px;margin-bottom:10px;">道具永久保存,点击取出</div>
<div class="ship-grid" id="shipGrid"></div>
</div>
</div>
<div id="gameOverUI">
<div class="panel" style="text-align:center;">
<h2 id="gameOverTitle">任务失败</h2>
<p id="gameOverMsg" style="margin:15px 0;color:#aaa;"></p>
<p style="color:#ffd700;">本局收集: $<span id="finalCollected">0</span></p>
<button class="menu-btn" onclick="Game.backToMenu()">返回主菜单</button>
</div>
</div>
</div>
<script>
// 全局错误捕获
window.addEventListener('error', function(e) {
console.error('JS错误:', e.message, e.filename, e.lineno);
var div = document.createElement('div');
div.style.cssText = 'position:fixed;top:0;left:0;right:0;background:#ff0000;color:#fff;padding:10px;z-index:99999;font-size:12px;';
div.textContent = 'ERROR: ' + e.message + ' (' + e.lineno + ')';
document.body.appendChild(div);
});
// ==================== 常量定义 ====================
const TILE = 32;
const MAP_W = 80;
const MAP_H = 80;
const VIEW_RADIUS = 3; // 不开手电时可见3步(包括自身)
const MOUSE_VIEW = 20; // 开手电时鼠标方向可见20步
const MOUSE_VIEW_DARK = 5; // 不开手电时鼠标方向最多5步
const GAME_TIME = 30 * 60; // 30分钟
const SHIP_REFILL_DIST = 3; // 离飞船3步充电
// 地图瓦片类型
const T_FLOOR = 0, T_WALL = 1, T_GATE = 2, T_OUTSIDE = 3, T_BUILDING = 4;
// ==================== 音效系统 ====================
const Audio = {
ctx: null,
init() {
if (!this.ctx) this.ctx = new (window.AudioContext || window.webkitAudioContext)();
},
play(type) {
if (!this.ctx) return;
const ctx = this.ctx;
const now = ctx.currentTime;
const o = ctx.createOscillator();
const g = ctx.createGain();
o.connect(g); g.connect(ctx.destination);
switch(type) {
case 'pickup':
o.frequency.setValueAtTime(600, now);
o.frequency.exponentialRampToValueAtTime(1200, now + 0.1);
g.gain.setValueAtTime(0.2, now);
g.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
o.start(now); o.stop(now + 0.15);
break;
case 'drop':
o.frequency.setValueAtTime(400, now);
o.frequency.exponentialRampToValueAtTime(150, now + 0.15);
g.gain.setValueAtTime(0.15, now);
g.gain.exponentialRampToValueAtTime(0.01, now + 0.2);
o.start(now); o.stop(now + 0.2);
break;
case 'sell':
o.frequency.setValueAtTime(800, now);
o.frequency.setValueAtTime(1000, now + 0.08);
o.frequency.setValueAtTime(1400, now + 0.16);
g.gain.setValueAtTime(0.2, now);
g.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
o.start(now); o.stop(now + 0.3);
break;
case 'hurt':
o.type = 'sawtooth';
o.frequency.setValueAtTime(200, now);
o.frequency.exponentialRampToValueAtTime(80, now + 0.2);
g.gain.setValueAtTime(0.25, now);
g.gain.exponentialRampToValueAtTime(0.01, now + 0.25);
o.start(now); o.stop(now + 0.25);
break;
case 'monster':
o.type = 'square';
o.frequency.setValueAtTime(100 + Math.random()*50, now);
g.gain.setValueAtTime(0.1, now);
g.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
o.start(now); o.stop(now + 0.3);
break;
case 'flashlight':
o.frequency.setValueAtTime(1500, now);
g.gain.setValueAtTime(0.08, now);
g.gain.exponentialRampToValueAtTime(0.01, now + 0.05);
o.start(now); o.stop(now + 0.05);
break;
case 'buy':
o.frequency.setValueAtTime(500, now);
o.frequency.setValueAtTime(700, now + 0.06);
o.frequency.setValueAtTime(900, now + 0.12);
g.gain.setValueAtTime(0.15, now);
g.gain.exponentialRampToValueAtTime(0.01, now + 0.25);
o.start(now); o.stop(now + 0.25);
break;
case 'gunshot':
o.type = 'sawtooth';
o.frequency.setValueAtTime(150, now);
o.frequency.exponentialRampToValueAtTime(50, now + 0.1);
g.gain.setValueAtTime(0.3, now);
g.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
o.start(now); o.stop(now + 0.15);
break;
case 'radio':
// 收音机开启音效
o.type = 'sine';
o.frequency.setValueAtTime(440, now);
g.gain.setValueAtTime(0.15, now);
g.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
o.start(now); o.stop(now + 0.3);
break;
case 'door':
o.type = 'triangle';
o.frequency.setValueAtTime(200, now);
o.frequency.linearRampToValueAtTime(400, now + 0.3);
g.gain.setValueAtTime(0.15, now);
g.gain.exponentialRampToValueAtTime(0.01, now + 0.4);
o.start(now); o.stop(now + 0.4);
break;
case 'warning':
o.type = 'square';
o.frequency.setValueAtTime(440, now);
o.frequency.setValueAtTime(880, now + 0.15);
g.gain.setValueAtTime(0.12, now);
g.gain.exponentialRampToValueAtTime(0.01, now + 0.4);
o.start(now); o.stop(now + 0.4);
break;
}
},
// 收音机音乐 - BL STUDIO HARDTEKK (Slowed) 风格模拟
radioMusic: {
playing: false,
nodes: [],
startTime: 0,
start() {
if (this.playing || !Audio.ctx) return;
this.playing = true;
const ctx = Audio.ctx;
this.startTime = ctx.currentTime;
// 硬派科技舞曲风格 - slowed down
const bpm = 100; // slowed
const beatLen = 60 / bpm;
// 低音鼓
const kickInterval = setInterval(() => {
if (!this.playing) { clearInterval(kickInterval); return; }
const t = ctx.currentTime;
const o = ctx.createOscillator();
const g = ctx.createGain();
o.type = 'sine';
o.frequency.setValueAtTime(120, t);
o.frequency.exponentialRampToValueAtTime(40, t + 0.15);
g.gain.setValueAtTime(0.4, t);
g.gain.exponentialRampToValueAtTime(0.01, t + 0.2);
o.connect(g); g.connect(ctx.destination);
o.start(t); o.stop(t + 0.2);
}, beatLen * 1000);
// 贝斯线
const bassNotes = [55, 55, 73, 55, 65, 55, 82, 73];
let bassIdx = 0;
const bassInterval = setInterval(() => {
if (!this.playing) { clearInterval(bassInterval); return; }
const t = ctx.currentTime;
const freq = bassNotes[bassIdx % bassNotes.length];
bassIdx++;
const o = ctx.createOscillator();
const g = ctx.createGain();
o.type = 'sawtooth';
o.frequency.setValueAtTime(freq, t);
g.gain.setValueAtTime(0.12, t);
g.gain.exponentialRampToValueAtTime(0.01, t + beatLen * 0.9);
const filter = ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 300;
o.connect(filter); filter.connect(g); g.connect(ctx.destination);
o.start(t); o.stop(t + beatLen);
}, beatLen * 1000);
// 高音镲
const hatInterval = setInterval(() => {
if (!this.playing) { clearInterval(hatInterval); return; }
const t = ctx.currentTime;
const bufferSize = ctx.sampleRate * 0.05;
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
const noise = ctx.createBufferSource();
noise.buffer = buffer;
const g = ctx.createGain();
const filter = ctx.createBiquadFilter();
filter.type = 'highpass';
filter.frequency.value = 6000;
g.gain.setValueAtTime(0.08, t);
g.gain.exponentialRampToValueAtTime(0.01, t + 0.04);
noise.connect(filter); filter.connect(g); g.connect(ctx.destination);
noise.start(t); noise.stop(t + 0.05);
}, (beatLen / 2) * 1000);
this.nodes = [kickInterval, bassInterval, hatInterval];
document.getElementById('radioPlaying').style.display = 'block';
},
stop() {
this.playing = false;
this.nodes.forEach(n => clearInterval(n));
this.nodes = [];
document.getElementById('radioPlaying').style.display = 'none';
}
}
};
// ==================== 物品定义 ====================
const ITEMS = {
// ==================== 物资(垃圾)- 致命公司全系列 ====================
junk_apparatus: { name:'装置', icon:'⚙️', price:65, type:'junk', color:'#aaa' },
junk_bell: { name:'大钟', icon:'🔔', price:60, type:'junk', color:'#ddaa44' },
junk_bigbolt: { name:'大螺栓', icon:'🔩', price:30, type:'junk', color:'#888' },
junk_bottle: { name:'瓶子', icon:'🍾', price:35, type:'junk', color:'#88cc88' },
junk_brush: { name:'刷子', icon:'🪥', price:25, type:'junk', color:'#dd88aa' },
junk_candy: { name:'糖果', icon:'🍬', price:20, type:'junk', color:'#ff88cc' },
junk_cashregister: { name:'收银机', icon:'🧾', price:90, type:'junk', color:'#66cc66' },
junk_chemicaljug: { name:'化学壶', icon:'⚗️', price:50, type:'junk', color:'#88ff88' },
junk_clownhorn: { name:'小丑喇叭', icon:'📯', price:50, type:'junk', color:'#ff8866' },
junk_clock: { name:'时钟', icon:'🕰️', price:45, type:'junk', color:'#aa9966' },
junk_cod: { name:'鳕鱼', icon:'🐟', price:52, type:'junk', color:'#88bbdd' },
junk_comedymask: { name:'喜剧面具', icon:'🎭', price:40, type:'junk', color:'#ffdd66' },
junk_cookiemold: { name:'饼干模具', icon:'🍪', price:35, type:'junk', color:'#cc9966' },
junk_copperpipe: { name:'铜管', icon:'🥫', price:40, type:'junk', color:'#dd8844' },
junk_dustpan: { name:'簸箕', icon:'🧹', price:28, type:'junk', color:'#cc8844' },
junk_eggbeater: { name:'打蛋器', icon:'🥚', price:22, type:'junk', color:'#eeeecc' },
junk_fancylamp: { name:'花式台灯', icon:'💡', price:45, type:'junk', color:'#ffee88' },
junk_gift: { name:'礼物盒', icon:'🎁', price:55, type:'junk', color:'#ff6688' },
junk_goldbar: { name:'金条', icon:'🪙', price:120, type:'junk', color:'#ffd700' },
junk_goldcup: { name:'金杯', icon:'🏆', price:100, type:'junk', color:'#ffdd00' },
junk_hairdryer: { name:'吹风机', icon:'💨', price:30, type:'junk', color:'#dd6688' },
junk_jar: { name:'腌菜罐', icon:'🫙', price:25, type:'junk', color:'#aabbcc' },
junk_kitchencounter: { name:'厨房台面', icon:'🍳', price:60, type:'junk', color:'#bb9977' },
junk_largeaxle: { name:'大轴', icon:'⚙️', price:55, type:'junk', color:'#777' },
junk_laserpointer: { name:'激光笔', icon:'🔴', price:35, type:'junk', color:'#ff4444' },
junk_magic7ball: { name:'魔法7球', icon:'🔮', price:40, type:'junk', color:'#9944dd' },
junk_magnifyingglass: { name:'放大镜', icon:'🔍', price:30, type:'junk', color:'#aaccdd' },
junk_metalsheet: { name:'金属板', icon:'🪟', price:45, type:'junk', color:'#999' },
junk_mug: { name:'杯子', icon:'☕', price:22, type:'junk', color:'#dd9966' },
junk_oldphone: { name:'旧电话', icon:'📞', price:38, type:'junk', color:'#666688' },
junk_painting: { name:'画', icon:'🖼️', price:50, type:'junk', color:'#cc8844' },
junk_perfume: { name:'香水瓶', icon:'🧴', price:42, type:'junk', color:'#ddaaff' },
junk_pill: { name:'药瓶', icon:'💊', price:30, type:'junk', color:'#ff88aa' },
junk_plasticfish: { name:'塑料鱼', icon:'🐠', price:28, type:'junk', color:'#ffaa44' },
junk_plate: { name:'盘子', icon:'🍽️', price:20, type:'junk', color:'#cccccc' },
junk_remote: { name:'遥控器', icon:'📱', price:38, type:'junk', color:'#666688' },
junk_ring: { name:'戒指', icon:'💍', price:75, type:'junk', color:'#ddeeff' },
junk_robottoy: { name:'机器人玩具', icon:'🤖', price:40, type:'junk', color:'#88aacc' },
junk_rubberduck: { name:'橡皮鸭', icon:'🦆', price:25, type:'junk', color:'#ffdd44' },
junk_sodacan: { name:'汽水罐', icon:'🥤', price:18, type:'junk', color:'#ff6644' },
junk_steeringwheel: { name:'方向盘', icon:'☸️', price:58, type:'junk', color:'#666666' },
junk_stopsign: { name:'停车标志', icon:'🛑', price:45, type:'junk', color:'#ff4444' },
junk_tatteredcape: { name:'破旧斗篷', icon:'🧣', price:35, type:'junk', color:'#884466' },
junk_teeth: { name:'牙齿', icon:'🦷', price:20, type:'junk', color:'#eeeeee' },
junk_tragedymask: { name:'悲剧面具', icon:'🎭', price:40, type:'junk', color:'#6666aa' },
junk_toycube: { name:'玩具方块', icon:'🧊', price:22, type:'junk', color:'#88ccff' },
junk_vase: { name:'花瓶', icon:'🏺', price:48, type:'junk', color:'#cc99cc' },
junk_whoopiecushion: { name:'放屁垫', icon:'💨', price:20, type:'junk', color:'#cc6688' },
junk_yieldsign: { name:'让行标志', icon:'⚠️', price:40, type:'junk', color:'#ffaa00' },
junk_zeddog: { name:'Zedd狗', icon:'🐕', price:35, type:'junk', color:'#aa7755' },
// ==================== 道具(商店购买,蓝色,不被清空)====================
tool_flashlight: { name:'手电筒', icon:'🔦', price:0, type:'tool', color:'#4488ff', desc:'基础照明,电量1分钟' },
tool_superflash: { name:'超强手电', icon:'🔦', price:500, type:'tool', color:'#44aaff', desc:'强光照明,电量3分钟' },
tool_radio: { name:'收音机', icon:'📻', price:150, type:'tool', color:'#4488ff', desc:'停止所有怪物1分钟' },
tool_gun: { name:'手枪', icon:'🔫', price:1000, type:'tool', color:'#4488ff', desc:'武器,需要子弹' },
tool_bullet: { name:'子弹', icon:'🔸', price:100, type:'tool', color:'#4488ff', desc:'手枪弹药x1' },
tool_backpack: { name:'小包', icon:'🎒', price:300, type:'tool', color:'#4488ff', desc:'+1物品栏,按5切换' },
tool_shovel: { name:'铲子', icon:'', price:300, type:'tool', color:'#4488ff', desc:'近战武器,空格攻击' },
tool_boombox: { name:'音箱', icon:'📻', price:120, type:'tool', color:'#4488ff', desc:'播放音乐吸引怪物' },
tool_stungrenade: { name:'眩晕手榴弹', icon:'💣', price:250, type:'tool', color:'#4488ff', desc:'空格投掷,眩晕怪物' },
tool_zapgun: { name:'电击枪', icon:'⚡', price:400, type:'tool', color:'#4488ff', desc:'电击麻痹怪物' },
tool_jetpack: { name:'喷气背包', icon:'🚀', price:1000, type:'tool', color:'#4488ff', desc:'提升移动速度' },
tool_extensionladder: { name:'伸缩梯', icon:'🪜', price:500, type:'tool', color:'#4488ff', desc:'探索道具' },
tool_radarbooster: { name:'雷达增强器', icon:'📡', price:300, type:'tool', color:'#4488ff', desc:'显示物资位置' },
tool_spraypaint: { name:'喷漆', icon:'🎨', price:200, type:'tool', color:'#4488ff', desc:'标记路径' },
tool_walkietalkie: { name:'对讲机', icon:'📟', price:200, type:'tool', color:'#4488ff', desc:'通讯道具' },
tool_hazardsuit: { name:'防护服', icon:'🦺', price:300, type:'tool', color:'#4488ff', desc:'减少受到的伤害' },
tool_lockpicker: { name:'开锁器', icon:'🔑', price:200, type:'tool', color:'#4488ff', desc:'开锁道具' },
tool_tzpinhalant: { name:'TZP吸入剂', icon:'💨', price:300, type:'tool', color:'#4488ff', desc:'临时提升速度' },
};
// 商店商品列表(超过8个自动分页)
const SHOP_ITEMS = [
'tool_superflash','tool_radio','tool_gun','tool_bullet',
'tool_backpack','tool_shovel','tool_boombox','tool_stungrenade',
'tool_zapgun','tool_jetpack','tool_extensionladder','tool_radarbooster',
'tool_spraypaint','tool_walkietalkie','tool_hazardsuit','tool_lockpicker',
'tool_tzpinhalant'
];
// ==================== 15种怪物定义(模仿致命公司)====================
const MONSTERS = [
{ id:'circuit_bee', name:'电路蜜蜂', icon:'🐝', hp:20, dmg:8, speed:2.2, color:'#ffcc00', behavior:'swarm', desc:'群体追击' },
{ id:'centipede', name:'巨型蜈蚣', icon:'🐛', hp:40, dmg:15, speed:1.5, color:'#884422', behavior:'ambush', desc:'伏击掉落' },
{ id:'coilhead', name:'线圈头', icon:'🗿', hp:60, dmg:25, speed:3.0, color:'#aa8855', behavior:'stare', desc:'你看它不动' },
{ id:'eyeless_dog', name:'瞎眼狗', icon:'🐕', hp:80, dmg:30, speed:2.8, color:'#555555', behavior:'sound', desc:'听声追击' },
{ id:'forest_keeper', name:'森林守护者', icon:'🌳', hp:120, dmg:40, speed:1.8, color:'#2d5a27', behavior:'patrol', desc:'高大巡逻' },
{ id:'giant', name:'巨人', icon:'👹', hp:150, dmg:50, speed:1.2, color:'#8b4513', behavior:'chase', desc:'缓慢但致命' },
{ id:'hoarding_bug', name:'地穴蝙蝠', icon:'🦇', hp:15, dmg:5, speed:3.5, color:'#4a235a', behavior:'steal', desc:'偷物资' },
{ id:'jester', name:'小丑盒', icon:'🤡', hp:70, dmg:35, speed:2.0, color:'#ff4444', behavior:'windup', desc:'蓄力后狂暴' },
{ id:'masked', name:'面具人', icon:'🎭', hp:50, dmg:20, speed:2.5, color:'#ffffff', behavior:'mimic', desc:'模仿玩家' },
{ id:'maneater', name:'泥怪', icon:'🟤', hp:90, dmg:28, speed:1.0, color:'#6b4423', behavior:'ground', desc:'地底突袭' },
{ id:'earth_leviathan', name:'沙虫', icon:'🪱', hp:200, dmg:60, speed:0.8, color:'#c0a080', behavior:'underground', desc:'地下吞噬' },
{ id:'spore_lizard', name:'孢子蜥蜴', icon:'🦎', hp:35, dmg:12, speed:2.0, color:'#556b2f', behavior:'spore', desc:'喷孢子减速' },
{ id:'bunker_spider', name:'地堡蜘蛛', icon:'🕷️', hp:45, dmg:18, speed:2.3, color:'#2f2f2f', behavior:'web', desc:'结网陷阱' },
{ id:'thumper', name:'冲刺虫', icon:'🦗', hp:30, dmg:16, speed:4.0, color:'#cd853f', behavior:'dash', desc:'高速冲刺' },
{ id:'ghost_girl', name:'幽灵女孩', icon:'👻', hp:999, dmg:999, speed:1.5, color:'#e6e6fa', behavior:'haunt', desc:'无法杀死,猎杀' },
];
// ==================== 地图名称 ====================
const MAP_NAMES = ['220-资产','47-决断','9-丁尼生','21-奥芬','12-黑森林'];
// ==================== 游戏状态 ====================
window.Game = {
state: 'menu', // menu, playing, shop, shipinv, gameover
canvas: null,
ctx: null,
mouse: { x: 0, y: 0, worldX: 0, worldY: 0, angle: 0 },
keys: {},
map: null,
mapName: '',
player: null,
monsters: [],
items: [], // 地图上的物品
droppedItems: [], // 丢弃的物品
shipPos: { x: 0, y: 0 },
gatePos: { x: 0, y: 0 },
insideArea: { x: 0, y: 0, w: 0, h: 0 },
taskAmount: 0,
collectedAmount: 0,
totalMoney: 0,
timeLeft: GAME_TIME,
firstGame: true,
shopPage: 0,
shipInventory: [null,null,null,null,null], // 5格永久存储
radioActive: false,
radioTimer: 0,
camera: { x: 0, y: 0 },
lightCanvas: null,
lightCtx: null,
lastTime: 0,
gameOverReason: '',
init() {
this.canvas = document.getElementById('game');
this.ctx = this.canvas.getContext('2d');
this.resize();
window.addEventListener('resize', () => this.resize());
// 光照离屏canvas
this.lightCanvas = document.createElement('canvas');
this.lightCtx = this.lightCanvas.getContext('2d');
// 输入
window.addEventListener('keydown', e => {
this.keys[e.key.toLowerCase()] = true;
// Escape关闭弹窗
if (e.key === 'Escape') {
if (this.state === 'shop') this.closeShop();
else if (this.state === 'shipinv') this.closeShipInv();
return;
}
// 菜单状态下按Enter开始游戏
if (this.state === 'menu' && e.key === 'Enter') {
this.startGame();
return;
}
this.handleKey(e.key.toLowerCase());
if (['w','a','s','d',' ','e','q','t','f','o','y','1','2','3','4','5'].includes(e.key.toLowerCase())) {
e.preventDefault();
}
});
window.addEventListener('keyup', e => { this.keys[e.key.toLowerCase()] = false; });
this.canvas.addEventListener('mousemove', e => {
const rect = this.canvas.getBoundingClientRect();
this.mouse.x = e.clientX - rect.left;
this.mouse.y = e.clientY - rect.top;
});
this.canvas.addEventListener('click', () => {
Audio.init();
});
// 显示菜单
document.getElementById('menuUI').style.display = 'flex';
requestAnimationFrame(t => this.loop(t));
},
resize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
},
showControls() {
const el = document.getElementById('controlsDetail');
el.style.display = el.style.display === 'none' ? 'block' : 'none';
},
// ==================== 开始游戏 ====================
startGame() {
Audio.init();
document.getElementById('menuUI').style.display = 'none';
document.getElementById('gameOverUI').style.display = 'none';
// 随机选择地图
const mapIdx = Math.floor(Math.random() * MAP_NAMES.length);
this.mapName = MAP_NAMES[mapIdx];
document.getElementById('mapName').textContent = this.mapName;
this.generateMap(mapIdx);
this.spawnPlayer();
this.spawnMonsters();
this.spawnJunk();
this.state = 'playing';
this.timeLeft = GAME_TIME;
this.collectedAmount = 0;
this.radioActive = false;
this.radioTimer = 0;
Audio.radioMusic.stop();
// 设置任务金额(不大于地图刷新的垃圾总值)
const totalJunkValue = this.items.filter(i => i.type === 'junk').reduce((s,i) => s + i.price, 0);
this.taskAmount = Math.min(Math.floor(totalJunkValue * (0.4 + Math.random() * 0.3)), totalJunkValue);
document.getElementById('taskAmount').textContent = '$' + this.taskAmount;
this.updateUI();
this.showMessage('欢迎来到 ' + this.mapName + '!进入大门收集物资,30分钟内撤离。', 4000);
},
// ==================== 地图生成 ====================
generateMap(mapIdx) {
// 初始化地图为外墙
this.map = [];
for (let y = 0; y < MAP_H; y++) {
this.map[y] = [];
for (let x = 0; x < MAP_W; x++) {
if (x === 0 || y === 0 || x === MAP_W-1 || y === MAP_H-1) {
this.map[y][x] = T_WALL;
} else {
this.map[y][x] = T_OUTSIDE;
}
}
}
// 内部区域(大门内)- 居中偏上
const inX = 15, inY = 10, inW = 50, inH = 45;
this.insideArea = { x: inX, y: inY, w: inW, h: inH };
// 填充内部为地板
for (let y = inY; y < inY + inH; y++) {
for (let x = inX; x < inX + inW; x++) {
this.map[y][x] = T_FLOOR;
}
}
// 内部墙壁(建筑结构)
const seed = mapIdx * 12345 + 67890;
let rng = seed;
const rand = () => { rng = (rng * 1103515245 + 12345) & 0x7fffffff; return rng / 0x7fffffff; };
// 生成房间墙壁
const numRooms = 8 + mapIdx * 2;
for (let i = 0; i < numRooms; i++) {
const rx = inX + 3 + Math.floor(rand() * (inW - 8));
const ry = inY + 3 + Math.floor(rand() * (inH - 8));
const rw = 4 + Math.floor(rand() * 6);
const rh = 3 + Math.floor(rand() * 5);
// 画房间墙(留门)
for (let x = rx; x < rx + rw && x < inX + inW - 1; x++) {
if (this.map[ry] && this.map[ry][x] === T_FLOOR) this.map[ry][x] = T_WALL;
if (this.map[ry+rh-1] && this.map[ry+rh-1][x] === T_FLOOR) this.map[ry+rh-1][x] = T_WALL;
}
for (let y = ry; y < ry + rh && y < inY + inH - 1; y++) {
if (this.map[y] && this.map[y][rx] === T_FLOOR) this.map[y][rx] = T_WALL;
if (this.map[y] && this.map[y][rx+rw-1] === T_FLOOR) this.map[y][rx+rw-1] = T_WALL;
}
// 留门
const doorSide = Math.floor(rand() * 4);
if (doorSide === 0 && this.map[ry]) this.map[ry][rx + Math.floor(rw/2)] = T_FLOOR;
if (doorSide === 1 && this.map[ry+rh-1]) this.map[ry+rh-1][rx + Math.floor(rw/2)] = T_FLOOR;
if (doorSide === 2 && this.map[ry]) this.map[ry + Math.floor(rh/2)][rx] = T_FLOOR;
if (doorSide === 3 && this.map[ry]) this.map[ry + Math.floor(rh/2)][rx+rw-1] = T_FLOOR;
}
// 随机障碍/柱子
for (let i = 0; i < 30; i++) {
const ox = inX + 2 + Math.floor(rand() * (inW - 4));
const oy = inY + 2 + Math.floor(rand() * (inH - 4));
if (this.map[oy] && this.map[oy][ox] === T_FLOOR) {
this.map[oy][ox] = T_BUILDING;
}
}
// 大门位置(内部区域底部中央)
const gateX = inX + Math.floor(inW / 2);
const gateY = inY + inH;
this.gatePos = { x: gateX, y: gateY };
// 在底部墙上开大门
for (let x = gateX - 1; x <= gateX + 1; x++) {
if (this.map[gateY] && x >= 0 && x < MAP_W) {
this.map[gateY][x] = T_GATE;
}
}
// 飞船位置 - 大门外,检查空旷
this.shipPos = this.findShipSpawn(gateX, gateY);
// 验证所有物资可达(在spawnJunk中检查)
},
findShipSpawn(gateX, gateY) {
// 在大门下方找空旷位置
for (let attempt = 0; attempt < 100; attempt++) {
const sx = gateX + Math.floor((Math.random() - 0.5) * 10);
const sy = gateY + 5 + Math.floor(Math.random() * 8);
if (sx < 2 || sx >= MAP_W-2 || sy < 2 || sy >= MAP_H-2) continue;
// 检查3x3区域是否空旷(无建筑、无墙)
let clear = true;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
const tile = this.map[sy+dy]?.[sx+dx];
if (tile === T_WALL || tile === T_BUILDING || tile === undefined) {
clear = false;
break;
}
}
if (!clear) break;
}
if (!clear) continue;
// 检查能否到达大门(简单直线检查)
let canReachGate = true;
for (let y = Math.min(sy, gateY); y <= Math.max(sy, gateY); y++) {
const tile = this.map[y]?.[sx];
if (tile === T_WALL || tile === T_BUILDING) { canReachGate = false; break; }
}
if (!canReachGate) continue;
// 检查能否上交物资(飞船位置本身可达)
return { x: sx, y: sy };
}
// 兜底:大门正下方
return { x: gateX, y: gateY + 6 };
},
// ==================== 生成玩家 ====================
spawnPlayer() {
this.player = {
x: this.shipPos.x * TILE + TILE/2,
y: this.shipPos.y * TILE + TILE/2,
speed: 3,
health: 100,
maxHealth: 100,
inventory: [null, null, null, null], // 4格
backpackSlot: null, // 小包额外1格
hasBackpack: false,
activeSlot: 0,
flashlightOn: false,
flashlightPower: 60, // 秒
maxFlashlightPower: 60,
hasSuperFlash: false,
hasGun: false,
bullets: 0,
facing: 0, // 角度
invulnTimer: 0,
};
// 第一局送手电筒
if (this.firstGame) {
this.player.inventory[0] = { ...ITEMS.tool_flashlight };
this.firstGame = false;
}
},
// ==================== 生成怪物 ====================
spawnMonsters() {
this.monsters = [];
const inArea = this.insideArea;
const numMonsters = 2 + Math.floor(Math.random() * 3); // 2-4个
const minDistBetween = 20; // 怪物之间最小距离(格),确保分散很远
for (let i = 0; i < numMonsters; i++) {
const mDef = MONSTERS[Math.floor(Math.random() * MONSTERS.length)];
let mx, my, attempts = 0;
let tooClose;
do {
mx = inArea.x + 2 + Math.floor(Math.random() * (inArea.w - 4));
my = inArea.y + 2 + Math.floor(Math.random() * (inArea.h - 4));
attempts++;
// 检查是否离其他怪物太近
tooClose = false;
for (const m of this.monsters) {
if (this.dist(mx, my, m.x/TILE, m.y/TILE) < minDistBetween) {
tooClose = true;
break;
}
}
} while ((this.map[my]?.[mx] !== T_FLOOR || this.dist(mx,my,this.gatePos.x,this.gatePos.y) < 10 || tooClose) && attempts < 100);
if (attempts >= 100) continue;
this.monsters.push({
...mDef,
x: mx * TILE + TILE/2,
y: my * TILE + TILE/2,
hp: mDef.hp * 2, // 血量x2
maxHp: mDef.hp * 2,
dmg: mDef.dmg * 2, // 伤害x2
vx: 0, vy: 0,
state: 'idle',
stateTimer: 0,
targetX: mx * TILE + TILE/2,
targetY: my * TILE + TILE/2,
windup: 0,
stunned: 0,
});
}
},
// ==================== 生成物资 ====================
spawnJunk() {
this.items = [];
this.droppedItems = [];
const inArea = this.insideArea;
const junkKeys = Object.keys(ITEMS).filter(k => ITEMS[k].type === 'junk');
const numJunk = 25 + Math.floor(Math.random() * 10);
for (let i = 0; i < numJunk; i++) {
const key = junkKeys[Math.floor(Math.random() * junkKeys.length)];
let jx, jy, attempts = 0;
do {
jx = inArea.x + 1 + Math.floor(Math.random() * (inArea.w - 2));
jy = inArea.y + 1 + Math.floor(Math.random() * (inArea.h - 2));
attempts++;
} while ((this.map[jy]?.[jx] !== T_FLOOR) && attempts < 50);
if (attempts >= 50) continue;
// 检查物资是否可达(从大门出发BFS简化检查)
if (!this.isReachable(this.gatePos.x, this.gatePos.y, jx, jy)) continue;
this.items.push({
...ITEMS[key],
id: key + '_' + i,
x: jx * TILE + TILE/2,
y: jy * TILE + TILE/2,
tileX: jx,
tileY: jy,
picked: false,
});
}
},
// 简单可达性检查(BFS)
isReachable(sx, sy, tx, ty) {
const visited = new Set();
const queue = [[sx, sy]];
visited.add(sx + ',' + sy);
let count = 0;
while (queue.length > 0 && count < 5000) {
const [x, y] = queue.shift();
if (x === tx && y === ty) return true;
const dirs = [[0,1],[0,-1],[1,0],[-1,0]];
for (const [dx, dy] of dirs) {
const nx = x + dx, ny = y + dy;
const key = nx + ',' + ny;
if (visited.has(key)) continue;
const tile = this.map[ny]?.[nx];
if (tile === T_WALL || tile === T_BUILDING || tile === undefined) continue;
visited.add(key);
queue.push([nx, ny]);
}
count++;
}
return false;
},
// ==================== 主循环 ====================
loop(timestamp) {
const dt = Math.min((timestamp - this.lastTime) / 1000, 0.1);
this.lastTime = timestamp;
if (this.state === 'playing') {
this.update(dt);
}
this.render();
requestAnimationFrame(t => this.loop(t));
},
// ==================== 更新 ====================
update(dt) {
// 计时器
this.timeLeft -= dt;
if (this.timeLeft <= 0) {
this.gameOver('时间耗尽,未能撤离!');
return;
}
// 玩家移动
this.updatePlayer(dt);
// 怪物更新
if (!this.radioActive) {
this.updateMonsters(dt);
} else {
this.radioTimer -= dt;
if (this.radioTimer <= 0) {
this.radioActive = false;
Audio.radioMusic.stop();
this.showMessage('收音机停止,怪物恢复活动!', 2500);
}
}
// 手电筒电量
if (this.player.flashlightOn && this.player.flashlightPower > 0) {
this.player.flashlightPower -= dt;
if (this.player.flashlightPower <= 0) {
this.player.flashlightPower = 0;
this.player.flashlightOn = false;
Audio.play('warning');
this.showMessage('手电筒电量耗尽!', 2000);
}
}
// 离飞船3步内充电
const distToShip = this.dist(this.player.x/TILE, this.player.y/TILE, this.shipPos.x, this.shipPos.y);
if (distToShip <= SHIP_REFILL_DIST) {
this.player.flashlightPower = this.player.maxFlashlightPower;
}
// 无敌时间
if (this.player.invulnTimer > 0) this.player.invulnTimer -= dt;
// 更新鼠标世界坐标和角度
this.mouse.worldX = this.mouse.x + this.camera.x;
this.mouse.worldY = this.mouse.y + this.camera.y;
this.mouse.angle = Math.atan2(this.mouse.worldY - this.player.y, this.mouse.worldX - this.player.x);
this.player.facing = this.mouse.angle;
// 摄像机跟随(玩家居中)
this.camera.x = this.player.x - this.canvas.width / 2;
this.camera.y = this.player.y - this.canvas.height / 2;
// UI更新
this.updateUI();
this.updateHint();
},
updatePlayer(dt) {
const p = this.player;
let dx = 0, dy = 0;
if (this.keys['w']) dy -= 1;
if (this.keys['s']) dy += 1;
if (this.keys['a']) dx -= 1;
if (this.keys['d']) dx += 1;
if (dx !== 0 || dy !== 0) {
const len = Math.sqrt(dx*dx + dy*dy);
dx /= len; dy /= len;
const newX = p.x + dx * p.speed * 60 * dt;
const newY = p.y + dy * p.speed * 60 * dt;
// 碰撞检测
if (!this.isWall(newX, p.y)) p.x = newX;
if (!this.isWall(p.x, newY)) p.y = newY;
// 地图边界
p.x = Math.max(TILE, Math.min(MAP_W * TILE - TILE, p.x));
p.y = Math.max(TILE, Math.min(MAP_H * TILE - TILE, p.y));
}
},
isWall(px, py) {
const tx = Math.floor(px / TILE);
const ty = Math.floor(py / TILE);
const tile = this.map[ty]?.[tx];
return tile === T_WALL || tile === T_BUILDING;
},
// ==================== 怪物AI ====================
updateMonsters(dt) {
const p = this.player;
const playerTileX = Math.floor(p.x / TILE);
const playerTileY = Math.floor(p.y / TILE);
const playerInside = this.isInside(playerTileX, playerTileY);
for (const m of this.monsters) {
if (m.stunned > 0) { m.stunned -= dt; continue; }
const mTileX = Math.floor(m.x / TILE);
const mTileY = Math.floor(m.y / TILE);
const dist = this.dist(m.x, m.y, p.x, p.y) / TILE;
// 怪物只在大门内活动
if (!this.isInside(mTileX, mTileY)) {
// 被引到外面了,往回走
const cx = this.insideArea.x + this.insideArea.w/2;
const cy = this.insideArea.y + this.insideArea.h/2;
this.moveMonster(m, cx * TILE, cy * TILE, dt);
continue;
}
// 只有玩家在大门内时才追击
if (!playerInside) {
// 巡逻
if (Math.random() < 0.01) {
m.targetX = (this.insideArea.x + Math.random() * this.insideArea.w) * TILE;
m.targetY = (this.insideArea.y + Math.random() * this.insideArea.h) * TILE;
}
this.moveMonster(m, m.targetX, m.targetY, dt);
continue;
}
// 各种行为
switch(m.behavior) {
case 'swarm': // 蜜蜂 - 群体追击
case 'chase': // 巨人 - 直接追击
case 'dash': // 冲刺虫
if (dist < 15) {
this.moveMonster(m, p.x, p.y, dt);
}
break;
case 'stare': // 线圈头 - 玩家看着就不动
const angleToMonster = Math.atan2(m.y - p.y, m.x - p.x);
const angleDiff = Math.abs(this.normAngle(angleToMonster - p.facing));
if (angleDiff < 0.5 && dist < 12) {
// 玩家看着它,不动
} else {
this.moveMonster(m, p.x, p.y, dt);
}
break;
case 'sound': // 瞎眼狗 - 听声
if (dist < 20) {
this.moveMonster(m, p.x, p.y, dt);
}
break;
case 'patrol': // 森林守护者 - 巡逻,靠近追击
if (dist < 10) {
this.moveMonster(m, p.x, p.y, dt);
} else {
if (Math.random() < 0.008) {
m.targetX = (this.insideArea.x + Math.random() * this.insideArea.w) * TILE;
m.targetY = (this.insideArea.y + Math.random() * this.insideArea.h) * TILE;
}
this.moveMonster(m, m.targetX, m.targetY, dt);
}
break;
case 'steal': // 地穴蝙蝠 - 偷物资
if (dist < 8) {
this.moveMonster(m, p.x, p.y, dt);
}
break;
case 'windup': // 小丑盒 - 蓄力后狂暴
if (dist < 12) {
m.windup += dt;
if (m.windup > 3) {
this.moveMonster(m, p.x, p.y, dt, m.speed * 2);
}
} else {
m.windup = Math.max(0, m.windup - dt);
}
break;
case 'mimic': // 面具人 - 模仿
if (dist < 15) {
this.moveMonster(m, p.x, p.y, dt);
}
break;
case 'ground': // 泥怪
case 'underground': // 沙虫
if (dist < 10) {
this.moveMonster(m, p.x, p.y, dt);
}
break;
case 'spore': // 孢子蜥蜴
if (dist < 12) {
this.moveMonster(m, p.x, p.y, dt);
}
break;
case 'web': // 蜘蛛
if (dist < 14) {
this.moveMonster(m, p.x, p.y, dt);
}
break;
case 'haunt': // 幽灵女孩 - 持续追击
this.moveMonster(m, p.x, p.y, dt);
break;
case 'ambush': // 蜈蚣
if (dist < 8) {
this.moveMonster(m, p.x, p.y, dt);
}
break;
}
// 攻击玩家
if (dist < 1.2 && p.invulnTimer <= 0) {
p.health -= m.dmg;
p.invulnTimer = 0.8;
Audio.play('hurt');
if (p.health <= 0) {
this.gameOver('你被 ' + m.name + ' 杀死了!');
return;
}
}
}
},
moveMonster(m, tx, ty, dt, speedMult = 1) {
const dx = tx - m.x;
const dy = ty - m.y;
const d = Math.sqrt(dx*dx + dy*dy);
if (d < 2) return;
const nx = m.x + (dx/d) * m.speed * speedMult * 60 * dt;
const ny = m.y + (dy/d) * m.speed * speedMult * 60 * dt;
if (!this.isWall(nx, m.y)) m.x = nx;
if (!this.isWall(m.x, ny)) m.y = ny;
},
isInside(tx, ty) {
const a = this.insideArea;
return tx >= a.x && tx < a.x + a.w && ty >= a.y && ty < a.y + a.h;
},
normAngle(a) {
while (a > Math.PI) a -= Math.PI * 2;
while (a < -Math.PI) a += Math.PI * 2;
return a;
},
dist(x1, y1, x2, y2) {
return Math.sqrt((x2-x1)**2 + (y2-y1)**2);
},
// ==================== 键盘处理 ====================
handleKey(key) {
if (this.state !== 'playing') return;
const p = this.player;
// 物品栏切换
if (key >= '1' && key <= '4') {
p.activeSlot = parseInt(key) - 1;
Audio.play('pickup');
}
if (key === '5' && p.hasBackpack) {
p.activeSlot = 4; // 小包槽
Audio.play('pickup');
}
// 手电筒
if (key === 'f') {
const item = p.activeSlot === 4 ? p.backpackSlot : p.inventory[p.activeSlot];
if (item && (item.name === '手电筒' || item.name === '超强手电')) {
if (p.flashlightPower > 0) {
p.flashlightOn = !p.flashlightOn;
Audio.play('flashlight');
}
}
}
// 拾取
if (key === 'e') {
this.pickupNearby();
}
// 上交物资
if (key === 'q') {
this.sellJunk();
}
// 丢弃
if (key === 't') {
this.dropItem();
}
// 飞船储物
if (key === 'o') {
const d = this.dist(p.x, p.y, this.shipPos.x * TILE, this.shipPos.y * TILE) / TILE;
if (d < 3) {
this.openShipInv();
} else {
this.showMessage('需要靠近飞船才能打开储物柜', 1500);
}
}
// 上车/撤离
if (key === 'y') {
const d = this.dist(p.x, p.y, this.shipPos.x * TILE, this.shipPos.y * TILE) / TILE;
if (d < 3) {
this.extract();
} else {
this.showMessage('需要靠近飞船才能上车撤离', 1500);
}
}
// 使用道具(空格)
if (key === ' ') {
this.useActiveItem();
}
},
// ==================== 拾取 ====================
pickupNearby() {
const p = this.player;
const px = Math.floor(p.x / TILE);
const py = Math.floor(p.y / TILE);
// 检查附近物资
for (const item of this.items) {
if (item.picked) continue;
const d = this.dist(p.x, p.y, item.x, item.y) / TILE;
if (d < 1.5) {
// 找空槽
const slot = this.findEmptySlot();
if (slot === -1) {
this.showMessage('物品栏已满!', 1500);
return;
}
if (slot === 4) {
p.backpackSlot = { ...item };
} else {
p.inventory[slot] = { ...item };
}
item.picked = true;
Audio.play('pickup');
this.showMessage('拾取: ' + item.name + ' ($' + item.price + ')', 1500);
return;
}
}
// 检查丢弃的物品
for (let i = this.droppedItems.length - 1; i >= 0; i--) {
const item = this.droppedItems[i];
const d = this.dist(p.x, p.y, item.x, item.y) / TILE;
if (d < 1.5) {
const slot = this.findEmptySlot();
if (slot === -1) {
this.showMessage('物品栏已满!', 1500);
return;
}
if (slot === 4) {
p.backpackSlot = { ...item };
} else {
p.inventory[slot] = { ...item };
}
this.droppedItems.splice(i, 1);
Audio.play('pickup');
this.showMessage('拾取: ' + item.name, 1500);
return;
}
}
},
findEmptySlot() {
const p = this.player;
for (let i = 0; i < 4; i++) {
if (!p.inventory[i]) return i;
}
if (p.hasBackpack && !p.backpackSlot) return 4;
return -1;
},
// ==================== 上交物资 ====================
sellJunk() {
const p = this.player;
// 检查是否在飞船旁
const d = this.dist(p.x, p.y, this.shipPos.x * TILE, this.shipPos.y * TILE) / TILE;
if (d > 3) {
this.showMessage('需要在飞船旁才能上交物资!', 1500);
return;
}
let sold = 0;
for (let i = 0; i < 4; i++) {
if (p.inventory[i] && p.inventory[i].type === 'junk') {
sold += p.inventory[i].price;
p.inventory[i] = null;
}
}
if (p.backpackSlot && p.backpackSlot.type === 'junk') {
sold += p.backpackSlot.price;
p.backpackSlot = null;
}
if (sold > 0) {
this.collectedAmount += sold;
this.totalMoney += sold;
Audio.play('sell');
this.showMessage('上交物资获得 $' + sold + '!总计: $' + this.collectedAmount, 2000);
} else {
this.showMessage('没有可上交的物资', 1500);
}
},
// ==================== 丢弃 ====================
dropItem() {
const p = this.player;
const item = p.activeSlot === 4 ? p.backpackSlot : p.inventory[p.activeSlot];
if (!item) {
this.showMessage('该槽位为空', 1000);
return;
}
// 收音机使用后直接丢地上(在useActiveItem中处理)
this.droppedItems.push({
...item,
x: p.x + Math.cos(p.facing) * 30,
y: p.y + Math.sin(p.facing) * 30,
});
if (p.activeSlot === 4) {
p.backpackSlot = null;
} else {
p.inventory[p.activeSlot] = null;
}
Audio.play('drop');
this.showMessage('丢弃: ' + item.name, 1200);
},
// ==================== 使用道具 ====================
useActiveItem() {
const p = this.player;
const item = p.activeSlot === 4 ? p.backpackSlot : p.inventory[p.activeSlot];
if (!item) return;
if (item.name === '收音机') {
// 不能在门外使用
const px = Math.floor(p.x / TILE);
const py = Math.floor(p.y / TILE);
if (!this.isInside(px, py)) {
this.showMessage('收音机不能在大门外使用!', 2000);
return;
}
// 使用后直接丢到地上
this.radioActive = true;
this.radioTimer = 60; // 1分钟
Audio.radioMusic.start();
Audio.play('radio');
// 丢到地上
this.droppedItems.push({
...item,
x: p.x,
y: p.y,
radioTimer: 60,
});
if (p.activeSlot === 4) {
p.backpackSlot = null;
} else {
p.inventory[p.activeSlot] = null;
}
this.showMessage('📻 收音机已启动!所有怪物停止活动1分钟', 3000);
}
if (item.name === '手枪') {
if (p.bullets <= 0) {
this.showMessage('没有子弹!', 1500);
return;
}
p.bullets--;
Audio.play('gunshot');
// 射击 - 检查鼠标方向上的怪物
const range = 8 * TILE;
for (const m of this.monsters) {
const d = this.dist(p.x, p.y, m.x, m.y);
if (d > range) continue;
const angle = Math.atan2(m.y - p.y, m.x - p.x);
const diff = Math.abs(this.normAngle(angle - p.facing));
if (diff < 0.3) {
m.hp -= 50;
m.stunned = 0.5;
if (m.hp <= 0) {
this.monsters = this.monsters.filter(x => x !== m);
this.showMessage('击杀: ' + m.name, 1500);
}
break;
}
}
}
},
// ==================== 撤离 ====================
extract() {
// 上交所有物资
this.sellJunk();
if (this.collectedAmount >= this.taskAmount) {
this.showMessage('任务完成!撤离成功,前往商店...', 2000);
} else {
this.showMessage('未完成任务金额,但仍可撤离。前往商店...', 2000);
}
setTimeout(() => {
this.openShop();
}, 1500);
},
// ==================== 商店 ====================
openShop() {
this.state = 'shop';
document.getElementById('shopUI').style.display = 'flex';
document.getElementById('shopMoney').textContent = this.totalMoney;
this.shopPage = 0;
this.renderShop();
},
closeShop() {
document.getElementById('shopUI').style.display = 'none';
this.state = 'playing';
// 回到飞船位置
this.player.x = this.shipPos.x * TILE + TILE/2;
this.player.y = this.shipPos.y * TILE + TILE/2;
this.showMessage('按 Y 上车出发下一张地图', 2500);
},
renderShop() {
const grid = document.getElementById('shopGrid');
const perPage = 8; // 2排 x 4列
const totalPages = Math.ceil(SHOP_ITEMS.length / perPage);
const start = this.shopPage * perPage;
const items = SHOP_ITEMS.slice(start, start + perPage);
grid.innerHTML = '';
for (const key of items) {
const item = ITEMS[key];
const div = document.createElement('div');
div.className = 'shop-item';
div.innerHTML = `<div class="icon">${item.icon}</div><div class="name">${item.name}</div><div class="price">$${item.price}</div>`;
div.onclick = () => this.buyItem(key);
grid.appendChild(div);
}
// 分页控制
const pageControls = document.getElementById('pageControls');
if (totalPages > 1) {
pageControls.style.display = 'flex';
document.getElementById('pageNum').textContent = (this.shopPage + 1) + '/' + totalPages;
document.getElementById('prevPage').disabled = this.shopPage === 0;
document.getElementById('nextPage').disabled = this.shopPage >= totalPages - 1;
} else {
pageControls.style.display = 'none';
}
},
shopPage(dir) {
const perPage = 8;
const totalPages = Math.ceil(SHOP_ITEMS.length / perPage);
this.shopPage = Math.max(0, Math.min(totalPages - 1, this.shopPage + dir));
this.renderShop();
},
buyItem(key) {
const item = ITEMS[key];
if (this.totalMoney < item.price) {
this.showMessage('余额不足!', 1500);
return;
}
// 特殊处理
if (key === 'tool_backpack') {
if (this.player.hasBackpack) {
this.showMessage('已经有小包了', 1500);
return;
}
this.player.hasBackpack = true;
this.totalMoney -= item.price;
Audio.play('buy');
this.showMessage('购买: 小包!按5切换小包槽位', 2000);
} else if (key === 'tool_bullet') {
this.player.bullets++;
this.totalMoney -= item.price;
Audio.play('buy');
this.showMessage('购买子弹!当前: ' + this.player.bullets + '发', 1500);
} else if (key === 'tool_gun') {
if (this.player.hasGun) {
this.showMessage('已经有手枪了', 1500);
return;
}
const slot = this.findEmptySlot();
if (slot === -1) {
this.showMessage('物品栏已满,无法购买!', 1500);
return;
}
this.player.hasGun = true;
if (slot === 4) this.player.backpackSlot = { ...item };
else this.player.inventory[slot] = { ...item };
this.totalMoney -= item.price;
Audio.play('buy');
this.showMessage('购买: 手枪!空格射击', 2000);
} else if (key === 'tool_superflash') {
const slot = this.findEmptySlot();
if (slot === -1) {
this.showMessage('物品栏已满,无法购买!', 1500);
return;
}
if (slot === 4) this.player.backpackSlot = { ...item };
else this.player.inventory[slot] = { ...item };
this.player.hasSuperFlash = true;
this.player.maxFlashlightPower = 180; // 3分钟
this.player.flashlightPower = 180;
this.totalMoney -= item.price;
Audio.play('buy');
this.showMessage('购买: 超强手电!电量3分钟', 2000);
} else if (key === 'tool_radio') {
const slot = this.findEmptySlot();
if (slot === -1) {
this.showMessage('物品栏已满,无法购买!', 1500);
return;
}
if (slot === 4) this.player.backpackSlot = { ...item };
else this.player.inventory[slot] = { ...item };
this.totalMoney -= item.price;
Audio.play('buy');
this.showMessage('购买: 收音机!空格使用', 2000);
}
document.getElementById('shopMoney').textContent = this.totalMoney;
this.renderShop();
},
// ==================== 飞船储物 ====================
openShipInv() {
this.state = 'shipinv';
document.getElementById('shipInvUI').style.display = 'flex';
this.renderShipInv();
},
closeShipInv() {
document.getElementById('shipInvUI').style.display = 'none';
this.state = 'playing';
},
renderShipInv() {
const grid = document.getElementById('shipGrid');
grid.innerHTML = '';
for (let i = 0; i < 5; i++) {
const item = this.shipInventory[i];
const div = document.createElement('div');
div.className = 'ship-slot';
if (item) {
div.innerHTML = `<div class="icon">${item.icon}</div><div style="font-size:10px">${item.name}</div>`;
div.onclick = () => this.takeFromShip(i);
} else {
div.innerHTML = '<div style="color:#555;font-size:10px">空</div>';
div.onclick = () => this.storeToShip(i);
}
grid.appendChild(div);
}
},
storeToShip(slotIdx) {
const p = this.player;
const item = p.activeSlot === 4 ? p.backpackSlot : p.inventory[p.activeSlot];
if (!item) {
this.showMessage('当前槽位为空', 1200);
return;
}
if (item.type === 'junk') {
this.showMessage('物资不能存入飞船,需要上交', 1500);
return;
}
this.shipInventory[slotIdx] = { ...item };
if (p.activeSlot === 4) p.backpackSlot = null;
else p.inventory[p.activeSlot] = null;
Audio.play('pickup');
this.renderShipInv();
this.showMessage('已存入飞船: ' + item.name, 1500);
},
takeFromShip(slotIdx) {
const p = this.player;
const item = this.shipInventory[slotIdx];
if (!item) return;
const slot = this.findEmptySlot();
if (slot === -1) {
this.showMessage('物品栏已满!', 1500);
return;
}
if (slot === 4) p.backpackSlot = { ...item };
else p.inventory[slot] = { ...item };
this.shipInventory[slotIdx] = null;
Audio.play('pickup');
this.renderShipInv();
this.showMessage('取出: ' + item.name, 1500);
},
// ==================== 游戏结束 ====================
gameOver(reason) {
this.state = 'gameover';
this.gameOverReason = reason;
Audio.radioMusic.stop();
document.getElementById('gameOverTitle').textContent = '任务失败';
document.getElementById('gameOverMsg').textContent = reason;
document.getElementById('finalCollected').textContent = this.collectedAmount;
document.getElementById('gameOverUI').style.display = 'flex';
},
backToMenu() {
document.getElementById('gameOverUI').style.display = 'none';
document.getElementById('menuUI').style.display = 'flex';
this.state = 'menu';
this.totalMoney = 0;
this.firstGame = true;
this.shipInventory = [null,null,null,null,null];
},
// ==================== UI更新 ====================
updateUI() {
const p = this.player;
document.getElementById('collectedAmount').textContent = '$' + this.collectedAmount;
const mins = Math.floor(this.timeLeft / 60);
const secs = Math.floor(this.timeLeft % 60);
document.getElementById('timer').textContent = mins + ':' + (secs < 10 ? '0' : '') + secs;
if (this.timeLeft < 60) {
document.getElementById('timer').style.color = '#ff4444';
}
// 血量
document.getElementById('healthFill').style.width = (p.health / p.maxHealth * 100) + '%';
// 手电筒电量
document.getElementById('flashlightFill').style.width = (p.flashlightPower / p.maxFlashlightPower * 100) + '%';
// 物品栏
const inv = document.getElementById('inventory');
inv.innerHTML = '';
for (let i = 0; i < 4; i++) {
const item = p.inventory[i];
const slot = document.createElement('div');
slot.className = 'slot' + (p.activeSlot === i ? ' active' : '') + (item && item.type === 'tool' ? ' tool' : '');
slot.innerHTML = `<span class="key">${i+1}</span>` +
(item ? `<div class="item-icon">${item.icon}</div><div class="item-name">${item.name}</div>` : '');
inv.appendChild(slot);
}
// 小包槽
if (p.hasBackpack) {
const item = p.backpackSlot;
const slot = document.createElement('div');
slot.className = 'slot' + (p.activeSlot === 4 ? ' active' : '') + (item && item.type === 'tool' ? ' tool' : '');
slot.innerHTML = `<span class="key">5</span>` +
(item ? `<div class="item-icon">${item.icon}</div><div class="item-name">${item.name}</div>` : '<div style="font-size:9px;color:#666">小包</div>');
inv.appendChild(slot);
}
// 子弹显示
if (p.hasGun) {
const bulletSlot = document.createElement('div');
bulletSlot.className = 'slot tool';
bulletSlot.innerHTML = `<div class="item-icon">🔸</div><div class="item-name">x${p.bullets}</div>`;
inv.appendChild(bulletSlot);
}
},
updateHint() {
const p = this.player;
const hint = document.getElementById('hint');
const px = Math.floor(p.x / TILE);
const py = Math.floor(p.y / TILE);
const distToShip = this.dist(p.x, p.y, this.shipPos.x * TILE, this.shipPos.y * TILE) / TILE;
const distToGate = this.dist(p.x, p.y, this.gatePos.x * TILE, this.gatePos.y * TILE) / TILE;
// 检查附近物资
let nearItem = false;
for (const item of this.items) {
if (!item.picked && this.dist(p.x, p.y, item.x, item.y) / TILE < 1.5) {
nearItem = true; break;
}
}
for (const item of this.droppedItems) {
if (this.dist(p.x, p.y, item.x, item.y) / TILE < 1.5) {
nearItem = true; break;
}
}
if (nearItem) {
hint.textContent = '按 E 拾取';
hint.style.display = 'block';
} else if (distToShip < 3) {
hint.textContent = '按 Q 上交物资 | 按 O 飞船储物 | 按 Y 上车撤离';
hint.style.display = 'block';
} else if (distToGate < 3 && !this.isInside(px, py)) {
hint.textContent = '进入大门 - 内部有怪物!';
hint.style.display = 'block';
} else {
hint.style.display = 'none';
}
},
showMessage(text, duration = 2000) {
const msg = document.getElementById('message');
msg.textContent = text;
msg.style.display = 'block';
clearTimeout(this._msgTimer);
this._msgTimer = setTimeout(() => { msg.style.display = 'none'; }, duration);
},
// ==================== 渲染 ====================
render() {
const ctx = this.ctx;
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
if (this.state === 'menu' || this.state === 'gameover') return;
ctx.save();
ctx.translate(-this.camera.x, -this.camera.y);
// 渲染地图
this.renderMap();
// 渲染物资
this.renderItems();
// 渲染飞船
this.renderShip();
// 渲染大门
this.renderGate();
// 渲染怪物(只有可见的)
this.renderMonsters();
// 渲染玩家
this.renderPlayer();
ctx.restore();
// 光照/黑雾覆盖
this.renderLighting();
},
renderMap() {
const ctx = this.ctx;
const startX = Math.max(0, Math.floor(this.camera.x / TILE));
const startY = Math.max(0, Math.floor(this.camera.y / TILE));
const endX = Math.min(MAP_W, Math.ceil((this.camera.x + this.canvas.width) / TILE) + 1);
const endY = Math.min(MAP_H, Math.ceil((this.camera.y + this.canvas.height) / TILE) + 1);
for (let y = startY; y < endY; y++) {
for (let x = startX; x < endX; x++) {
const tile = this.map[y]?.[x];
const px = x * TILE, py = y * TILE;
if (tile === T_WALL) {
ctx.fillStyle = '#3a3a3a';
ctx.fillRect(px, py, TILE, TILE);
ctx.fillStyle = '#2a2a2a';
ctx.fillRect(px + 2, py + 2, TILE - 4, TILE - 4);
} else if (tile === T_BUILDING) {
ctx.fillStyle = '#4a3a2a';
ctx.fillRect(px, py, TILE, TILE);
ctx.fillStyle = '#3a2a1a';
ctx.fillRect(px + 3, py + 3, TILE - 6, TILE - 6);
} else if (tile === T_GATE) {
ctx.fillStyle = '#664422';
ctx.fillRect(px, py, TILE, TILE);
ctx.fillStyle = '#886633';
ctx.fillRect(px + 2, py + 2, TILE - 4, TILE - 4);
} else if (tile === T_FLOOR) {
// 内部地板
ctx.fillStyle = '#1a1a1f';
ctx.fillRect(px, py, TILE, TILE);
if ((x + y) % 2 === 0) {
ctx.fillStyle = '#1e1e24';
ctx.fillRect(px, py, TILE, TILE);
}
} else {
// 外部地面
ctx.fillStyle = '#1a2a1a';
ctx.fillRect(px, py, TILE, TILE);
if ((x + y) % 3 === 0) {
ctx.fillStyle = '#1d2d1d';
ctx.fillRect(px, py, TILE, TILE);
}
}
}
}
// 画内部区域边界线
const a = this.insideArea;
ctx.strokeStyle = '#444466';
ctx.lineWidth = 2;
ctx.strokeRect(a.x * TILE, a.y * TILE, a.w * TILE, a.h * TILE);
},
renderItems() {
const ctx = this.ctx;
for (const item of this.items) {
if (item.picked) continue;
ctx.font = '20px serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 发光效果
ctx.shadowColor = item.color || '#ffd700';
ctx.shadowBlur = 8;
ctx.fillText(item.icon, item.x, item.y);
ctx.shadowBlur = 0;
}
// 丢弃的物品
for (const item of this.droppedItems) {
ctx.font = '20px serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = item.type === 'tool' ? '#4488ff' : '#ffd700';
ctx.shadowBlur = 8;
ctx.fillText(item.icon, item.x, item.y);
ctx.shadowBlur = 0;
}
},
renderShip() {
const ctx = this.ctx;
const sx = this.shipPos.x * TILE;
const sy = this.shipPos.y * TILE;
// 飞船主体
ctx.fillStyle = '#555566';
ctx.fillRect(sx - TILE, sy - TILE/2, TILE * 3, TILE * 2);
ctx.fillStyle = '#666677';
ctx.fillRect(sx - TILE + 4, sy - TILE/2 + 4, TILE * 3 - 8, TILE * 2 - 8);
// 窗户
ctx.fillStyle = '#88ccff';
ctx.fillRect(sx - 8, sy - 4, 16, 10);
// 文字
ctx.fillStyle = '#ffd700';
ctx.font = 'bold 10px monospace';
ctx.textAlign = 'center';
ctx.fillText('飞船', sx + TILE/2, sy + TILE + 8);
// 3步充电范围指示
ctx.strokeStyle = 'rgba(255,221,68,0.2)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(sx + TILE/2, sy + TILE/2, SHIP_REFILL_DIST * TILE, 0, Math.PI * 2);
ctx.stroke();
},
renderGate() {
const ctx = this.ctx;
const gx = this.gatePos.x * TILE;
const gy = this.gatePos.y * TILE;
ctx.fillStyle = '#884422';
ctx.font = 'bold 14px monospace';
ctx.textAlign = 'center';
ctx.fillText('⬇ 大门 ⬇', gx + TILE/2, gy - 8);
// 大门指示
ctx.strokeStyle = '#aa6633';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(gx - TILE, gy + TILE);
ctx.lineTo(gx + TILE * 2, gy + TILE);
ctx.stroke();
},
renderMonsters() {
const ctx = this.ctx;
for (const m of this.monsters) {
// 只渲染在视野范围内的
const d = this.dist(m.x, m.y, this.player.x, this.player.y) / TILE;
if (d > MOUSE_VIEW + 5) continue;
// 怪物身体
ctx.fillStyle = m.color;
ctx.beginPath();
ctx.arc(m.x, m.y, 14, 0, Math.PI * 2);
ctx.fill();
// 图标
ctx.font = '18px serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(m.icon, m.x, m.y);
// 血条
if (m.hp < m.maxHp) {
ctx.fillStyle = '#333';
ctx.fillRect(m.x - 15, m.y - 22, 30, 4);
ctx.fillStyle = '#ff4444';
ctx.fillRect(m.x - 15, m.y - 22, 30 * (m.hp / m.maxHp), 4);
}
// 小丑盒蓄力指示
if (m.behavior === 'windup' && m.windup > 0) {
ctx.fillStyle = '#ff0000';
ctx.font = '10px monospace';
ctx.fillText('⚠️', m.x, m.y - 28);
}
}
},
renderPlayer() {
const ctx = this.ctx;
const p = this.player;
// 无敌闪烁
if (p.invulnTimer > 0 && Math.floor(p.invulnTimer * 10) % 2 === 0) {
ctx.globalAlpha = 0.5;
}
// 身体
ctx.fillStyle = '#4488ff';
ctx.beginPath();
ctx.arc(p.x, p.y, 12, 0, Math.PI * 2);
ctx.fill();
// 朝向指示
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.lineTo(p.x + Math.cos(p.facing) * 18, p.y + Math.sin(p.facing) * 18);
ctx.stroke();
// 玩家图标
ctx.font = '14px serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('🧑', p.x, p.y);
ctx.globalAlpha = 1;
},
// ==================== 光照系统 ====================
renderLighting() {
const ctx = this.ctx;
const lctx = this.lightCtx;
// 设置光照canvas大小
if (this.lightCanvas.width !== this.canvas.width || this.lightCanvas.height !== this.canvas.height) {
this.lightCanvas.width = this.canvas.width;
this.lightCanvas.height = this.canvas.height;
}
// 全黑
lctx.fillStyle = 'rgba(0,0,0,0.97)';
lctx.fillRect(0, 0, this.lightCanvas.width, this.lightCanvas.height);
const p = this.player;
const cx = this.canvas.width / 2; // 玩家在屏幕中央
const cy = this.canvas.height / 2;
// 基础视野(3步圆形,柔和边缘)
const baseGrad = lctx.createRadialGradient(cx, cy, 0, cx, cy, VIEW_RADIUS * TILE);
baseGrad.addColorStop(0, 'rgba(0,0,0,0)');
baseGrad.addColorStop(0.7, 'rgba(0,0,0,0.3)');
baseGrad.addColorStop(1, 'rgba(0,0,0,0.97)');
lctx.globalCompositeOperation = 'destination-out';
lctx.fillStyle = baseGrad;
lctx.beginPath();
lctx.arc(cx, cy, VIEW_RADIUS * TILE, 0, Math.PI * 2);
lctx.fill();
// 鼠标方向视野(开手电20步,不开手电最多5步)
const mouseAngle = Math.atan2(this.mouse.y - cy, this.mouse.x - cx);
const spreadAngle = 0.6; // 扇形张角
const mouseViewDist = (p.flashlightOn && p.flashlightPower > 0) ? MOUSE_VIEW : MOUSE_VIEW_DARK;
lctx.beginPath();
lctx.moveTo(cx, cy);
lctx.arc(cx, cy, mouseViewDist * TILE, mouseAngle - spreadAngle, mouseAngle + spreadAngle);
lctx.closePath();
const mouseGrad = lctx.createRadialGradient(cx, cy, 0, cx, cy, mouseViewDist * TILE);
mouseGrad.addColorStop(0, 'rgba(0,0,0,0.8)');
mouseGrad.addColorStop(0.5, 'rgba(0,0,0,0.5)');
mouseGrad.addColorStop(1, 'rgba(0,0,0,0)');
lctx.fillStyle = mouseGrad;
lctx.fill();
// 手电筒 - 扩散式照明,撞墙停止
if (p.flashlightOn && p.flashlightPower > 0) {
this.renderFlashlight(lctx, cx, cy, p.facing);
}
lctx.globalCompositeOperation = 'source-over';
// 将光照层画到主canvas
ctx.drawImage(this.lightCanvas, 0, 0);
},
renderFlashlight(lctx, cx, cy, angle) {
const p = this.player;
const maxDist = (p.hasSuperFlash ? 15 : 12) * TILE;
const steps = 40; // 光线数量
const spread = 0.8; // 总扩散角度
// 画扩散光线(边缘线,隐藏),然后填充
const rays = [];
for (let i = 0; i <= steps; i++) {
const rayAngle = angle - spread/2 + (spread * i / steps);
// 光线传播,撞墙停止
let dist = maxDist;
const stepSize = 8;
for (let d = 0; d < maxDist; d += stepSize) {
const wx = p.x + Math.cos(rayAngle) * d;
const wy = p.y + Math.sin(rayAngle) * d;
if (this.isWall(wx, wy)) {
dist = d;
break;
}
}
// 离得越远扩散越大(通过增加光线间距实现)
rays.push({ angle: rayAngle, dist: dist });
}
// 填充光锥
lctx.globalCompositeOperation = 'destination-out';
lctx.beginPath();
lctx.moveTo(cx, cy);
for (const ray of rays) {
// 转换到屏幕坐标
const sx = cx + Math.cos(ray.angle) * ray.dist;
const sy = cy + Math.sin(ray.angle) * ray.dist;
lctx.lineTo(sx, sy);
}
lctx.closePath();
// 渐变填充 - 中心亮,边缘暗
const lightGrad = lctx.createRadialGradient(cx, cy, 0, cx, cy, maxDist);
lightGrad.addColorStop(0, 'rgba(0,0,0,1)');
lightGrad.addColorStop(0.4, 'rgba(0,0,0,0.9)');
lightGrad.addColorStop(0.8, 'rgba(0,0,0,0.5)');
lightGrad.addColorStop(1, 'rgba(0,0,0,0)');
lctx.fillStyle = lightGrad;
lctx.fill();
// 暖色叠加
lctx.globalCompositeOperation = 'source-over';
lctx.beginPath();
lctx.moveTo(cx, cy);
for (const ray of rays) {
const sx = cx + Math.cos(ray.angle) * ray.dist;
const sy = cy + Math.sin(ray.angle) * ray.dist;
lctx.lineTo(sx, sy);
}
lctx.closePath();
const warmGrad = lctx.createRadialGradient(cx, cy, 0, cx, cy, maxDist);
warmGrad.addColorStop(0, 'rgba(255,240,180,0.15)');
warmGrad.addColorStop(0.5, 'rgba(255,220,120,0.08)');
warmGrad.addColorStop(1, 'rgba(255,200,80,0)');
lctx.fillStyle = warmGrad;
lctx.fill();
},
};
// 启动
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => Game.init());
} else {
Game.init();
}
</script>
</body>
</html>Game Source: LETHAL 2D - 致命公司2D版
Creator: SolarWolf30
Libraries: none
Complexity: complex (2221 lines, 74.6 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: lethal-2d-2d-solarwolf30" to link back to the original. Then publish at arcadelab.ai/publish.