// ==UserScript==
// @name 几何决斗
// @namespace
// @version 42.5
// @author sunhy
// @description 修复:语法错误、属性面板保存、爆炸箭bug
// @match *://*/*
// @grant none
// @icon https://cdn.luogu.com.cn/upload/image_hosting/vbrmr14i.png
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
WIDTH: 1200,
HEIGHT: 800,
SIZE: 26,
SPEED: {
NORMAL: 10,
ATTACK: 5,
CHARGE: 2
},
ARROW_SPEED: 12,
COOLDOWNS: {
NORMAL: 200,
EXPLOSION: 300,
SPLIT: 400
},
CHARGE_TIME: 400,
AIM_LINE_TIME: 300,
MAX_PARTICLES: 200,
SHAKE_DURATION: 60
};
// 状态变量
let fishMode = true;
let gameMode = 'single';
let gameState = 'playing';
let screenShake = 0;
let shakeTimer = 0;
let winner = '';
let winningPlayer = '';
let aiMode = 'enhanced';
let cheatMode = false;
let cheatPanelVisible = false;
const game = {
keys: {},
p1: null, p2: null,
arrows: [],
aimLines: [],
particles: [],
bgOffset: 0,
frame: 0
};
// 箭矢类型
const arrowType = {
p1: 'normal',
p2: 'normal'
};
// 显示计时器
const typeDisplayTimer = {
p1: 0,
p2: 0
};
// 破解属性 - 默认值
const defaultCheatProperties = {
playerSpeed: 1.5,
arrowCooldown: 0.5,
arrowSpeed: 1.5,
knockbackForce: 1.5,
playerSize: 0.8,
arrowSize: 1.0,
knockbackResist: 0.5,
chargeTime: 0.5,
splitArrowCount: 3,
explosiveRadius: 1.5,
infiniteArrows: false,
arrowPenetration: false,
homingArrows: false,
invincible: false,
};
// 当前破解属性
const cheatProperties = { ...defaultCheatProperties };
// 单人模式属性备份
let singleModeProperties = { ...defaultCheatProperties };
// === 本地存储功能 ===
function saveCheatProperties() {
try {
localStorage.setItem('geometryDuel_cheatProperties', JSON.stringify(cheatProperties));
console.log('破解属性已保存');
} catch (error) {
console.error('保存属性失败:', error);
}
}
function loadCheatProperties() {
try {
const saved = localStorage.getItem('geometryDuel_cheatProperties');
if (saved) {
const loaded = JSON.parse(saved);
Object.keys(defaultCheatProperties).forEach(key => {
if (loaded[key] !== undefined) {
cheatProperties[key] = loaded[key];
}
});
console.log('破解属性已加载');
return true;
}
} catch (error) {
console.error('加载属性失败:', error);
}
return false;
}
// 保存单人模式属性
function saveSingleModeProperties() {
if (gameMode === 'single') {
singleModeProperties = { ...cheatProperties };
}
}
// 恢复单人模式属性
function restoreSingleModeProperties() {
if (gameMode === 'single') {
Object.assign(cheatProperties, singleModeProperties);
} else {
// 双人模式时重置为默认值
Object.assign(cheatProperties, defaultCheatProperties);
}
}
// === 本地存储功能结束 ===
// === 创建界面元素 ===
const container = document.createElement('div');
container.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:2147483647;background:#fff;opacity:0;pointer-events:none;';
const canvas = document.createElement('canvas');
canvas.width = CONFIG.WIDTH;
canvas.height = CONFIG.HEIGHT;
canvas.style.cssText = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);';
container.appendChild(canvas);
const modeBtn = document.createElement('div');
modeBtn.textContent = '单人';
modeBtn.style.cssText = 'position:absolute;top:20px;right:20px;padding:8px 16px;background:#000;color:#fff;font-family:monospace;font-size:14px;cursor:pointer;z-index:2147483648;opacity:0;pointer-events:none;';
modeBtn.onclick = (e) => {
e.stopPropagation();
// 保存当前模式的属性
saveSingleModeProperties();
// 切换模式
if (gameMode === 'single') {
gameMode = 'dual';
modeBtn.textContent = '双人';
} else if (gameMode === 'dual') {
gameMode = 'single';
modeBtn.textContent = '单人';
}
// 恢复对应模式的属性
restoreSingleModeProperties();
resetGame();
updateButtonVisibility();
};
container.appendChild(modeBtn);
// AI模式按钮
const aiModeBtn = document.createElement('div');
aiModeBtn.textContent = 'AI: 增强';
aiModeBtn.style.cssText = `
position: absolute;
top: 20px;
left: 20px;
padding: 8px 16px;
background: #000;
color: #fff;
font-family: monospace;
font-size: 14px;
cursor: pointer;
z-index: 2147483648;
opacity: 0;
pointer-events: none;
border-radius: 4px;
border: 2px solid #0ff;
`;
aiModeBtn.onclick = (e) => {
e.stopPropagation();
aiMode = aiMode === 'enhanced' ? 'simple' : 'enhanced';
aiModeBtn.textContent = `AI: ${aiMode === 'enhanced' ? '增强' : '简单'}`;
aiModeBtn.style.borderColor = aiMode === 'enhanced' ? '#0ff' : '#ff0';
};
container.appendChild(aiModeBtn);
// 破解模式按钮
const cheatModeBtn = document.createElement('div');
cheatModeBtn.textContent = '破解模式: 关';
cheatModeBtn.style.cssText = `
position: absolute;
top: 60px;
left: 20px;
padding: 8px 16px;
background: #000;
color: #0f0;
font-family: monospace;
font-size: 14px;
cursor: pointer;
z-index: 2147483648;
opacity: 0;
pointer-events: none;
border-radius: 4px;
border: 2px solid #0f0;
`;
cheatModeBtn.onclick = (e) => {
e.stopPropagation();
cheatMode = !cheatMode;
cheatModeBtn.textContent = `破解模式: ${cheatMode ? '开' : '关'}`;
cheatModeBtn.style.borderColor = cheatMode ? '#f00' : '#0f0';
cheatModeBtn.style.color = cheatMode ? '#f00' : '#0f0';
if (cheatMode) loadCheatProperties();
updateButtonVisibility();
cheatPanelVisible = false;
updateCheatPanel();
if (cheatMode && gameMode !== 'single') {
gameMode = 'single';
modeBtn.textContent = '单人';
restoreSingleModeProperties();
resetGame();
}
};
container.appendChild(cheatModeBtn);
// 属性面板按钮
const cheatPanelBtn = document.createElement('div');
cheatPanelBtn.textContent = '属性面板';
cheatPanelBtn.style.cssText = `
position: absolute;
top: 100px;
left: 20px;
padding: 8px 16px;
background: #000;
color: #ff0;
font-family: monospace;
font-size: 14px;
cursor: pointer;
z-index: 2147483648;
opacity: 0;
pointer-events: none;
border-radius: 4px;
border: 2px solid #ff0;
`;
cheatPanelBtn.onclick = (e) => {
e.stopPropagation();
cheatPanelVisible = !cheatPanelVisible;
updateCheatPanel();
};
container.appendChild(cheatPanelBtn);
// 属性面板
const cheatPanel = document.createElement('div');
cheatPanel.id = 'cheatPanel';
cheatPanel.style.cssText = `
position: absolute;
top: 140px;
left: 20px;
background: rgba(255, 255, 255, 0.95);
color: #000;
font-family: monospace;
font-size: 12px;
padding: 10px;
border-radius: 4px;
border: 2px solid #f00;
z-index: 2147483648;
opacity: 0;
pointer-events: none;
max-width: 300px;
max-height: 400px;
overflow-y: auto;
box-shadow: 0 0 10px rgba(0,0,0,0.3);
`;
container.appendChild(cheatPanel);
// 添加到页面
if (document.body) {
document.body.appendChild(container);
} else {
window.addEventListener('load', () => document.body.appendChild(container));
}
const ctx = canvas.getContext('2d');
// === 界面元素创建结束 ===
// === 辅助函数 ===
function updateButtonVisibility() {
if (fishMode) {
modeBtn.style.opacity = '0';
modeBtn.style.pointerEvents = 'none';
aiModeBtn.style.opacity = '0';
aiModeBtn.style.pointerEvents = 'none';
cheatModeBtn.style.opacity = '0';
cheatModeBtn.style.pointerEvents = 'none';
cheatPanelBtn.style.opacity = '0';
cheatPanelBtn.style.pointerEvents = 'none';
} else {
modeBtn.style.opacity = '1';
modeBtn.style.pointerEvents = 'auto';
aiModeBtn.style.opacity = gameMode === 'single' ? '1' : '0';
aiModeBtn.style.pointerEvents = gameMode === 'single' ? 'auto' : 'none';
cheatModeBtn.style.opacity = gameMode === 'single' ? '1' : '0';
cheatModeBtn.style.pointerEvents = gameMode === 'single' ? 'auto' : 'none';
cheatPanelBtn.style.opacity = (gameMode === 'single' && cheatMode) ? '1' : '0';
cheatPanelBtn.style.pointerEvents = (gameMode === 'single' && cheatMode) ? 'auto' : 'none';
}
}
// ESC隐藏/显示功能
function toggleFish() {
fishMode = !fishMode;
container.style.opacity = fishMode ? '0' : '1';
container.style.pointerEvents = fishMode ? 'none' : 'auto';
updateButtonVisibility();
cheatPanelVisible = false;
updateCheatPanel();
}
// 属性面板UI生成
function createSliderRow(label, property, value, min, max, step) {
return `
<div style="margin: 5px 0; display: flex; align-items: center;">
<span style="flex: 1; min-width: 80px;">${label}:</span>
<input type="range" id="slider_${property}" min="${min}" max="${max}" step="${step}" value="${value}" style="flex: 2; margin: 0 10px;">
<span id="value_${property}" style="width: 40px; text-align: right;">${value.toFixed(1)}</span>
</div>
`;
}
function createCheckboxRow(label, property, checked) {
return `
<div style="margin: 5px 0; display: flex; align-items: center;">
<input type="checkbox" id="checkbox_${property}" ${checked ? 'checked' : ''} style="margin-right: 8px;">
<label for="checkbox_${property}" style="flex: 1;">${label}</label>
</div>
`;
}
function updateCheatPanel() {
if (!cheatMode || !cheatPanelVisible || gameMode !== 'single') {
cheatPanel.style.opacity = '0';
cheatPanel.style.pointerEvents = 'none';
return;
}
// 加载当前属性
loadCheatProperties();
cheatPanel.style.opacity = '1';
cheatPanel.style.pointerEvents = 'auto';
let html = '<div style="margin-bottom: 10px; font-weight: bold; text-align: center; color: #f00;">破解属性面板</div>';
// 基础属性
html += '<div style="margin-bottom: 5px; font-weight: bold; border-bottom: 1px solid #ccc;">基础属性</div>';
html += createSliderRow('玩家速度', 'playerSpeed', cheatProperties.playerSpeed, 0.5, 3.0, 0.1);
html += createSliderRow('射击间隔', 'arrowCooldown', cheatProperties.arrowCooldown, 0.1, 1.0, 0.1);
html += createSliderRow('箭矢速度', 'arrowSpeed', cheatProperties.arrowSpeed, 0.5, 3.0, 0.1);
html += createSliderRow('击退力', 'knockbackForce', cheatProperties.knockbackForce, 0.5, 3.0, 0.1);
html += createSliderRow('玩家体积', 'playerSize', cheatProperties.playerSize, 0.5, 1.5, 0.1);
html += createSliderRow('箭矢体积', 'arrowSize', cheatProperties.arrowSize, 0.5, 2.0, 0.1);
html += createSliderRow('受击退', 'knockbackResist', cheatProperties.knockbackResist, 0.1, 2.0, 0.1);
html += createSliderRow('蓄力时间', 'chargeTime', cheatProperties.chargeTime, 0.1, 1.0, 0.1);
// 箭矢属性
html += '<div style="margin: 10px 0 5px 0; font-weight: bold; border-bottom: 1px solid #ccc;">箭矢属性</div>';
html += createSliderRow('分裂箭数量', 'splitArrowCount', cheatProperties.splitArrowCount, 2, 5, 1);
html += createSliderRow('爆炸范围', 'explosiveRadius', cheatProperties.explosiveRadius, 0.5, 3.0, 0.1);
// 特殊功能
html += '<div style="margin: 10px 0 5px 0; font-weight: bold; border-bottom: 1px solid #ccc;">特殊功能</div>';
html += createCheckboxRow('无限箭矢', 'infiniteArrows', cheatProperties.infiniteArrows);
html += createCheckboxRow('穿透箭矢', 'arrowPenetration', cheatProperties.arrowPenetration);
html += createCheckboxRow('追踪箭矢', 'homingArrows', cheatProperties.homingArrows);
html += createCheckboxRow('无敌模式', 'invincible', cheatProperties.invincible);
// 控制按钮
html += '<div style="margin-top: 15px; text-align: center;">';
html += '<button id="savePropertiesBtn" style="padding: 5px 15px; background: #4CAF50; color: white; border: none; border-radius: 3px; cursor: pointer; font-family: monospace;">保存</button>';
html += '<button id="resetPropertiesBtn" style="padding: 5px 15px; margin-left: 10px; background: #f44336; color: white; border: none; border-radius: 3px; cursor: pointer; font-family: monospace;">重置</button>';
html += '</div>';
html += '<div id="saveStatus" style="margin-top: 10px; font-size: 11px; color: #666; text-align: center;"></div>';
cheatPanel.innerHTML = html;
// 绑定事件
document.querySelectorAll('#cheatPanel input[type="range"]').forEach(slider => {
slider.addEventListener('input', (e) => {
const property = e.target.id.replace('slider_', '');
const value = parseFloat(e.target.value);
cheatProperties[property] = value;
e.target.nextElementSibling.textContent = value.toFixed(1);
saveCheatProperties();
});
});
document.querySelectorAll('#cheatPanel input[type="checkbox"]').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const property = e.target.id.replace('checkbox_', '');
cheatProperties[property] = e.target.checked;
saveCheatProperties();
});
});
// 保存按钮
const saveBtn = document.getElementById('savePropertiesBtn');
if (saveBtn) {
saveBtn.addEventListener('click', () => {
saveCheatProperties();
const statusEl = document.getElementById('saveStatus');
if (statusEl) {
statusEl.textContent = '属性已保存!';
setTimeout(() => { statusEl.textContent = ''; }, 2000);
}
});
}
// 重置按钮
const resetBtn = document.getElementById('resetPropertiesBtn');
if (resetBtn) {
resetBtn.addEventListener('click', () => {
Object.assign(cheatProperties, defaultCheatProperties);
saveCheatProperties();
updateCheatPanel();
const statusEl = document.getElementById('saveStatus');
if (statusEl) {
statusEl.textContent = '已重置为默认值!';
setTimeout(() => { statusEl.textContent = ''; }, 2000);
}
});
}
}
// === 数学工具类 ===
class Vec {
constructor(x, y) { this.x = x; this.y = y; }
add(v) { return new Vec(this.x + v.x, this.y + v.y); }
sub(v) { return new Vec(this.x - v.x, this.y - v.y); }
mult(n) { return new Vec(this.x * n, this.y * n); }
mag() { return Math.sqrt(this.x * this.x + this.y * this.y); }
norm() { const m = this.mag(); return m ? new Vec(this.x / m, this.y / m) : new Vec(0, 0); }
angle() { return Math.atan2(this.y, this.x); }
dist(v) { return Math.sqrt((this.x - v.x) ** 2 + (this.y - v.y) ** 2); }
}
// === 粒子系统 ===
function addSquareParticle(x, y, vx, vy, size, life, color = '#000') {
if (game.particles.length >= CONFIG.MAX_PARTICLES) return;
game.particles.push({
x, y, vx, vy, size, life, maxLife: life, color,
rot: Math.random() * Math.PI,
rotSpeed: (Math.random() - 0.5) * 0.2
});
}
function updateParticles() {
for (let i = game.particles.length - 1; i >= 0; i--) {
const p = game.particles[i];
p.x += p.vx;
p.y += p.vy;
p.vx *= 0.96;
p.vy *= 0.96;
p.rot += p.rotSpeed;
p.life -= 0.03;
if (p.life <= 0) game.particles.splice(i, 1);
}
}
function drawParticles(ctx) {
for (const p of game.particles) {
const a = p.life / p.maxLife;
ctx.globalAlpha = a * 0.9;
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rot);
const s = p.size * a;
ctx.fillStyle = p.color;
ctx.fillRect(-s / 2, -s / 2, s, s);
ctx.strokeStyle = '#000';
ctx.lineWidth = 1.5;
ctx.strokeRect(-s / 2, -s / 2, s, s);
ctx.restore();
}
ctx.globalAlpha = 1;
}
// === 游戏核心类 ===
class Arrow {
constructor(owner, pos, target, type = 'normal', offsetAngle = 0) {
this.owner = owner;
this.type = type;
this.x = pos.x;
this.y = pos.y;
this.spawnX = pos.x;
this.spawnY = pos.y;
let dir = target.sub(pos);
if (offsetAngle !== 0) {
const angle = dir.angle() + offsetAngle;
dir = new Vec(Math.cos(angle), Math.sin(angle));
} else {
dir = dir.norm();
}
// 基础速度
let speed = CONFIG.ARROW_SPEED;
// 修复bug:只在单人破解模式下应用箭矢速度增强
if (cheatMode && owner === 'p1' && gameMode === 'single') {
speed *= cheatProperties.arrowSpeed;
}
this.vx = dir.x * speed;
this.vy = dir.y * speed;
this.angle = dir.angle();
this.active = true;
this.life = 200;
// 修复bug:只在单人破解模式下应用箭矢体积
this.arrowSizeMultiplier = (cheatMode && owner === 'p1' && gameMode === 'single') ?
cheatProperties.arrowSize : 1.0;
// 爆炸箭属性
this.exploded = false;
// 分裂箭属性
this.isSplitChild = offsetAngle !== 0;
this.splitTarget = target;
this.splitCurveTime = 0;
// 修复bug:只在单人破解模式下应用特殊功能
this.homing = (cheatMode && cheatProperties.homingArrows && owner === 'p1' && gameMode === 'single');
this.penetration = (cheatMode && cheatProperties.arrowPenetration && owner === 'p1' && gameMode === 'single');
this.penetrated = false;
}
getTip() {
return {
x: this.x + Math.cos(this.angle) * 8 * this.arrowSizeMultiplier,
y: this.y + Math.sin(this.angle) * 8 * this.arrowSizeMultiplier
};
}
getTail() {
return {
x: this.x - Math.cos(this.angle) * 8 * this.arrowSizeMultiplier,
y: this.y - Math.sin(this.angle) * 8 * this.arrowSizeMultiplier
};
}
getHitRadius() {
return 10 * this.arrowSizeMultiplier;
}
// 爆炸效果 - 恢复上一版效果
createExplosion() {
if (this.exploded) return;
this.exploded = true;
this.active = false;
// 基础爆炸范围
const baseRadius = 200;
// 修复bug:只在单人破解模式下应用爆炸范围增强
const explosionRadius = (cheatMode && this.owner === 'p1' && gameMode === 'single') ?
baseRadius * cheatProperties.explosiveRadius : baseRadius;
const centerForce = 25;
// 产生黑色方形粒子
for(let i = 0; i < 40; i++) {
const angle = Math.random() * Math.PI * 2;
const dist = Math.random() * explosionRadius;
const x = this.x + Math.cos(angle) * dist;
const y = this.y + Math.sin(angle) * dist;
const force = centerForce * (1 - dist/explosionRadius) * 0.3;
addSquareParticle(
x, y,
Math.cos(angle) * force,
Math.sin(angle) * force,
3 + Math.random() * 4,
0.8,
'#000'
);
}
// 对玩家造成击退效果
const players = [game.p1, game.p2].filter(p => p);
for(const player of players) {
const dx = player.pos.x - this.x;
const dy = player.pos.y - this.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if(dist < explosionRadius) {
let force = centerForce * (1 - dist/explosionRadius);
// 修复bug:只在单人破解模式下增强击退力
if (cheatMode && this.owner === 'p1' && gameMode === 'single') {
force *= cheatProperties.knockbackForce;
}
const knockbackDir = new Vec(dx, dy).norm();
player.vel.x += knockbackDir.x * force;
player.vel.y += knockbackDir.y * force;
}
}
// 对其他箭矢造成影响
for(const other of game.arrows) {
if(other === this || !other.active) continue;
const dx = other.x - this.x;
const dy = other.y - this.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if(dist < explosionRadius) {
other.active = false;
for(let i = 0; i < 8; i++) {
const a = Math.random() * Math.PI * 2;
addSquareParticle(
other.x, other.y,
Math.cos(a)*2,
Math.sin(a)*2,
4, 0.7, '#000'
);
}
}
}
}
update() {
if (!this.active) return false;
// 追踪箭逻辑
if (this.homing && !this.isSplitChild) {
const targetPlayer = this.owner === 'p1' ? game.p2 : game.p1;
if (targetPlayer) {
const dx = targetPlayer.pos.x - this.x;
const dy = targetPlayer.pos.y - this.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
const targetDir = new Vec(dx, dy).norm();
const currentDir = new Vec(this.vx, this.vy).norm();
const blend = 0.1;
const newDir = new Vec(
currentDir.x * (1 - blend) + targetDir.x * blend,
currentDir.y * (1 - blend) + targetDir.y * blend
).norm();
const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
this.vx = newDir.x * speed;
this.vy = newDir.y * speed;
this.angle = newDir.angle();
}
}
}
// 分裂箭子箭的弧形飞行 - 恢复上一版效果
if (this.type === 'split' && this.isSplitChild) {
this.splitCurveTime += 0.02;
// 获取当前目标(实时的玩家位置)
const targetPlayer = this.owner === 'p1' ? game.p2 : game.p1;
if (targetPlayer) {
const currentTarget = targetPlayer.pos;
const dx = currentTarget.x - this.x;
const dy = currentTarget.y - this.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist > 0) {
// 计算向当前目标位置的方向
const targetDir = new Vec(dx, dy).norm();
const currentDir = new Vec(this.vx, this.vy).norm();
// 增强追踪修正,让子箭能跟上被击退的玩家
const blend = 0.1;
const newDir = new Vec(
currentDir.x * (1-blend) + targetDir.x * blend,
currentDir.y * (1-blend) + targetDir.y * blend
).norm();
const speed = Math.sqrt(this.vx*this.vx + this.vy*this.vy);
this.vx = newDir.x * speed;
this.vy = newDir.y * speed;
this.angle = newDir.angle();
// 更新目标位置
this.splitTarget = currentTarget;
}
}
}
this.x += this.vx;
this.y += this.vy;
this.life--;
// 边界检测
if (this.penetration) {
// 穿透箭矢
if (this.x < 0) this.x = CONFIG.WIDTH;
else if (this.x > CONFIG.WIDTH) this.x = 0;
if (this.y < 0) this.y = CONFIG.HEIGHT;
else if (this.y > CONFIG.HEIGHT) this.y = 0;
} else if (this.x < 0 || this.x > CONFIG.WIDTH || this.y < 0 || this.y > CONFIG.HEIGHT) {
if (this.type === 'explosion') {
this.createExplosion();
}
return false;
}
// 粒子效果
if (game.frame % 4 === 0) {
addSquareParticle(this.x, this.y, 0, 0, 2, 0.5, '#000');
}
// 寿命检查
if (this.life <= 0) {
if (this.type === 'explosion') {
this.createExplosion();
}
return false;
}
// 箭矢碰撞
const myTip = this.getTip();
for (const other of game.arrows) {
if (other === this || !other.active || other.owner === this.owner) continue;
const otherTip = other.getTip();
const tipDist = Math.sqrt((myTip.x - otherTip.x) ** 2 + (myTip.y - otherTip.y) ** 2);
if (tipDist < 10) {
// 碰撞粒子
for (let i = 0; i < 8; i++) {
const a = Math.random() * Math.PI * 2;
addSquareParticle(
(myTip.x + otherTip.x) / 2,
(myTip.y + otherTip.y) / 2,
Math.cos(a) * 2,
Math.sin(a) * 2,
4, 0.7, '#000'
);
}
other.active = false;
// 爆炸箭碰撞时爆炸
if (this.type === 'explosion' && !this.exploded) {
this.createExplosion();
}
if (other.type === 'explosion' && !other.exploded) {
other.createExplosion();
}
return false;
}
}
// 命中玩家
const target = this.owner === 'p1' ? game.p2 : game.p1;
const tip = this.getTip();
const dist = Math.sqrt((tip.x - target.pos.x) ** 2 + (tip.y - target.pos.y) ** 2);
if (dist < target.size) {
// 无敌模式检测
if (cheatMode && cheatProperties.invincible && target.id === 'p1' && gameMode === 'single') {
return false;
}
if (this.type === 'explosion' && !this.exploded) {
this.createExplosion();
} else {
// 普通击退
let knockbackForce = 15;
// 修复bug:只在单人破解模式下应用击退力增强
if (cheatMode && this.owner === 'p1' && gameMode === 'single') {
knockbackForce *= cheatProperties.knockbackForce;
}
const knockbackDir = new Vec(Math.cos(this.angle), Math.sin(this.angle));
// 修复bug:只在单人破解模式下应用受击退减弱
if (cheatMode && target.id === 'p1' && gameMode === 'single') {
knockbackForce *= cheatProperties.knockbackResist;
}
target.vel.x = knockbackDir.x * knockbackForce;
target.vel.y = knockbackDir.y * knockbackForce;
// 命中粒子
for (let i = 0; i < 12; i++) {
const a = Math.random() * Math.PI * 2;
addSquareParticle(tip.x, tip.y, Math.cos(a) * 2, Math.sin(a) * 2, 4, 0.9, '#000');
}
}
// 穿透箭矢不消失
if (!this.penetration) {
return false;
}
}
return true;
}
draw(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.angle);
ctx.strokeStyle = '#000';
ctx.fillStyle = '#000';
const sizeMult = this.arrowSizeMultiplier;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(-12 * sizeMult, 0);
ctx.lineTo(8 * sizeMult, 0);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(10 * sizeMult, 0);
ctx.lineTo(2 * sizeMult, 4 * sizeMult);
ctx.lineTo(2 * sizeMult, -4 * sizeMult);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(-10 * sizeMult, -3 * sizeMult);
ctx.lineTo(-14 * sizeMult, 0);
ctx.lineTo(-10 * sizeMult, 3 * sizeMult);
ctx.stroke();
ctx.restore();
}
}
class AimLine {
constructor(owner, pos, angle) {
this.owner = owner;
this.x = pos.x;
this.y = pos.y;
this.angle = angle;
this.life = Math.ceil(CONFIG.AIM_LINE_TIME / 16.67);
this.active = true;
this.maxLife = this.life;
}
update() {
const player = this.owner === 'p1' ? game.p1 : game.p2;
this.x = player.pos.x;
this.y = player.pos.y;
this.life--;
if (this.life <= 0) {
this.fireLaser();
return false;
}
return true;
}
fireLaser() {
const player = this.owner === 'p1' ? game.p1 : game.p2;
const target = this.owner === 'p1' ? game.p2 : game.p1;
const dir = new Vec(Math.cos(this.angle), Math.sin(this.angle));
let hit = false;
let laserLength = 2000;
for (let i = 0; i < 200; i++) {
const checkX = this.x + dir.x * i * 10;
const checkY = this.y + dir.y * i * 10;
if (checkX < 0 || checkX > CONFIG.WIDTH || checkY < 0 || checkY > CONFIG.HEIGHT) {
laserLength = i * 10;
break;
}
const dist = Math.sqrt((checkX - target.pos.x) ** 2 + (checkY - target.pos.y) ** 2);
if (dist < target.size) {
hit = true;
if (this.owner === 'p1') {
winner = 'p1';
winningPlayer = gameMode === 'single' ? '您' : 'P1';
} else {
winner = 'p2';
winningPlayer = gameMode === 'single' ? 'AI' : 'P2';
}
break;
}
}
screenShake = hit ? 30 : 15;
shakeTimer = CONFIG.SHAKE_DURATION;
if (hit) {
gameState = 'killcam';
}
}
draw(ctx) {
const dir = new Vec(Math.cos(this.angle), Math.sin(this.angle));
const endX = this.x + dir.x * 1200;
const endY = this.y + dir.y * 1200;
const progress = 1 - (this.life / this.maxLife);
const flash = 0.5 + Math.sin(Date.now() / 15) * 0.3 + progress * 0.4;
const alpha = Math.min(1, flash);
ctx.strokeStyle = `rgba(255,30,30,${alpha})`;
ctx.lineWidth = 2 + progress * 2;
ctx.setLineDash([10 - progress * 5, 5]);
ctx.beginPath();
ctx.moveTo(this.x, this.y);
ctx.lineTo(endX, endY);
ctx.stroke();
ctx.setLineDash([]);
}
}
class Player {
constructor(id, isAI) {
this.id = id;
this.isAI = isAI;
this.arrowType = id === 'p1' ? arrowType.p1 : arrowType.p2;
this.reset();
}
reset() {
this.pos = new Vec(this.id === 'p1' ? 150 : CONFIG.WIDTH - 150, this.id === 'p1' ? CONFIG.HEIGHT - 150 : 150);
this.vel = new Vec(0, 0);
this.size = CONFIG.SIZE;
this.rot = 0;
this.charging = false;
this.chargeStart = 0;
this.chargeLevel = 0;
this.chargeAng = 0;
this.aimAngle = 0;
this.lastAttack = 0;
this.aiTimer = 0;
this.canUlt = true;
}
get speed() {
if (this.charging) return CONFIG.SPEED.CHARGE;
if (cheatMode && this.id === 'p1' && gameMode === 'single') {
return CONFIG.SPEED.NORMAL * cheatProperties.playerSpeed;
}
return CONFIG.SPEED.NORMAL;
}
get displaySize() {
if (cheatMode && this.id === 'p1' && gameMode === 'single') {
return CONFIG.SIZE * cheatProperties.playerSize;
}
return CONFIG.SIZE;
}
getCooldown() {
if (this.arrowType === 'normal') return CONFIG.COOLDOWNS.NORMAL;
if (this.arrowType === 'explosion') return CONFIG.COOLDOWNS.EXPLOSION;
if (this.arrowType === 'split') return CONFIG.COOLDOWNS.SPLIT;
return CONFIG.COOLDOWNS.NORMAL;
}
update() {
this.pos.x += this.vel.x;
this.pos.y += this.vel.y;
const currentSize = this.displaySize;
if (this.pos.x < currentSize) {
this.pos.x = currentSize;
this.vel.x = Math.abs(this.vel.x) * 0.7;
} else if (this.pos.x > CONFIG.WIDTH - currentSize) {
this.pos.x = CONFIG.WIDTH - currentSize;
this.vel.x = -Math.abs(this.vel.x) * 0.7;
}
if (this.pos.y < currentSize) {
this.pos.y = currentSize;
this.vel.y = Math.abs(this.vel.y) * 0.7;
} else if (this.pos.y > CONFIG.HEIGHT - currentSize) {
this.pos.y = CONFIG.HEIGHT - currentSize;
this.vel.y = -Math.abs(this.vel.y) * 0.7;
}
this.vel.x *= 0.94;
this.vel.y *= 0.94;
if (Math.abs(this.vel.x) + Math.abs(this.vel.y) > 0.5) {
let targetRot = Math.atan2(this.vel.y, this.vel.x);
let diff = targetRot - this.rot;
while (diff > Math.PI) diff -= Math.PI * 2;
while (diff < -Math.PI) diff += Math.PI * 2;
this.rot += diff * 0.12;
}
if (this.charging) {
if (cheatMode && this.id === 'p1' && gameMode === 'single') {
this.chargeLevel = Math.min((Date.now() - this.chargeStart) / (CONFIG.CHARGE_TIME * cheatProperties.chargeTime), 1);
} else {
this.chargeLevel = Math.min((Date.now() - this.chargeStart) / CONFIG.CHARGE_TIME, 1);
}
this.chargeAng += 0.15;
const target = this.id === 'p1' ? game.p2.pos : game.p1.pos;
this.aimAngle = target.sub(this.pos).angle();
}
if (typeDisplayTimer[this.id] > 0) {
typeDisplayTimer[this.id]--;
}
}
move(dir) {
if (dir.mag() > 0) {
const want = dir.norm().mult(this.speed);
this.vel.x += (want.x - this.vel.x) * 0.15;
this.vel.y += (want.y - this.vel.y) * 0.15;
}
}
switchArrowType(newType) {
if (this.arrowType !== newType) {
this.arrowType = newType;
arrowType[this.id] = newType;
typeDisplayTimer[this.id] = 60;
}
}
cycleArrowType() {
const types = ['normal', 'explosion', 'split'];
const currentIndex = types.indexOf(this.arrowType);
const nextIndex = (currentIndex + 1) % types.length;
this.switchArrowType(types[nextIndex]);
}
shoot(target) {
let cooldown = this.getCooldown();
if (cheatMode && this.id === 'p1' && gameMode === 'single') {
cooldown *= cheatProperties.arrowCooldown;
}
if (!(cheatMode && cheatProperties.infiniteArrows && this.id === 'p1' && gameMode === 'single')) {
if (Date.now() - this.lastAttack < cooldown) return;
}
this.lastAttack = Date.now();
if (this.arrowType === 'split') {
// 恢复上一版分裂箭效果
const mainAngle = target.sub(this.pos).angle();
const dist = target.sub(this.pos).mag();
const predictTime = dist / CONFIG.ARROW_SPEED;
const predictTarget = new Vec(
target.x + (this.id === 'p1' ? game.p2.vel.x : game.p1.vel.x) * predictTime * 0.5,
target.y + (this.id === 'p1' ? game.p2.vel.y : game.p1.vel.y) * predictTime * 0.5
);
// 修复bug:只在单人破解模式下应用分裂箭数量增强
const splitCount = (cheatMode && this.id === 'p1' && gameMode === 'single') ?
cheatProperties.splitArrowCount : 3;
if (splitCount === 3) {
// 主箭
const mainArrow = new Arrow(this.id, this.pos, predictTarget, 'split');
game.arrows.push(mainArrow);
// 左侧子箭 (-15度)
const leftTarget = new Vec(
this.pos.x + Math.cos(mainAngle - Math.PI/12) * dist,
this.pos.y + Math.sin(mainAngle - Math.PI/12) * dist
);
const leftArrow = new Arrow(this.id, this.pos, leftTarget, 'split', -Math.PI/12);
leftArrow.isSplitChild = true;
leftArrow.splitTarget = predictTarget;
game.arrows.push(leftArrow);
// 右侧子箭 (+15度)
const rightTarget = new Vec(
this.pos.x + Math.cos(mainAngle + Math.PI/12) * dist,
this.pos.y + Math.sin(mainAngle + Math.PI/12) * dist
);
const rightArrow = new Arrow(this.id, this.pos, rightTarget, 'split', Math.PI/12);
rightArrow.isSplitChild = true;
rightArrow.splitTarget = predictTarget;
game.arrows.push(rightArrow);
} else if (splitCount === 2) {
// 2支箭
const angles = [-Math.PI/8, Math.PI/8];
for (const angle of angles) {
const adjustedTarget = new Vec(
this.pos.x + Math.cos(mainAngle + angle) * dist,
this.pos.y + Math.sin(mainAngle + angle) * dist
);
const arrow = new Arrow(this.id, this.pos, adjustedTarget, 'split', angle);
arrow.isSplitChild = true;
arrow.splitTarget = predictTarget;
game.arrows.push(arrow);
}
} else if (splitCount === 4) {
// 4支箭
const angles = [-Math.PI/6, -Math.PI/12, Math.PI/12, Math.PI/6];
for (const angle of angles) {
const adjustedTarget = new Vec(
this.pos.x + Math.cos(mainAngle + angle) * dist,
this.pos.y + Math.sin(mainAngle + angle) * dist
);
const arrow = new Arrow(this.id, this.pos, adjustedTarget, 'split', angle);
arrow.isSplitChild = true;
arrow.splitTarget = predictTarget;
game.arrows.push(arrow);
}
} else if (splitCount === 5) {
// 5支箭
const angles = [-Math.PI/6, -Math.PI/12, 0, Math.PI/12, Math.PI/6];
for (const angle of angles) {
const adjustedTarget = new Vec(
this.pos.x + Math.cos(mainAngle + angle) * dist,
this.pos.y + Math.sin(mainAngle + angle) * dist
);
const arrow = new Arrow(this.id, this.pos, adjustedTarget, 'split', angle);
arrow.isSplitChild = true;
arrow.splitTarget = predictTarget;
game.arrows.push(arrow);
}
}
} else {
game.arrows.push(new Arrow(this.id, this.pos, target, this.arrowType));
}
}
startCharge() {
if (this.charging) return;
this.charging = true;
this.chargeStart = Date.now();
const target = this.id === 'p1' ? game.p2.pos : game.p1.pos;
this.aimAngle = target.sub(this.pos).angle();
}
releaseCharge() {
if (!this.charging) return;
const full = this.chargeLevel >= 1;
this.charging = false;
this.chargeLevel = 0;
if (full && this.canUlt) game.aimLines.push(new AimLine(this.id, this.pos, this.aimAngle));
}
draw(ctx) {
ctx.save();
ctx.translate(this.pos.x, this.pos.y);
ctx.rotate(this.rot);
const currentSize = this.displaySize;
if (this.id === 'p1') {
ctx.fillStyle = '#fff';
ctx.fillRect(-currentSize / 2, -currentSize / 2, currentSize, currentSize);
ctx.strokeStyle = '#000';
ctx.lineWidth = 2.5;
ctx.strokeRect(-currentSize / 2, -currentSize / 2, currentSize, currentSize);
} else {
ctx.fillStyle = '#000';
ctx.fillRect(-currentSize / 2, -currentSize / 2, currentSize, currentSize);
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2.5;
ctx.strokeRect(-currentSize / 2, -currentSize / 2, currentSize, currentSize);
}
ctx.restore();
if (typeDisplayTimer[this.id] > 0) {
const typeText = {
'normal': '普通',
'explosion': '爆炸',
'split': '分裂'
}[this.arrowType] || '普通';
ctx.fillStyle = this.id === 'p1' ?
`rgba(0,0,0,0.8)` :
`rgba(255,255,255,0.8)`;
ctx.font = 'bold 16px monospace';
ctx.textAlign = 'center';
ctx.fillText(typeText, this.pos.x, this.pos.y - 40);
}
if (this.charging) {
const r = currentSize + 16;
const alpha = 0.4 + this.chargeLevel * 0.6;
ctx.save();
ctx.translate(this.pos.x, this.pos.y);
ctx.rotate(this.chargeAng);
ctx.strokeStyle = `rgba(0,0,0,${alpha})`;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(0, 0, r, 0.2, Math.PI * 0.8);
ctx.stroke();
ctx.rotate(-this.chargeAng * 1.5);
ctx.strokeStyle = `rgba(0,0,0,${alpha * 0.8})`;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(0, 0, r - 8, Math.PI * 1.2, -0.2);
ctx.stroke();
ctx.restore();
if (this.chargeLevel >= 1) {
ctx.fillStyle = `rgba(200,0,0,${0.5 + Math.sin(Date.now() / 20) * 0.5})`;
ctx.font = 'bold 18px monospace';
ctx.textAlign = 'center';
ctx.fillText('MAX', this.pos.x, this.pos.y - 50);
}
}
}
}
// === 游戏逻辑函数 ===
function resetGame() {
game.p1 = new Player('p1', false);
game.p2 = new Player('p2', gameMode === 'single');
if (cheatMode) {
loadCheatProperties();
}
game.arrows = [];
game.aimLines = [];
game.particles = [];
gameState = 'playing';
screenShake = 0;
shakeTimer = 0;
winner = '';
winningPlayer = '';
updateButtonVisibility();
cheatPanelVisible = false;
updateCheatPanel();
}
function update() {
if (gameState === 'killcam') {
if (shakeTimer > 0) {
shakeTimer--;
if (shakeTimer <= 0) screenShake = 0;
} else screenShake = 0;
updateParticles();
return;
}
if (gameState !== 'playing') return;
game.frame++;
if (shakeTimer > 0) {
shakeTimer--;
if (shakeTimer <= 0) screenShake = 0;
} else if (screenShake > 0) {
screenShake *= 0.9;
if (screenShake < 0.5) screenShake = 0;
}
if (game.p1) {
let moveDir = new Vec(0, 0);
if (game.keys['w']) moveDir.y -= 1;
if (game.keys['s']) moveDir.y += 1;
if (game.keys['a']) moveDir.x -= 1;
if (game.keys['d']) moveDir.x += 1;
game.p1.move(moveDir);
game.p1.update();
}
if (game.p2) {
if (game.p2.isAI) {
// 简化AI逻辑
const target = game.p1.pos;
const dx = target.x - game.p2.pos.x;
const dy = target.y - game.p2.pos.y;
const dist = Math.sqrt(dx * dx + dy * dy);
let moveDir = new Vec(0, 0);
if (dist > 250) {
moveDir = new Vec(dx, dy).norm().mult(0.6);
} else if (dist < 100) {
moveDir = new Vec(-dx, -dy).norm().mult(0.4);
} else {
const sideDir = new Vec(-dy, dx);
if (Math.random() > 0.5) sideDir.mult(-1);
moveDir = sideDir.norm().mult(0.3);
}
game.p2.move(moveDir);
if (game.p2.aiTimer++ > 12 && dist > 80 && dist < 400) {
if (Math.random() < 0.2) {
game.p2.shoot(target);
game.p2.aiTimer = 0;
}
}
} else {
let moveDir = new Vec(0, 0);
if (game.keys['i']) moveDir.y -= 1;
if (game.keys['k']) moveDir.y += 1;
if (game.keys['j']) moveDir.x -= 1;
if (game.keys['l']) moveDir.x += 1;
game.p2.move(moveDir);
}
game.p2.update();
}
game.arrows = game.arrows.filter(a => a.update());
game.aimLines = game.aimLines.filter(l => l.update());
updateParticles();
}
function draw() {
// 注意:这里没有fishMode检查,由CSS控制显示/隐藏
let sx = 0, sy = 0;
if (screenShake > 0) {
sx = (Math.random() - 0.5) * screenShake;
sy = (Math.random() - 0.5) * screenShake;
}
ctx.save();
ctx.translate(sx, sy);
// 背景
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, CONFIG.WIDTH, CONFIG.HEIGHT);
// 网格
ctx.strokeStyle = '#ddd';
ctx.lineWidth = 1;
game.bgOffset = (game.bgOffset + 0.5) % 40;
for (let i = -40; i < CONFIG.WIDTH + 40; i += 40) {
ctx.beginPath();
ctx.moveTo(i - game.bgOffset, 0);
ctx.lineTo(i - game.bgOffset, CONFIG.HEIGHT);
ctx.stroke();
}
for (let i = -40; i < CONFIG.HEIGHT + 40; i += 40) {
ctx.beginPath();
ctx.moveTo(0, i - game.bgOffset);
ctx.lineTo(CONFIG.WIDTH, i - game.bgOffset);
ctx.stroke();
}
// 游戏对象
if (game.p1) game.p1.draw(ctx);
if (game.p2) game.p2.draw(ctx);
for (const arrow of game.arrows) arrow.draw(ctx);
for (const line of game.aimLines) line.draw(ctx);
// 粒子
drawParticles(ctx);
// UI
ctx.fillStyle = '#666';
ctx.font = '14px monospace';
ctx.textAlign = 'left';
if (gameMode === 'single') {
ctx.fillText('P1: WASD移动 | J射击 | K蓄力 | L切换箭矢', 20, 30);
ctx.fillText(`AI模式: ${aiMode === 'enhanced' ? '增强' : '简单'}`, 20, 50);
if (game.p1) {
const arrowTypeText = {
'normal': '普通',
'explosion': '爆炸',
'split': '分裂'
};
ctx.fillText(`当前箭矢: ${arrowTypeText[game.p1.arrowType]}`, 20, 70);
}
if (cheatMode) {
ctx.fillText('破解模式: 开', 20, 90);
} else {
ctx.fillText('破解模式: 关', 20, 90);
}
} else {
ctx.fillText('P1: WASD移动 | Q射击 | E蓄力 | X切换', 20, 30);
ctx.fillText('P2: IJKL移动 | U射击 | O蓄力 | M切换', 20, 50);
}
ctx.fillText('ESC隐藏/显示 | 点击右上角切换单/双人', 20, gameMode === 'single' ? 110 : 70);
// 标注
ctx.fillStyle = 'rgba(128, 128, 128, 0.6)';
ctx.font = '12px monospace';
ctx.textAlign = 'right';
ctx.fillText('出自:sunhy,洛谷:862592,有意者私信', CONFIG.WIDTH - 20, CONFIG.HEIGHT - 20);
// KO画面
if (gameState === 'killcam') {
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(0, 0, CONFIG.WIDTH, CONFIG.HEIGHT);
ctx.fillStyle = '#fff';
ctx.font = 'bold 60px monospace';
ctx.textAlign = 'center';
ctx.fillText('KO', CONFIG.WIDTH / 2, CONFIG.HEIGHT / 2 - 20);
ctx.font = 'bold 40px monospace';
if (winner === 'p1') {
ctx.fillText(gameMode === 'single' ? '🎉 您赢了!' : '🎉 P1 赢了!', CONFIG.WIDTH / 2, CONFIG.HEIGHT / 2 + 40);
} else if (winner === 'p2') {
ctx.fillText(gameMode === 'single' ? '💀 您输了!' : '🎉 P2 赢了!', CONFIG.WIDTH / 2, CONFIG.HEIGHT / 2 + 40);
}
ctx.font = '20px monospace';
ctx.fillText('按空格键重新开始', CONFIG.WIDTH / 2, CONFIG.HEIGHT / 2 + 100);
}
ctx.restore();
}
// === 事件监听 ===
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
e.preventDefault();
toggleFish();
return;
}
if (fishMode) return;
if (gameState === 'killcam') {
if (e.code === 'Space') resetGame();
return;
}
const k = e.key.toLowerCase();
game.keys[k] = true;
if (gameState !== 'playing') return;
// 箭矢类型切换
if (gameMode === 'single') {
if (k === 'l' && game.p1) {
e.preventDefault();
game.p1.cycleArrowType();
}
} else {
if (k === 'x' && game.p1) {
e.preventDefault();
game.p1.cycleArrowType();
}
if (k === 'm' && game.p2 && !game.p2.isAI) {
e.preventDefault();
game.p2.cycleArrowType();
}
}
// 射击控制
if (gameMode === 'single') {
if (game.p1) {
if (k === 'j') {
e.preventDefault();
game.p1.shoot(game.p2.pos);
}
if (k === 'k' && !game.p1.charging) {
e.preventDefault();
game.p1.startCharge();
}
}
} else {
if (game.p1) {
if (k === 'q') {
e.preventDefault();
game.p1.shoot(game.p2.pos);
}
if (k === 'e' && !game.p1.charging) {
e.preventDefault();
game.p1.startCharge();
}
}
if (gameMode === 'dual' && game.p2 && !game.p2.isAI) {
if (k === 'u') {
e.preventDefault();
game.p2.shoot(game.p1.pos);
}
if (k === 'o' && !game.p2.charging) {
e.preventDefault();
game.p2.startCharge();
}
}
}
});
document.addEventListener('keyup', (e) => {
const k = e.key.toLowerCase();
game.keys[k] = false;
if (fishMode) return;
if (gameState === 'killcam') return;
if (gameMode === 'single' && k === 'k' && game.p1 && game.p1.charging) {
game.p1.releaseCharge();
}
if (gameMode === 'dual') {
if (k === 'e' && game.p1 && game.p1.charging) {
game.p1.releaseCharge();
}
if (k === 'o' && game.p2 && game.p2.charging && !game.p2.isAI) {
game.p2.releaseCharge();
}
}
});
// === 游戏启动 ===
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
window.addEventListener('load', () => {
loadCheatProperties();
// 初始化单人模式属性备份
singleModeProperties = { ...cheatProperties };
resetGame();
loop();
});
// 窗口大小调整
function resize() {
const s = Math.min(window.innerWidth / CONFIG.WIDTH, window.innerHeight / CONFIG.HEIGHT) * 0.98;
canvas.style.width = (CONFIG.WIDTH * s) + 'px';
canvas.style.height = (CONFIG.HEIGHT * s) + 'px';
}
window.addEventListener('resize', resize);
resize();
})();