矛盾升级:皇室战争
by HyperShark772145 lines85.5 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>矛盾升级:皇室战争</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #050810;
display: flex; justify-content: center; align-items: center;
min-height: 100vh; font-family: 'Microsoft YaHei', sans-serif;
color: #fff; overflow: hidden;
}
#gameContainer {
position: relative;
box-shadow: 0 0 60px rgba(0, 200, 255, 0.35);
border-radius: 10px; overflow: hidden;
}
canvas { display: block; cursor: pointer; position: relative; z-index: 1; }
#ui {
position: absolute; top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none; z-index: 10;
}
.screen {
position: absolute; top: 0; left: 0;
width: 100%; height: 100%;
display: flex; flex-direction: column;
justify-content: center; align-items: center;
background: rgba(5, 10, 25, 0.96);
pointer-events: all;
text-align: center; padding: 20px; z-index: 20;
overflow-y: auto;
}
.screen.hidden { display: none; pointer-events: none; }
h1 {
font-size: 38px; letter-spacing: 4px;
background: linear-gradient(90deg, #ff3d71, #ffaa00, #00e5ff);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
margin-bottom: 8px;
}
h2 { font-size: 17px; color: #00e5ff; margin-bottom: 14px; letter-spacing: 4px; }
p { font-size: 14px; color: #a0b0c0; margin: 5px 0; line-height: 1.6; }
button {
margin-top: 10px; padding: 12px 32px; font-size: 15px;
font-family: inherit; font-weight: bold; letter-spacing: 1px;
color: #001018; border: none; border-radius: 8px; cursor: pointer;
background: linear-gradient(90deg, #00e5ff, #00ffa0);
transition: transform 0.15s, box-shadow 0.15s;
pointer-events: all; position: relative; z-index: 30;
}
button:hover:not(:disabled) { transform: scale(1.06); box-shadow: 0 0 25px rgba(0, 229, 255, 0.7); }
button:disabled {
background: rgba(60, 70, 80, 0.6);
color: #4a5a60;
cursor: not-allowed; transform: none;
}
button.small { padding: 6px 14px; font-size: 12px; margin-top: 0; }
.controls-info { margin-top: 16px; font-size: 13px; color: #708090; line-height: 1.9; }
.controls-info b { color: #00e5ff; }
.alliance {
margin: 6px 0; padding: 12px 18px;
background: rgba(20, 30, 50, 0.6);
border-radius: 10px; font-size: 14px;
display: inline-block; max-width: 780px;
}
.alliance b { letter-spacing: 1px; }
.allianceNameRow {
display: flex; align-items: center; gap: 10px;
justify-content: center; margin-bottom: 8px;
}
.allianceNameRow input {
width: 180px; padding: 6px 12px;
font-family: inherit; font-size: 15px; font-weight: bold;
color: #fff; background: rgba(0, 0, 0, 0.5);
border: 2px solid rgba(0, 200, 255, 0.5);
border-radius: 8px; text-align: center;
outline: none; pointer-events: all;
}
.allianceNameRow input:focus { border-color: #00e5ff; box-shadow: 0 0 15px rgba(0, 229, 255, 0.6); }
.allianceNameRow input.enemy { border-color: rgba(255, 61, 113, 0.5); }
.allianceNameRow input.enemy:focus { border-color: #ff3d71; box-shadow: 0 0 15px rgba(255, 61, 113, 0.6); }
.allianceNameRow input.empty { border-color: rgba(255, 200, 0, 0.8); animation: pulseEmpty 1.2s infinite; }
.nameList {
display: flex; flex-wrap: wrap; gap: 6px;
justify-content: center; margin-top: 8px;
}
.nameList input {
width: 88px; padding: 4px 8px;
font-family: inherit; font-size: 12px;
color: #fff; background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(0, 200, 255, 0.4);
border-radius: 5px; text-align: center;
outline: none; pointer-events: all;
}
.nameList input:focus { border-color: #00e5ff; box-shadow: 0 0 10px rgba(0, 229, 255, 0.5); }
.nameList input.enemy { border-color: rgba(255, 61, 113, 0.4); }
.nameList input.enemy:focus { border-color: #ff3d71; box-shadow: 0 0 10px rgba(255, 61, 113, 0.5); }
.nameList input.empty,
.allianceNameRow input.empty { animation: pulseEmpty 1.2s infinite; }
@keyframes pulseEmpty {
0%, 100% { box-shadow: 0 0 6px rgba(255, 200, 0, 0.5); }
50% { box-shadow: 0 0 16px rgba(255, 200, 0, 0.9); }
}
.flagEditPanel {
display: flex; gap: 16px; margin: 10px 0;
align-items: flex-start; justify-content: center;
flex-wrap: wrap;
}
.flagBox {
display: flex; flex-direction: column;
align-items: center; gap: 6px;
padding: 10px;
background: rgba(0, 0, 0, 0.4);
border-radius: 10px;
border: 2px solid rgba(0, 200, 255, 0.3);
pointer-events: all;
}
.flagBox.enemy { border-color: rgba(255, 61, 113, 0.3); }
#flagCanvas1, #flagCanvas2 {
cursor: crosshair; border-radius: 6px;
background: #fff; display: block; image-rendering: pixelated;
}
.flagTools { display: flex; gap: 4px; flex-wrap: wrap; justify-content: center; max-width: 240px; }
.colorSwatch {
width: 22px; height: 22px; border-radius: 50%;
cursor: pointer; border: 2px solid rgba(255, 255, 255, 0.4);
transition: transform 0.12s;
}
.colorSwatch:hover { transform: scale(1.15); }
.colorSwatch.active { border-color: #ffdd00; transform: scale(1.2); }
.flagBtnRow { display: flex; gap: 6px; margin-top: 4px; }
.flagBtnRow button {
padding: 5px 10px; font-size: 11px; margin-top: 0;
background: rgba(60, 70, 90, 0.8); color: #c0c8d0;
}
.flagBtnRow button:hover { background: rgba(80, 90, 110, 1); box-shadow: none; }
.tabRow { display: flex; gap: 8px; margin-bottom: 12px; }
.tabBtn {
padding: 6px 20px; font-size: 13px; margin-top: 0;
background: rgba(40, 50, 70, 0.8); color: #a0b0c0;
border-radius: 8px 8px 0 0;
}
.tabBtn.active { background: linear-gradient(90deg, #00e5ff, #00a0ff); color: #001018; }
.laneInfo {
background: rgba(0, 200, 100, 0.15);
border: 1px solid rgba(0, 255, 160, 0.5);
border-radius: 8px; padding: 10px 18px; margin: 8px 0;
font-size: 13px; color: #00ffa0; max-width: 600px;
}
.warningBanner {
background: rgba(255, 200, 0, 0.15);
border: 1px solid rgba(255, 200, 0, 0.7);
color: #ffcc00; padding: 8px 16px; border-radius: 8px;
font-size: 13px; margin: 8px 0; max-width: 640px;
}
</style>
</head>
<body>
<div id="gameContainer">
<canvas id="game" width="1000" height="600"></canvas>
<div id="ui">
<div class="screen" id="menuScreen">
<h1>矛盾升级:皇室战争</h1>
<h2>三 路 战 场 · 6 v 6</h2>
<div class="tabRow">
<button class="tabBtn active" id="tabNames" onclick="switchTab('names')">📝 编辑名字</button>
<button class="tabBtn" id="tabFlags" onclick="switchTab('flags')">🎨 绘制旗帜</button>
</div>
<div id="namesTab">
<div class="warningBanner" id="nameWarning">
⚠️ 必须填完 2 个阵营名 + 12 个成员名才能开始游戏!
</div>
<div class="alliance" style="border:2px solid #3d9bff;">
<div class="allianceNameRow">
<b style="color:#3d9bff;">🔵 我方阵营名:</b>
<input type="text" id="allyName1" maxlength="10" placeholder="请输入阵营名" class="empty">
</div>
<div class="nameList" id="ally1List"></div>
</div>
<div class="alliance" style="border:2px solid #ff3d71;">
<div class="allianceNameRow">
<b style="color:#ff3d71;">🔴 敌方阵营名:</b>
<input type="text" id="allyName2" maxlength="10" placeholder="请输入阵营名" class="enemy empty">
</div>
<div class="nameList" id="ally2List"></div>
</div>
</div>
<div id="flagsTab" style="display:none;">
<p>点击画布绘制你的阵营旗帜(16×16 像素)</p>
<div class="flagEditPanel">
<div class="flagBox">
<b style="color:#3d9bff; font-size:13px;">🔵 我方阵营旗帜</b>
<canvas id="flagCanvas1" width="192" height="192"></canvas>
<div class="flagTools" id="tools1"></div>
<div class="flagBtnRow">
<button onclick="clearFlag(1)">清空</button>
<button onclick="resetFlag(1)">默认</button>
</div>
</div>
<div class="flagBox enemy">
<b style="color:#ff3d71; font-size:13px;">🔴 敌方阵营旗帜</b>
<canvas id="flagCanvas2" width="192" height="192"></canvas>
<div class="flagTools" id="tools2"></div>
<div class="flagBtnRow">
<button onclick="clearFlag(2)">清空</button>
<button onclick="resetFlag(2)">默认</button>
</div>
</div>
</div>
</div>
<div class="laneInfo">
⚔️ <b>三路进攻:</b>敌方会从左桥、右桥、中路同时进攻!<br>
中路敌人可直穿河流,需重点防守。
</div>
<button id="startBtn" onclick="startGame()" disabled>开始对战</button>
<div class="controls-info">
<b>操作:</b>点击卡牌选择 → 点击战场部署(仅限己方半场)<br>
<b>资源:</b>自动恢复「能量」(10 上限),卡牌消耗能量<br>
<b>目标:</b>摧毁敌方国王塔,或时间结束时比分占优<br>
<b>时间:</b>3 分钟常规 + 1 分钟加时
</div>
</div>
<div class="screen hidden" id="gameOverScreen">
<h1 id="gameOverTitle">对战结束</h1>
<p id="gameOverMsg"></p>
<p id="gameOverStat"></p>
<button onclick="backToMenu()">返回主菜单</button>
</div>
</div>
</div>
<script>
// ===================== 基础配置 =====================
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const FIELD_TOP = 80;
const FIELD_BOTTOM = 500;
const MID_Y = FIELD_TOP + (FIELD_BOTTOM - FIELD_TOP) / 2;
const RIVER_Y = MID_Y - 20;
const RIVER_H = 40;
const LANES = {
LEFT: { id:'LEFT', x: W * 0.20, name:'左路', bridgeX: W * 0.20 },
MID: { id:'MID', x: W * 0.50, name:'中路', bridgeX: null, midLane: true },
RIGHT: { id:'RIGHT', x: W * 0.80, name:'右路', bridgeX: W * 0.80 },
};
const BRIDGE_X1 = LANES.LEFT.bridgeX;
const BRIDGE_X2 = LANES.RIGHT.bridgeX;
const BRIDGE_W = 80;
const MID_CROSS_W = 120;
const TOWER_W = 60, TOWER_H = 70;
const KING_W = 80, KING_H = 90;
const NAMES = { ally1: ['','','','','',''], ally2: ['','','','','',''] };
const ALLIANCE_NAMES = { ally1: '', ally2: '' };
const ALLY1_COLORS = ['#3d9bff', '#00e5ff', '#00ffa0', '#66d9ff', '#a0e8ff', '#7fff7f'];
const ALLY2_COLORS = ['#ff3d71', '#ffaa00', '#ff6b9d', '#cc3300', '#a02050', '#ff8800'];
// ===================== 旗帜系统 =====================
const FLAG_SIZE = 16;
let flagData1 = null, flagData2 = null;
const PALETTE = [
'#000000','#ffffff','#ff0000','#00ff00','#0000ff','#ffff00',
'#ff00ff','#00ffff','#ff8800','#884400','#888888','#3d9bff',
'#ff3d71','#00ffa0','#ffaa00','#a02050',
];
let currentColor1 = '#ff0000', currentColor2 = '#ff3d71';
let currentTool1 = 'pen', currentTool2 = 'pen';
function createEmptyFlag() {
const f = [];
for (let y = 0; y < FLAG_SIZE; y++) f.push(new Array(FLAG_SIZE).fill(null));
return f;
}
function createDefaultFlag1() {
const f = createEmptyFlag();
for (let y = 0; y < FLAG_SIZE; y++) for (let x = 0; x < FLAG_SIZE; x++) f[y][x] = '#3d9bff';
const cx = 8, cy = 8;
for (let y = 0; y < FLAG_SIZE; y++) for (let x = 0; x < FLAG_SIZE; x++) {
const d = Math.hypot(x - cx + 0.5, y - cy + 0.5);
if (d < 3) f[y][x] = '#ffdd00';
else if (d < 4) f[y][x] = '#ffaa00';
}
return f;
}
function createDefaultFlag2() {
const f = createEmptyFlag();
for (let y = 0; y < FLAG_SIZE; y++) for (let x = 0; x < FLAG_SIZE; x++) f[y][x] = '#8a1a2f';
for (let i = 2; i < 14; i++) { f[i][i] = '#ff3d71'; f[i][15-i] = '#ff3d71'; }
for (let i = 4; i < 12; i++) { f[8][i] = '#ffaa00'; f[i][8] = '#ffaa00'; }
return f;
}
function renderFlag(flagCanvas, flagData) {
const fctx = flagCanvas.getContext('2d');
const cellSize = flagCanvas.width / FLAG_SIZE;
fctx.imageSmoothingEnabled = false;
fctx.fillStyle = '#f0f0f0';
fctx.fillRect(0, 0, flagCanvas.width, flagCanvas.height);
for (let y = 0; y < FLAG_SIZE; y++) for (let x = 0; x < FLAG_SIZE; x++) {
if ((x + y) % 2 === 0) {
fctx.fillStyle = '#e0e0e0';
fctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize);
}
}
for (let y = 0; y < FLAG_SIZE; y++) for (let x = 0; x < FLAG_SIZE; x++) {
const c = flagData[y][x];
if (c) { fctx.fillStyle = c; fctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); }
}
fctx.strokeStyle = 'rgba(0, 0, 0, 0.1)';
fctx.lineWidth = 1;
for (let i = 0; i <= FLAG_SIZE; i++) {
fctx.beginPath(); fctx.moveTo(i * cellSize, 0); fctx.lineTo(i * cellSize, flagCanvas.height); fctx.stroke();
fctx.beginPath(); fctx.moveTo(0, i * cellSize); fctx.lineTo(flagCanvas.width, i * cellSize); fctx.stroke();
}
}
function initFlagEditor() {
flagData1 = createDefaultFlag1();
flagData2 = createDefaultFlag2();
renderFlag(document.getElementById('flagCanvas1'), flagData1);
renderFlag(document.getElementById('flagCanvas2'), flagData2);
buildPalette('tools1', 1);
buildPalette('tools2', 2);
bindFlagEvents('flagCanvas1', 1);
bindFlagEvents('flagCanvas2', 2);
}
function buildPalette(containerId, which) {
const c = document.getElementById(containerId);
c.innerHTML = '';
PALETTE.forEach(color => {
const sw = document.createElement('div');
sw.className = 'colorSwatch';
sw.style.background = color;
sw.addEventListener('click', () => {
if (which === 1) currentColor1 = color; else currentColor2 = color;
c.querySelectorAll('.colorSwatch').forEach(s => s.classList.remove('active'));
sw.classList.add('active');
if (which === 1) currentTool1 = 'pen'; else currentTool2 = 'pen';
});
c.appendChild(sw);
});
const tools = ['pen', 'eraser', 'fill'];
const toolNames = { pen:'✏️画', eraser:'🧽擦', fill:'🪣填' };
tools.forEach(t => {
const btn = document.createElement('button');
btn.textContent = toolNames[t];
btn.style.cssText = 'padding:3px 8px; font-size:11px; margin-top:0; background:rgba(60,70,90,0.8); color:#fff;';
btn.addEventListener('click', () => {
if (which === 1) currentTool1 = t; else currentTool2 = t;
c.querySelectorAll('button').forEach(b => b.style.background = 'rgba(60,70,90,0.8)');
btn.style.background = 'linear-gradient(90deg,#00e5ff,#00ffa0)';
btn.style.color = '#001018';
});
c.appendChild(btn);
});
}
function bindFlagEvents(canvasId, which) {
const c = document.getElementById(canvasId);
let isDrawing = false;
function getPixel(e) {
const rect = c.getBoundingClientRect();
const x = Math.floor((e.clientX - rect.left) / rect.width * FLAG_SIZE);
const y = Math.floor((e.clientY - rect.top) / rect.height * FLAG_SIZE);
if (x < 0 || x >= FLAG_SIZE || y < 0 || y >= FLAG_SIZE) return null;
return { x, y };
}
function paintAt(p) {
if (!p) return;
const flagData = which === 1 ? flagData1 : flagData2;
const tool = which === 1 ? currentTool1 : currentTool2;
const color = which === 1 ? currentColor1 : currentColor2;
if (tool === 'pen') flagData[p.y][p.x] = color;
else if (tool === 'eraser') flagData[p.y][p.x] = null;
else if (tool === 'fill') floodFill(flagData, p.x, p.y, flagData[p.y][p.x], color);
renderFlag(c, flagData);
}
c.addEventListener('mousedown', e => { isDrawing = true; paintAt(getPixel(e)); });
c.addEventListener('mousemove', e => {
if (!isDrawing) return;
if (which === 1 && currentTool1 === 'fill') return;
if (which === 2 && currentTool2 === 'fill') return;
paintAt(getPixel(e));
});
c.addEventListener('mouseup', () => isDrawing = false);
c.addEventListener('mouseleave', () => isDrawing = false);
}
function floodFill(flag, x, y, targetColor, newColor) {
if (targetColor === newColor) return;
const stack = [[x, y]];
const visited = new Set();
while (stack.length > 0) {
const [cx, cy] = stack.pop();
if (cx < 0 || cx >= FLAG_SIZE || cy < 0 || cy >= FLAG_SIZE) continue;
if (visited.has(cy * FLAG_SIZE + cx)) continue;
visited.add(cy * FLAG_SIZE + cx);
if (flag[cy][cx] !== targetColor) continue;
flag[cy][cx] = newColor;
stack.push([cx+1, cy], [cx-1, cy], [cx, cy+1], [cx, cy-1]);
}
}
function clearFlag(which) {
const flagData = which === 1 ? flagData1 : flagData2;
for (let y = 0; y < FLAG_SIZE; y++) for (let x = 0; x < FLAG_SIZE; x++) flagData[y][x] = null;
renderFlag(document.getElementById(which === 1 ? 'flagCanvas1' : 'flagCanvas2'), flagData);
}
function resetFlag(which) {
if (which === 1) flagData1 = createDefaultFlag1();
else flagData2 = createDefaultFlag2();
renderFlag(document.getElementById(which === 1 ? 'flagCanvas1' : 'flagCanvas2'),
which === 1 ? flagData1 : flagData2);
}
function drawFlagOnGame(x, y, w, h, flagData, borderColor) {
if (!flagData) return;
const cellW = w / FLAG_SIZE;
const cellH = h / FLAG_SIZE;
ctx.fillStyle = borderColor;
ctx.fillRect(x - 3, y - 3, w + 6, h + 6);
ctx.fillStyle = '#f0f0f0';
ctx.fillRect(x, y, w, h);
for (let py = 0; py < FLAG_SIZE; py++) for (let px = 0; px < FLAG_SIZE; px++) {
const c = flagData[py][px];
if (c) { ctx.fillStyle = c; ctx.fillRect(x + px * cellW, y + py * cellH, cellW, cellH); }
}
}
function switchTab(tab) {
document.getElementById('namesTab').style.display = tab === 'names' ? 'block' : 'none';
document.getElementById('flagsTab').style.display = tab === 'flags' ? 'block' : 'none';
document.getElementById('tabNames').classList.toggle('active', tab === 'names');
document.getElementById('tabFlags').classList.toggle('active', tab === 'flags');
}
// ===================== 名字编辑 =====================
function buildNameInputs() {
const list1 = document.getElementById('ally1List');
const list2 = document.getElementById('ally2List');
list1.innerHTML = ''; list2.innerHTML = '';
for (let i = 0; i < 6; i++) {
const inp1 = document.createElement('input');
inp1.type = 'text'; inp1.value = NAMES.ally1[i]; inp1.maxLength = 8;
inp1.placeholder = '待填写';
inp1.className = NAMES.ally1[i] ? '' : 'empty';
inp1.addEventListener('input', () => {
NAMES.ally1[i] = inp1.value.trim();
inp1.className = NAMES.ally1[i] ? '' : 'empty';
checkAllNames();
});
list1.appendChild(inp1);
const inp2 = document.createElement('input');
inp2.type = 'text';
inp2.className = NAMES.ally2[i] ? 'enemy' : 'enemy empty';
inp2.value = NAMES.ally2[i]; inp2.maxLength = 8;
inp2.placeholder = '待填写';
inp2.addEventListener('input', () => {
NAMES.ally2[i] = inp2.value.trim();
inp2.className = NAMES.ally2[i] ? 'enemy' : 'enemy empty';
checkAllNames();
});
list2.appendChild(inp2);
}
const an1 = document.getElementById('allyName1');
an1.value = ALLIANCE_NAMES.ally1;
an1.className = ALLIANCE_NAMES.ally1 ? '' : 'empty';
an1.addEventListener('input', () => {
ALLIANCE_NAMES.ally1 = an1.value.trim();
an1.className = ALLIANCE_NAMES.ally1 ? '' : 'empty';
checkAllNames();
});
const an2 = document.getElementById('allyName2');
an2.value = ALLIANCE_NAMES.ally2;
an2.className = ALLIANCE_NAMES.ally2 ? 'enemy' : 'enemy empty';
an2.addEventListener('input', () => {
ALLIANCE_NAMES.ally2 = an2.value.trim();
an2.className = ALLIANCE_NAMES.ally2 ? 'enemy' : 'enemy empty';
checkAllNames();
});
checkAllNames();
}
function checkAllNames() {
const allyName1OK = ALLIANCE_NAMES.ally1 && ALLIANCE_NAMES.ally1.trim().length > 0;
const allyName2OK = ALLIANCE_NAMES.ally2 && ALLIANCE_NAMES.ally2.trim().length > 0;
const mem1OK = NAMES.ally1.every(n => n && n.trim().length > 0);
const mem2OK = NAMES.ally2.every(n => n && n.trim().length > 0);
const allFilled = allyName1OK && allyName2OK && mem1OK && mem2OK;
const btn = document.getElementById('startBtn');
const warn = document.getElementById('nameWarning');
if (allFilled) {
btn.disabled = false;
warn.style.background = 'rgba(0, 255, 160, 0.15)';
warn.style.border = '1px solid rgba(0, 255, 160, 0.7)';
warn.style.color = '#00ffa0';
warn.textContent = '✅ 全部填写完成,可以开始对战!';
} else {
btn.disabled = true;
warn.style.background = 'rgba(255, 200, 0, 0.15)';
warn.style.border = '1px solid rgba(255, 200, 0, 0.7)';
warn.style.color = '#ffcc00';
const nameCount = (allyName1OK ? 1 : 0) + (allyName2OK ? 1 : 0);
const memCount = NAMES.ally1.filter(n => n.trim()).length + NAMES.ally2.filter(n => n.trim()).length;
warn.textContent = `⚠️ 必须填完 2 个阵营名 + 12 个成员名(阵营名 ${nameCount}/2,成员名 ${memCount}/12)`;
}
}
function getOwnerName(side, index) {
const list = side === 'player' ? NAMES.ally1 : NAMES.ally2;
return list[index] || (side === 'player' ? '我方' : '敌方');
}
// ===================== 卡牌定义 =====================
const CARDS = {
p_leader: { idx:0, name:'领袖', cost:3, hp:200, atk:40, range:120, speed:0, type:'troop', color:ALLY1_COLORS[0], spawn:'ground', count:1, atkType:'bolt', atkColor:'#00e5ff' },
p_leaderWar: { idx:0, name:'战神领袖', cost:4, hp:400, atk:60, range:30, speed:50, type:'troop', color:'#4a5a70', spawn:'ground', count:1, atkType:'slash', atkColor:'#e0e8f0' },
p_leaderMega: { idx:0, name:'超级领袖', cost:7, hp:1500, atk:200, range:80, speed:25, type:'troop', color:ALLY1_COLORS[0], spawn:'ground', count:1, big:true, atkType:'laser', atkColor:'#00e5ff' },
p_1: { idx:1, name:'神射', cost:3, hp:180, atk:45, range:100, speed:35, type:'troop', color:ALLY1_COLORS[1], spawn:'ground', count:1, atkType:'arrow', atkColor:'#00e5ff' },
p_1Sniper:{ idx:1, name:'狙击手', cost:4, hp:250, atk:90, range:200, speed:30, type:'troop', color:ALLY1_COLORS[1], spawn:'ground', count:1, atkType:'snipe', atkColor:'#a0e8ff' },
p_1Mega: { idx:1, name:'超级狙击', cost:7, hp:1300, atk:220, range:160, speed:22, type:'troop', color:ALLY1_COLORS[1], spawn:'ground', count:1, big:true, atkType:'laser', atkColor:'#a0e8ff' },
p_2: { idx:2, name:'重装', cost:3, hp:220, atk:35, range:110, speed:25, type:'troop', color:ALLY1_COLORS[2], spawn:'ground', count:1, atkType:'bolt', atkColor:'#00ffa0' },
p_2Shield: { idx:2, name:'盾牌', cost:3, hp:800, atk:0, range:0, speed:0, type:'building', color:ALLY1_COLORS[2], spawn:'ground', count:1 },
p_2Drones: { idx:2, name:'无人机', cost:4, hp:80, atk:100, range:40, speed:80, type:'troop', color:ALLY1_COLORS[2], spawn:'air', count:6, atkType:'rapid', atkColor:'#00ffa0' },
p_3: { idx:3, name:'法师', cost:3, hp:190, atk:38, range:130, speed:30, type:'troop', color:ALLY1_COLORS[3], spawn:'ground', count:1, atkType:'magic', atkColor:'#66d9ff' },
p_3Sci: { idx:3, name:'科学官', cost:3, hp:180, atk:30, range:140, speed:35, type:'troop', color:'#ffffff', spawn:'ground', count:1, atkType:'beam', atkColor:'#00e5ff' },
p_4: { idx:4, name:'冰霜', cost:3, hp:210, atk:42, range:125, speed:28, type:'troop', color:ALLY1_COLORS[4], spawn:'ground', count:1, atkType:'frost', atkColor:'#a0e8ff' },
p_4Ice: { idx:4, name:'寒冰', cost:4, hp:230, atk:75, range:180, speed:32, type:'troop', color:ALLY1_COLORS[4], spawn:'ground', count:1, atkType:'frost', atkColor:'#e0f4ff' },
p_4Mega: { idx:4, name:'超极冰', cost:7, hp:1350, atk:200, range:140, speed:24, type:'troop', color:ALLY1_COLORS[4], spawn:'ground', count:1, big:true, atkType:'laser', atkColor:'#e0f4ff' },
p_5: { idx:5, name:'铁卫', cost:3, hp:230, atk:35, range:100, speed:32, type:'troop', color:ALLY1_COLORS[5], spawn:'ground', count:1, atkType:'bolt', atkColor:'#7fff7f' },
p_5Shield: { idx:5, name:'大盾', cost:3, hp:850, atk:0, range:0, speed:0, type:'building', color:ALLY1_COLORS[5], spawn:'ground', count:1 },
p_5Drones: { idx:5, name:'铁卫机群', cost:4, hp:90, atk:110, range:45, speed:85, type:'troop', color:ALLY1_COLORS[5], spawn:'air', count:5, atkType:'rapid', atkColor:'#7fff7f' },
spellLaser: { idx:-1, name:'联盟激光', cost:4, type:'spell', damage:250, radius:80, color:'#00e5ff', owner:'联盟' },
spellBomb: { idx:-1, name:'联盟轰炸', cost:5, type:'spell', damage:350, radius:110, color:'#ff3d71', owner:'联盟' },
};
const ENEMY_CARDS = {
e_leader: { idx:0, name:'领袖', cost:2, hp:250, atk:30, range:30, speed:60, type:'troop', color:ALLY2_COLORS[0], spawn:'ground', count:1, atkType:'slash', atkColor:'#ff8fa0' },
e_leaderBrute:{ idx:0, name:'壮汉', cost:5, hp:900, atk:70, range:35, speed:45, type:'troop', color:ALLY2_COLORS[0], spawn:'ground', count:1, atkType:'slash', atkColor:'#ff6060' },
e_leaderMega: { idx:0, name:'超级', cost:8, hp:2000, atk:150, range:100, speed:30, type:'troop', color:ALLY2_COLORS[0], spawn:'ground', count:1, big:true, atkType:'laser', atkColor:'#ff3d71' },
e_1: { idx:1, name:'疾跑', cost:3, hp:200, atk:50, range:100, speed:55, type:'troop', color:ALLY2_COLORS[1], spawn:'ground', count:1, atkType:'arrow', atkColor:'#ffaa00' },
e_1Run: { idx:1, name:'冲锋', cost:3, hp:400, atk:40, range:30, speed:75, type:'troop', color:ALLY2_COLORS[1], spawn:'ground', count:1, atkType:'slash', atkColor:'#ffd060' },
e_1Mega: { idx:1, name:'超级跑', cost:7, hp:1400, atk:180, range:90, speed:25, type:'troop', color:ALLY2_COLORS[1], spawn:'ground', count:1, big:true, atkType:'laser', atkColor:'#ffaa00' },
e_2: { idx:2, name:'飞行', cost:3, hp:170, atk:60, range:150, speed:30, type:'troop', color:ALLY2_COLORS[2], spawn:'ground', count:1, atkType:'bolt', atkColor:'#ff6b9d' },
e_2Fly: { idx:2, name:'飞鹰', cost:4, hp:300, atk:50, range:50, speed:90, type:'troop', color:ALLY2_COLORS[2], spawn:'air', count:2, atkType:'rapid', atkColor:'#ff9fb0' },
e_3: { idx:3, name:'群攻', cost:3, hp:230, atk:38, range:110, speed:30, type:'troop', color:ALLY2_COLORS[3], spawn:'ground', count:1, atkType:'magic', atkColor:'#cc3300' },
e_3Swarm: { idx:3, name:'蜂群', cost:4, hp:150, atk:20, range:30, speed:80, type:'troop', color:ALLY2_COLORS[3], spawn:'ground', count:3, atkType:'rapid', atkColor:'#ff5522' },
e_4: { idx:4, name:'重甲', cost:3, hp:220, atk:45, range:105, speed:38, type:'troop', color:ALLY2_COLORS[4], spawn:'ground', count:1, atkType:'bolt', atkColor:'#a02050' },
e_4Brute: { idx:4, name:'铁壁', cost:5, hp:950, atk:75, range:40, speed:42, type:'troop', color:ALLY2_COLORS[4], spawn:'ground', count:1, atkType:'slash', atkColor:'#d04070' },
e_4Mega: { idx:4, name:'超级甲', cost:7, hp:1500, atk:190, range:95, speed:26, type:'troop', color:ALLY2_COLORS[4], spawn:'ground', count:1, big:true, atkType:'laser', atkColor:'#a02050' },
e_5: { idx:5, name:'空袭', cost:3, hp:200, atk:55, range:130, speed:35, type:'troop', color:ALLY2_COLORS[5], spawn:'ground', count:1, atkType:'arrow', atkColor:'#ff8800' },
e_5Fly: { idx:5, name:'轰炸机', cost:4, hp:320, atk:55, range:55, speed:88, type:'troop', color:ALLY2_COLORS[5], spawn:'air', count:2, atkType:'rapid', atkColor:'#ffaa66' },
e_5Mega: { idx:5, name:'超级炸', cost:7, hp:1450, atk:195, range:110, speed:24, type:'troop', color:ALLY2_COLORS[5], spawn:'ground', count:1, big:true, atkType:'laser', atkColor:'#ff8800' },
};
let cardDeck = Object.keys(CARDS);
let enemyDeck = Object.keys(ENEMY_CARDS);
// ===================== 游戏状态 =====================
let game = null;
function createGame() {
return {
state: 'playing',
time: 180, overtime: false, overtimeTime: 60,
elixir: 5, enemyElixir: 5, maxElixir: 10,
elixirTimer: 0, enemyElixirTimer: 0,
elixirRate: 1/2.8,
playerHand: [], playerNext: null,
enemyHand: [], enemyNext: null,
units: [],
// ===== 攻击特效容器 =====
projectiles: [], // 弹道
beams: [], // 光束
slashes: [], // 斩击
hitSparks: [], // 命中火花
playerKing: { x: W/2, y: FIELD_BOTTOM - 40, hp: 3000, maxHp: 3000, w: KING_W, h: KING_H, side:'player', type:'king', target:null, attackTimer:0, attackFlash:0 },
playerTowerL:{ x: LANES.LEFT.x + 60, y: FIELD_BOTTOM - 50, hp: 1400, maxHp: 1400, w: TOWER_W, h: TOWER_H, side:'player', type:'princess', lane:'LEFT', target:null, attackTimer:0, attackFlash:0 },
playerTowerR:{ x: LANES.RIGHT.x - 60, y: FIELD_BOTTOM - 50, hp: 1400, maxHp: 1400, w: TOWER_W, h: TOWER_H, side:'player', type:'princess', lane:'RIGHT', target:null, attackTimer:0, attackFlash:0 },
enemyKing: { x: W/2, y: FIELD_TOP + 40, hp: 3000, maxHp: 3000, w: KING_W, h: KING_H, side:'enemy', type:'king', target:null, attackTimer:0, attackFlash:0 },
enemyTowerL: { x: LANES.LEFT.x + 60, y: FIELD_TOP + 50, hp: 1400, maxHp: 1400, w: TOWER_W, h: TOWER_H, side:'enemy', type:'princess', lane:'LEFT', target:null, attackTimer:0, attackFlash:0 },
enemyTowerR: { x: LANES.RIGHT.x - 60, y: FIELD_TOP + 50, hp: 1400, maxHp: 1400, w: TOWER_W, h: TOWER_H, side:'enemy', type:'princess', lane:'RIGHT', target:null, attackTimer:0, attackFlash:0 },
effects: [], floatingTexts: [],
selectedCard: null,
mouseX: 0, mouseY: 0,
enemyAITimer: 0,
lastTime: 0, totalTime: 0,
playerCrowns: 0, enemyCrowns: 0, result: null,
laneWarnings: { LEFT: 0, MID: 0, RIGHT: 0 },
execution: null,
screenShake: 0,
};
}
// ===================== 全局入口 =====================
window.startGame = function() {
const allyName1OK = ALLIANCE_NAMES.ally1 && ALLIANCE_NAMES.ally1.trim().length > 0;
const allyName2OK = ALLIANCE_NAMES.ally2 && ALLIANCE_NAMES.ally2.trim().length > 0;
const mem1OK = NAMES.ally1.every(n => n && n.trim().length > 0);
const mem2OK = NAMES.ally2.every(n => n && n.trim().length > 0);
if (!(allyName1OK && allyName2OK && mem1OK && mem2OK)) { checkAllNames(); return; }
try { initAudio(); } catch(e){}
document.getElementById('menuScreen').classList.add('hidden');
game = createGame();
initHands();
requestAnimationFrame(gameLoop);
};
window.backToMenu = function() {
document.getElementById('gameOverScreen').classList.add('hidden');
document.getElementById('menuScreen').classList.remove('hidden');
game = null;
renderMenuBackground();
};
window.switchTab = switchTab;
window.clearFlag = clearFlag;
window.resetFlag = resetFlag;
// ===================== 音效 =====================
let audioCtx = null;
function initAudio() {
if (!audioCtx) { try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e){} }
}
function playSound(type) {
if (!audioCtx) return;
const t = audioCtx.currentTime;
const o = audioCtx.createOscillator();
const g = audioCtx.createGain();
o.connect(g); g.connect(audioCtx.destination);
const presets = {
deploy:{w:'sine',f1:400,f2:600,d:0.15,v:0.08},
hit:{w:'triangle',f1:300,f2:200,d:0.08,v:0.05},
explode:{w:'sawtooth',f1:200,f2:40,d:0.4,v:0.15},
tower:{w:'square',f1:150,f2:80,d:0.6,v:0.15},
spell:{w:'sawtooth',f1:800,f2:100,d:0.5,v:0.12},
victory:{w:'sine',f1:400,f2:1200,d:1,v:0.2},
defeat:{w:'sine',f1:400,f2:100,d:1,v:0.2},
warn:{w:'square',f1:600,f2:900,d:0.2,v:0.1},
slash:{w:'sawtooth',f1:2000,f2:100,d:0.3,v:0.3},
blood:{w:'sawtooth',f1:100,f2:20,d:0.8,v:0.2},
bolt:{w:'square',f1:1200,f2:400,d:0.1,v:0.06},
laser:{w:'sawtooth',f1:1500,f2:800,d:0.15,v:0.05},
magic:{w:'sine',f1:600,f2:1200,d:0.2,v:0.05},
frost:{w:'sine',f1:1800,f2:600,d:0.2,v:0.05},
arrow:{w:'triangle',f1:800,f2:300,d:0.1,v:0.05},
rapid:{w:'square',f1:900,f2:600,d:0.05,v:0.03},
snipe:{w:'sawtooth',f1:2200,f2:200,d:0.2,v:0.12},
};
const p = presets[type]; if (!p) return;
o.type = p.w;
o.frequency.setValueAtTime(p.f1, t);
o.frequency.exponentialRampToValueAtTime(p.f2, t + p.d);
g.gain.setValueAtTime(p.v, t);
g.gain.exponentialRampToValueAtTime(0.001, t + p.d);
o.start(t); o.stop(t + p.d);
}
// ===================== 手牌 =====================
function drawCard() {
const remaining = cardDeck.filter(k => !game.playerHand.includes(k) && k !== game.playerNext);
if (remaining.length === 0) return cardDeck[Math.floor(Math.random() * cardDeck.length)];
return remaining[Math.floor(Math.random() * remaining.length)];
}
function drawEnemyCard() { return enemyDeck[Math.floor(Math.random() * enemyDeck.length)]; }
function initHands() {
game.playerHand = [];
for (let i = 0; i < 4; i++) game.playerHand.push(drawCard());
game.playerNext = drawCard();
game.enemyHand = [];
for (let i = 0; i < 4; i++) game.enemyHand.push(drawEnemyCard());
game.enemyNext = drawEnemyCard();
}
function getCardDisplayName(key, side) {
const def = side === 'player' ? CARDS[key] : ENEMY_CARDS[key];
if (!def) return '';
if (def.idx === undefined || def.idx < 0) return def.name;
const ownerName = getOwnerName(side, def.idx);
if (def.name === '领袖') return ownerName;
return ownerName + '·' + def.name;
}
function getCardOwner(key, side) {
const def = side === 'player' ? CARDS[key] : ENEMY_CARDS[key];
if (!def) return '';
if (def.idx === undefined || def.idx < 0) return def.owner || '联盟';
return getOwnerName(side, def.idx);
}
function getLaneOfUnit(u) {
if (u.lane) return u.lane;
if (u.x < W * 0.35) return 'LEFT';
if (u.x > W * 0.65) return 'RIGHT';
return 'MID';
}
function canCrossAt(x, y, flying) {
if (flying) return true;
if (Math.abs(y - MID_Y) > RIVER_H + 20) return true;
const inLeftBridge = Math.abs(x - BRIDGE_X1) < BRIDGE_W / 2;
const inRightBridge = Math.abs(x - BRIDGE_X2) < BRIDGE_W / 2;
const inMid = Math.abs(x - W / 2) < MID_CROSS_W / 2;
return inLeftBridge || inRightBridge || inMid;
}
// ===================== 游戏循环 =====================
function gameLoop(timestamp) {
if (!game) return;
if (!game.lastTime) game.lastTime = timestamp;
const dt = Math.min((timestamp - game.lastTime) / 1000, 0.05);
game.lastTime = timestamp;
if (game.state === 'playing') update(dt);
else if (game.state === 'execution') updateExecution(dt);
render();
requestAnimationFrame(gameLoop);
}
function update(dt) {
game.totalTime += dt;
if (game.screenShake > 0) game.screenShake -= dt;
for (const k in game.laneWarnings) { if (game.laneWarnings[k] > 0) game.laneWarnings[k] -= dt; }
if (!game.overtime) {
game.time -= dt;
if (game.time <= 0) {
game.time = 0;
if (game.playerCrowns === game.enemyCrowns) game.overtime = true;
else { endGame(); return; }
}
} else {
game.overtimeTime -= dt;
if (game.overtimeTime <= 0) { endGame(); return; }
}
game.elixirTimer += dt;
if (game.elixirTimer >= game.elixirRate) { game.elixirTimer = 0; if (game.elixir < game.maxElixir) game.elixir++; }
game.enemyElixirTimer += dt;
if (game.enemyElixirTimer >= game.elixirRate) { game.enemyElixirTimer = 0; if (game.enemyElixir < game.maxElixir) game.enemyElixir++; }
updateUnits(dt);
updateTowers(dt);
updateEnemyAI(dt);
updateProjectiles(dt);
updateBeams(dt);
updateSlashes(dt);
updateHitSparks(dt);
updateEffects(dt);
updateFloatingTexts(dt);
if (game.enemyKing.hp <= 0 || game.playerKing.hp <= 0) endGame();
}
// ===================== 单位逻辑 =====================
function spawnUnit(cardKey, x, y, side, forcedLane) {
const def = side === 'player' ? CARDS[cardKey] : ENEMY_CARDS[cardKey];
if (!def) return null;
const count = def.count || 1;
for (let i = 0; i < count; i++) {
const offsetX = count > 1 ? (i - (count-1)/2) * 25 : 0;
const offsetY = count > 1 ? (i % 2 === 0 ? -15 : 15) : 0;
const ux = x + offsetX, uy = y + offsetY;
let lane = forcedLane;
if (!lane) {
if (ux < W * 0.35) lane = 'LEFT';
else if (ux > W * 0.65) lane = 'RIGHT';
else lane = 'MID';
}
game.units.push({
key: cardKey, def, side, lane,
x: ux, y: uy,
hp: def.hp, maxHp: def.hp,
atk: def.atk, range: def.range, speed: def.speed,
attackTimer: 0, attackInterval: 1.0,
target: null, dead: false, hitFlash: 0,
flying: def.spawn === 'air',
color: def.color, big: def.big || false,
owner: getCardOwner(cardKey, side),
displayName: getCardDisplayName(cardKey, side),
atkType: def.atkType || 'bolt',
atkColor: def.atkColor || def.color,
facing: side === 'player' ? -1 : 1, // 1=朝下,-1=朝上
attackAnimTimer: 0,
// 自动计算朝向:目标在其上方为 1,下方为 -1
});
}
playSound('deploy');
return true;
}
function deployCard(handIndex, x, y) {
const cardKey = game.playerHand[handIndex];
if (!cardKey) return false;
const def = CARDS[cardKey];
if (def.type === 'spell') {
if (game.elixir < def.cost) return false;
if (y < FIELD_TOP || y > FIELD_BOTTOM) return false;
game.elixir -= def.cost;
castSpell(cardKey, x, y, 'player');
game.playerHand[handIndex] = game.playerNext;
game.playerNext = drawCard();
game.selectedCard = null;
return true;
}
if (y < MID_Y + 10) return false;
if (y < FIELD_TOP || y > FIELD_BOTTOM) return false;
if (game.elixir < def.cost) return false;
let lane;
if (x < W * 0.35) lane = 'LEFT';
else if (x > W * 0.65) lane = 'RIGHT';
else lane = 'MID';
if (lane === 'MID' && !def.flying && y < MID_Y + RIVER_H) {
if (y > MID_Y - RIVER_H / 2 && y < MID_Y + RIVER_H / 2) {
if (!canCrossAt(x, y, false)) return false;
}
}
if (!spawnUnit(cardKey, x, y, 'player', lane)) return false;
game.elixir -= def.cost;
game.playerHand[handIndex] = game.playerNext;
game.playerNext = drawCard();
game.selectedCard = null;
return true;
}
function castSpell(cardKey, x, y, side) {
const def = side === 'player' ? CARDS[cardKey] : ENEMY_CARDS[cardKey];
playSound('spell');
addEffect(x, y, 'spell', def.color, 0.6);
for (const u of game.units) {
if (u.dead) continue;
if (side === 'player' && u.side !== 'enemy') continue;
if (side === 'enemy' && u.side !== 'player') continue;
if (Math.hypot(u.x - x, u.y - y) <= def.radius) damageUnit(u, def.damage);
}
const towers = side === 'player'
? [game.enemyKing, game.enemyTowerL, game.enemyTowerR]
: [game.playerKing, game.playerTowerL, game.playerTowerR];
for (const t of towers) {
if (t.hp <= 0) continue;
if (Math.hypot(t.x - x, t.y - y) <= def.radius) damageTower(t, def.damage);
}
}
function damageUnit(u, dmg) {
if (u.dead) return;
u.hp -= dmg;
u.hitFlash = 0.15;
spawnHitSparks(u.x, u.y, u.side === 'player' ? '#66d9ff' : '#ff8fa0');
addFloatingText(u.x, u.y - 20, '-' + Math.round(dmg), '#ff6b6b');
if (u.hp <= 0) {
u.dead = true;
addEffect(u.x, u.y, 'boom', u.color, 0.4);
playSound('hit');
}
}
function damageTower(t, dmg) {
if (t.hp <= 0) return;
t.hp -= dmg;
t.attackFlash = 0.15;
spawnHitSparks(t.x, t.y, t.side === 'player' ? '#66d9ff' : '#ff8fa0');
addFloatingText(t.x, t.y - 40, '-' + Math.round(dmg), '#ff6b6b');
if (t.hp <= 0) {
t.hp = 0;
playSound('tower');
addEffect(t.x, t.y, 'explosion', '#ff3d71', 0.8);
game.screenShake = 0.5;
if (t.side === 'player') game.enemyCrowns++;
else game.playerCrowns++;
}
}
// ===================== 攻击特效生成 =====================
function spawnAttackEffect(attacker, target, isTowerTarget) {
const atkType = attacker.atkType || 'bolt';
const color = attacker.atkColor || attacker.color;
const sx = attacker.x, sy = attacker.y;
const tx = target.x, ty = target.y;
// 攻击动画
attacker.attackAnimTimer = 0.25;
// 根据类型生成不同特效
switch (atkType) {
case 'bolt':
game.projectiles.push({
x: sx, y: sy, sx, sy, tx, ty,
target, t: 0, dur: 0.25,
color, size: 5, type: 'bolt', dead: false,
});
playSound('bolt');
break;
case 'arrow':
game.projectiles.push({
x: sx, y: sy, sx, sy, tx, ty,
target, t: 0, dur: 0.3,
color, size: 4, type: 'arrow', dead: false,
angle: Math.atan2(ty - sy, tx - sx),
});
playSound('arrow');
break;
case 'snipe':
game.projectiles.push({
x: sx, y: sy, sx, sy, tx, ty,
target, t: 0, dur: 0.15,
color, size: 6, type: 'snipe', dead: false,
});
playSound('snipe');
break;
case 'laser':
game.beams.push({
x1: sx, y1: sy, x2: tx, y2: ty,
color, width: 8, life: 0.25, maxLife: 0.25, type: 'laser', dead: false,
});
playSound('laser');
game.screenShake = Math.max(game.screenShake, 0.08);
break;
case 'beam':
game.beams.push({
x1: sx, y1: sy, x2: tx, y2: ty,
color, width: 5, life: 0.2, maxLife: 0.2, type: 'beam', dead: false,
});
playSound('laser');
break;
case 'slash':
game.slashes.push({
x: tx, y: ty, angle: Math.atan2(ty - sy, tx - sx),
color, life: 0.25, maxLife: 0.25, size: 40, dead: false,
});
playSound('slash');
break;
case 'magic':
game.projectiles.push({
x: sx, y: sy, sx, sy, tx, ty,
target, t: 0, dur: 0.35,
color, size: 7, type: 'magic', dead: false,
spin: 0,
});
playSound('magic');
break;
case 'frost':
game.projectiles.push({
x: sx, y: sy, sx, sy, tx, ty,
target, t: 0, dur: 0.3,
color, size: 6, type: 'frost', dead: false,
});
playSound('frost');
break;
case 'rapid':
game.projectiles.push({
x: sx, y: sy, sx, sy, tx, ty,
target, t: 0, dur: 0.12,
color, size: 3, type: 'rapid', dead: false,
});
playSound('rapid');
break;
}
}
function updateProjectiles(dt) {
for (const p of game.projectiles) {
if (p.dead) continue;
p.t += dt;
if (p.t >= p.dur) { p.dead = true; continue; }
const t = p.t / p.dur;
p.x = p.sx + (p.tx - p.sx) * t;
p.y = p.sy + (p.ty - p.sy) * t;
if (p.type === 'magic') p.spin = (p.spin || 0) + dt * 15;
}
game.projectiles = game.projectiles.filter(p => !p.dead);
}
function updateBeams(dt) {
for (const b of game.beams) {
if (b.dead) continue;
b.life -= dt;
if (b.life <= 0) b.dead = true;
}
game.beams = game.beams.filter(b => !b.dead);
}
function updateSlashes(dt) {
for (const s of game.slashes) {
if (s.dead) continue;
s.life -= dt;
if (s.life <= 0) s.dead = true;
}
game.slashes = game.slashes.filter(s => !s.dead);
}
function spawnHitSparks(x, y, color) {
const count = 6;
for (let i = 0; i < count; i++) {
const ang = Math.random() * Math.PI * 2;
const spd = 60 + Math.random() * 120;
game.hitSparks.push({
x, y,
vx: Math.cos(ang) * spd,
vy: Math.sin(ang) * spd,
size: 2 + Math.random() * 3,
color, life: 0.4, maxLife: 0.4, dead: false,
});
}
}
function updateHitSparks(dt) {
for (const s of game.hitSparks) {
if (s.dead) continue;
s.x += s.vx * dt;
s.y += s.vy * dt;
s.vx *= 0.9;
s.vy *= 0.9;
s.life -= dt;
if (s.life <= 0) s.dead = true;
}
game.hitSparks = game.hitSparks.filter(s => !s.dead);
}
// ===================== 单位攻击循环 =====================
function updateUnits(dt) {
for (const u of game.units) {
if (u.dead) continue;
if (u.hitFlash > 0) u.hitFlash -= dt;
if (u.attackTimer > 0) u.attackTimer -= dt;
if (u.attackAnimTimer > 0) u.attackAnimTimer -= dt;
let target = null, minDist = Infinity;
const towers = u.side === 'player'
? [game.enemyTowerL, game.enemyTowerR, game.enemyKing]
: [game.playerTowerL, game.playerTowerR, game.playerKing];
for (const t of towers) {
if (t.hp <= 0) continue;
if (t.type === 'king' && u.side === 'player' && game.enemyTowerL.hp > 0 && game.enemyTowerR.hp > 0) continue;
if (t.type === 'king' && u.side === 'enemy' && game.playerTowerL.hp > 0 && game.playerTowerR.hp > 0) continue;
const d = Math.hypot(t.x - u.x, t.y - u.y);
if (d < minDist) { minDist = d; target = { type: 'tower', obj: t }; }
}
for (const other of game.units) {
if (other === u || other.dead || other.side === u.side) continue;
const d = Math.hypot(other.x - u.x, other.y - u.y);
if (d < minDist) { minDist = d; target = { type: 'unit', obj: other }; }
}
u.target = target;
if (!target) continue;
// 朝向
u.facing = target.obj.y > u.y ? 1 : -1;
const tx = target.obj.x, ty = target.obj.y;
const dist = Math.hypot(tx - u.x, ty - u.y);
const attackRange = u.range + (target.type === 'tower' ? 30 : 15);
if (dist <= attackRange && u.atk > 0) {
if (u.attackTimer <= 0) {
u.attackTimer = u.attackInterval;
// 生成攻击特效
spawnAttackEffect(u, target.obj, target.type === 'tower');
// 延迟造成伤害(让特效先飞过去)
const delay = u.atkType === 'slash' ? 0.15 : 0.2;
const dmgTarget = target.obj;
const dmgAmount = u.atk;
setTimeout(() => {
if (!game || game.state !== 'playing') return;
if (target.type === 'tower') damageTower(dmgTarget, dmgAmount);
else damageUnit(dmgTarget, dmgAmount);
}, delay * 1000);
}
} else if (u.speed > 0) {
const dx = tx - u.x, dy = ty - u.y;
const len = Math.hypot(dx, dy) || 1;
let nx = dx / len, ny = dy / len;
if (!u.flying && !canCrossAt(u.x, u.y, false) && (u.y < MID_Y + RIVER_H / 2 && u.y > MID_Y - RIVER_H / 2)) {
const leftDist = Math.abs(u.x - BRIDGE_X1);
const rightDist = Math.abs(u.x - BRIDGE_X2);
const midDist = Math.abs(u.x - W/2);
const minD = Math.min(leftDist, rightDist, midDist);
let targetX;
if (minD === leftDist) targetX = BRIDGE_X1;
else if (minD === rightDist) targetX = BRIDGE_X2;
else targetX = W/2;
const bdx = targetX - u.x;
if (Math.abs(bdx) > 5) { nx = bdx > 0 ? 1 : -1; ny = 0; }
else { ny = u.side === 'player' ? -1 : 1; nx = 0; }
}
u.x += nx * u.speed * dt;
u.y += ny * u.speed * dt;
u.x = Math.max(30, Math.min(W - 30, u.x));
u.y = Math.max(FIELD_TOP + 20, Math.min(FIELD_BOTTOM - 20, u.y));
if (u.x < W * 0.35) u.lane = 'LEFT';
else if (u.x > W * 0.65) u.lane = 'RIGHT';
else u.lane = 'MID';
}
}
game.units = game.units.filter(u => !u.dead);
}
function updateTowers(dt) {
const allTowers = [game.playerKing, game.playerTowerL, game.playerTowerR, game.enemyKing, game.enemyTowerL, game.enemyTowerR];
for (const t of allTowers) {
if (t.hp <= 0) continue;
if (t.attackFlash > 0) t.attackFlash -= dt;
t.attackTimer = (t.attackTimer || 0) - dt;
if (t.attackTimer > 0) continue;
const range = t.type === 'king' ? 180 : 150;
const dmg = t.type === 'king' ? 90 : 60;
for (const u of game.units) {
if (u.dead) continue;
if (t.side === 'player' && u.side !== 'enemy') continue;
if (t.side === 'enemy' && u.side !== 'player') continue;
if (Math.hypot(u.x - t.x, u.y - t.y) <= range) {
// 塔的攻击特效
const towerColor = t.side === 'player' ? '#3d9bff' : '#ff3d71';
game.beams.push({
x1: t.x, y1: t.y, x2: u.x, y2: u.y,
color: towerColor, width: 4, life: 0.2, maxLife: 0.2, type: 'tower', dead: false,
});
spawnHitSparks(u.x, u.y, towerColor);
damageUnit(u, dmg);
t.attackTimer = 1.0;
t.attackFlash = 0.2;
addEffect(u.x, u.y, 'hit', '#fff', 0.2);
break;
}
}
}
}
// ===================== 敌方 AI =====================
function updateEnemyAI(dt) {
game.enemyAITimer += dt;
if (game.enemyAITimer < 1.5) return;
game.enemyAITimer = 0;
let bestKey = null, bestCost = Infinity;
for (const k of game.enemyHand) {
const def = ENEMY_CARDS[k];
if (def && def.cost <= game.enemyElixir && def.cost < bestCost) { bestCost = def.cost; bestKey = k; }
}
if (game.enemyElixir >= 8) {
for (const k of game.enemyHand) {
const def = ENEMY_CARDS[k];
if (def && def.cost === 8 && def.cost <= game.enemyElixir) { bestKey = k; bestCost = 8; break; }
}
}
if (!bestKey) return;
const def = ENEMY_CARDS[bestKey];
if (!def) return;
const laneStrength = { LEFT: 0, MID: 0, RIGHT: 0 };
for (const u of game.units) {
if (u.side !== 'player' || u.dead) continue;
laneStrength[getLaneOfUnit(u)] += u.hp + u.atk * 10;
}
const towerHp = { LEFT: game.playerTowerL.hp, MID: game.playerKing.hp, RIGHT: game.playerTowerR.hp };
let chosenLane;
if (def.spawn === 'air') {
chosenLane = ['LEFT', 'MID', 'RIGHT'][Math.floor(Math.random() * 3)];
} else {
const weakness = {
LEFT: (1 - towerHp.LEFT / 1400) * 100 + (100 - Math.min(laneStrength.LEFT, 100)) + Math.random() * 30,
MID: (1 - towerHp.MID / 3000) * 50 + (100 - Math.min(laneStrength.MID, 100)) + Math.random() * 30,
RIGHT:(1 - towerHp.RIGHT / 1400) * 100 + (100 - Math.min(laneStrength.RIGHT, 100)) + Math.random() * 30,
};
if (Math.random() < 0.7) {
chosenLane = Object.keys(weakness).reduce((a, b) => weakness[a] > weakness[b] ? a : b);
} else {
chosenLane = ['LEFT', 'MID', 'RIGHT'][Math.floor(Math.random() * 3)];
}
}
game.laneWarnings[chosenLane] = 1.5;
const laneX = LANES[chosenLane].x;
const x = laneX + (Math.random() - 0.5) * 40;
const y = MID_Y - 40 - Math.random() * 30;
if (def.type === 'spell') {
let target = null;
for (const u of game.units) {
if (u.side !== 'player' || u.dead) continue;
if (!target || u.y < target.y) target = u;
}
if (target) {
castSpell(bestKey, target.x, target.y, 'enemy');
game.enemyElixir -= def.cost;
const idx = game.enemyHand.indexOf(bestKey);
if (idx >= 0) { game.enemyHand[idx] = game.enemyNext; game.enemyNext = drawEnemyCard(); }
}
} else {
spawnUnit(bestKey, x, y, 'enemy', chosenLane);
game.enemyElixir -= def.cost;
const idx = game.enemyHand.indexOf(bestKey);
if (idx >= 0) { game.enemyHand[idx] = game.enemyNext; game.enemyNext = drawEnemyCard(); }
}
}
// ===================== 特效管理 =====================
function addFloatingText(x, y, text, color) {
game.floatingTexts.push({ x, y, text, color, life: 1, vy: -50, dead: false });
}
function updateFloatingTexts(dt) {
for (const f of game.floatingTexts) {
f.y += f.vy * dt; f.life -= dt * 1.5;
if (f.life <= 0) f.dead = true;
}
game.floatingTexts = game.floatingTexts.filter(f => !f.dead);
}
function addEffect(x, y, type, color, life) {
game.effects.push({ x, y, type, color, life, maxLife: life, radius: 0 });
}
function updateEffects(dt) {
for (const e of game.effects) {
e.life -= dt;
if (e.type === 'spell') e.radius = (1 - e.life / e.maxLife) * 100;
if (e.type === 'boom') e.radius = (1 - e.life / e.maxLife) * 40;
if (e.type === 'explosion') e.radius = (1 - e.life / e.maxLife) * 80;
if (e.life <= 0) e.dead = true;
}
game.effects = game.effects.filter(e => !e.dead);
}
// ===================== 处决动画 =====================
function startExecution(victimSide, winnerSide) {
const victimKing = victimSide === 'player' ? game.playerKing : game.enemyKing;
game.state = 'execution';
game.execution = {
timer: 0, victimSide, winnerSide,
victimX: victimKing.x, victimY: victimKing.y,
executioner: {
x: W/2, y: winnerSide === 'player' ? FIELD_BOTTOM - 40 : FIELD_TOP + 40,
startX: W/2, startY: winnerSide === 'player' ? FIELD_BOTTOM - 40 : FIELD_TOP + 40,
endX: victimKing.x, endY: victimKing.y - 40,
},
bloodParticles: [],
phase: 0, phaseTimer: 0,
headFalling: null,
showTitle: false, titleText: '', titleSub: '',
shakeTime: 0,
};
playSound('slash');
}
function updateExecution(dt) {
const ex = game.execution;
if (!ex) return;
ex.timer += dt; ex.phaseTimer += dt;
if (ex.phase === 0) {
if (ex.phaseTimer >= 1.5) { ex.phase = 1; ex.phaseTimer = 0; playSound('warn'); }
} else if (ex.phase === 1) {
const t = Math.min(1, ex.phaseTimer / 1.2);
const ease = 1 - Math.pow(1 - t, 3);
ex.executioner.x = ex.executioner.startX + (ex.executioner.endX - ex.executioner.startX) * ease;
ex.executioner.y = ex.executioner.startY + (ex.executioner.endY - ex.executioner.startY) * ease;
if (t >= 1) { ex.phase = 2; ex.phaseTimer = 0; playSound('slash'); }
} else if (ex.phase === 2) {
if (ex.phaseTimer >= 0.5) {
ex.phase = 3; ex.phaseTimer = 0;
playSound('blood');
for (let i = 0; i < 40; i++) {
const ang = Math.random() * Math.PI * 2;
const spd = 100 + Math.random() * 250;
ex.bloodParticles.push({
x: ex.victimX, y: ex.victimY - 20,
vx: Math.cos(ang) * spd, vy: Math.sin(ang) * spd - 80,
size: 3 + Math.random() * 6,
life: 1.5, maxLife: 1.5,
color: Math.random() < 0.7 ? '#8a0020' : '#cc0033',
});
}
ex.headFalling = {
x: ex.victimX, y: ex.victimY - 20,
vx: (Math.random() - 0.5) * 100, vy: -150, rot: 0,
vrot: (Math.random() - 0.5) * 10,
};
ex.shakeTime = 0.8;
}
} else if (ex.phase === 3) {
for (const p of ex.bloodParticles) {
p.x += p.vx * dt; p.y += p.vy * dt;
p.vy += 400 * dt; p.life -= dt;
}
ex.bloodParticles = ex.bloodParticles.filter(p => p.life > 0);
if (ex.headFalling) {
ex.headFalling.x += ex.headFalling.vx * dt;
ex.headFalling.y += ex.headFalling.vy * dt;
ex.headFalling.vy += 600 * dt;
ex.headFalling.rot += ex.headFalling.vrot * dt;
}
if (ex.shakeTime > 0) ex.shakeTime -= dt;
if (ex.phaseTimer >= 2.0) {
ex.phase = 4; ex.phaseTimer = 0; ex.showTitle = true;
const winnerName = ex.winnerSide === 'player' ? ALLIANCE_NAMES.ally1 : ALLIANCE_NAMES.ally2;
const loserName = ex.victimSide === 'player' ? ALLIANCE_NAMES.ally1 : ALLIANCE_NAMES.ally2;
ex.titleText = winnerName + ' 胜利!';
ex.titleSub = loserName + ' 已被处决';
}
} else if (ex.phase === 4) {
for (const p of ex.bloodParticles) {
p.x += p.vx * dt; p.y += p.vy * dt;
p.vy += 400 * dt; p.life -= dt;
}
ex.bloodParticles = ex.bloodParticles.filter(p => p.life > 0);
if (ex.headFalling) {
ex.headFalling.x += ex.headFalling.vx * dt;
ex.headFalling.y += ex.headFalling.vy * dt;
ex.headFalling.vy += 600 * dt;
ex.headFalling.rot += ex.headFalling.vrot * dt;
}
if (ex.phaseTimer >= 2.5) finalizeEndGame(ex.winnerSide);
}
}
function finalizeEndGame(winnerSide) {
game.state = 'ended';
const result = winnerSide === 'player' ? 'victory' : 'defeat';
game.result = result;
const titleEl = document.getElementById('gameOverTitle');
const msg = winnerSide === 'player'
? ALLIANCE_NAMES.ally1 + ' 通过处决赢得了胜利!'
: ALLIANCE_NAMES.ally2 + ' 处决了' + ALLIANCE_NAMES.ally1 + '!';
if (result === 'victory') {
titleEl.textContent = ALLIANCE_NAMES.ally1 + ' 胜利!';
titleEl.style.background = 'linear-gradient(90deg,#00ffa0,#00e5ff)';
} else {
titleEl.textContent = ALLIANCE_NAMES.ally2 + ' 胜利!';
titleEl.style.background = 'linear-gradient(90deg,#ff3d71,#ff6b6b)';
}
titleEl.style.webkitBackgroundClip = 'text';
titleEl.style.webkitTextFillColor = 'transparent';
document.getElementById('gameOverMsg').textContent = msg;
document.getElementById('gameOverStat').textContent =
`皇冠比分:${ALLIANCE_NAMES.ally1} ${game.playerCrowns} - ${game.enemyCrowns} ${ALLIANCE_NAMES.ally2}`;
document.getElementById('gameOverScreen').classList.remove('hidden');
}
// ===================== 渲染 =====================
function render() {
ctx.save();
if (game && game.execution && game.execution.shakeTime > 0) {
const k = game.execution.shakeTime * 15;
ctx.translate((Math.random() - 0.5) * k, (Math.random() - 0.5) * k);
}
if (game && game.screenShake > 0) {
const k = game.screenShake * 20;
ctx.translate((Math.random() - 0.5) * k, (Math.random() - 0.5) * k);
}
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, '#1a2040');
grad.addColorStop(0.5, '#0d1525');
grad.addColorStop(1, '#1a2040');
ctx.fillStyle = grad;
ctx.fillRect(-40, -40, W + 80, H + 80);
if (!game) { ctx.restore(); return; }
drawField();
drawTower(game.enemyKing, '敌', game.execution && game.execution.victimSide === 'enemy');
drawTower(game.enemyTowerL, '敌', false);
drawTower(game.enemyTowerR, '敌', false);
drawTower(game.playerKing, '我', game.execution && game.execution.victimSide === 'player');
drawTower(game.playerTowerL, '我', false);
drawTower(game.playerTowerR, '我', false);
if (game.state !== 'execution') {
for (const u of game.units) drawUnit(u);
} else {
ctx.globalAlpha = 0.35;
for (const u of game.units) drawUnit(u);
ctx.globalAlpha = 1;
}
// ===== 攻击特效渲染(单位上方) =====
drawSlashes();
drawProjectiles();
drawBeams();
drawHitSparks();
drawEffects();
drawFloatingTexts();
drawLaneWarnings();
if (game.execution) drawExecution();
drawTopBar();
if (game.state !== 'execution') { drawCards(); drawElixirBar(); drawDeployPreview(); }
ctx.restore();
}
// ===== 弹道绘制 =====
function drawProjectiles() {
for (const p of game.projectiles) {
if (p.dead) continue;
ctx.save();
ctx.shadowColor = p.color;
ctx.shadowBlur = 15;
const t = p.t / p.dur;
if (p.type === 'bolt') {
// 能量球
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
// 拖尾
ctx.globalAlpha = 0.6;
ctx.beginPath();
ctx.arc(p.x - (p.tx - p.sx) * 0.05, p.y - (p.ty - p.sy) * 0.05, p.size * 0.7, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 0.3;
ctx.beginPath();
ctx.arc(p.x - (p.tx - p.sx) * 0.1, p.y - (p.ty - p.sy) * 0.1, p.size * 0.4, 0, Math.PI * 2);
ctx.fill();
} else if (p.type === 'arrow') {
// 箭矢
ctx.translate(p.x, p.y);
ctx.rotate(p.angle);
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.moveTo(-12, 0);
ctx.lineTo(8, -3);
ctx.lineTo(12, 0);
ctx.lineTo(8, 3);
ctx.closePath();
ctx.fill();
// 箭头光
ctx.globalAlpha = 0.5;
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(12, 0, 2, 0, Math.PI * 2);
ctx.fill();
} else if (p.type === 'snipe') {
// 狙击弹:长条形
const ang = Math.atan2(p.ty - p.sy, p.tx - p.sx);
ctx.translate(p.x, p.y);
ctx.rotate(ang);
ctx.fillStyle = p.color;
ctx.fillRect(-20, -2, 30, 4);
ctx.fillStyle = '#fff';
ctx.fillRect(5, -3, 6, 6);
} else if (p.type === 'magic') {
// 魔法弹:旋转六角星
ctx.translate(p.x, p.y);
ctx.rotate(p.spin || 0);
ctx.fillStyle = p.color;
ctx.beginPath();
for (let i = 0; i < 6; i++) {
const ang = i * Math.PI / 3;
const r = i % 2 === 0 ? p.size : p.size * 0.5;
const px = Math.cos(ang) * r;
const py = Math.sin(ang) * r;
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
}
ctx.closePath();
ctx.fill();
// 中心光
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(0, 0, p.size * 0.3, 0, Math.PI * 2);
ctx.fill();
} else if (p.type === 'frost') {
// 霜冻弹:雪花
ctx.translate(p.x, p.y);
ctx.strokeStyle = p.color;
ctx.lineWidth = 2;
for (let i = 0; i < 6; i++) {
const ang = i * Math.PI / 3;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(Math.cos(ang) * p.size, Math.sin(ang) * p.size);
ctx.stroke();
}
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(0, 0, 2, 0, Math.PI * 2);
ctx.fill();
} else if (p.type === 'rapid') {
// 快速弹:小光点
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
ctx.globalAlpha = 1;
}
// ===== 光束绘制 =====
function drawBeams() {
for (const b of game.beams) {
if (b.dead) continue;
const alpha = b.life / b.maxLife;
ctx.save();
ctx.globalAlpha = alpha;
ctx.shadowColor = b.color;
ctx.shadowBlur = 20;
// 外光晕
ctx.strokeStyle = b.color;
ctx.lineWidth = b.width * 2;
ctx.globalAlpha = alpha * 0.4;
ctx.beginPath();
ctx.moveTo(b.x1, b.y1);
ctx.lineTo(b.x2, b.y2);
ctx.stroke();
// 内芯
ctx.globalAlpha = alpha;
ctx.strokeStyle = '#fff';
ctx.lineWidth = b.width * 0.5;
ctx.beginPath();
ctx.moveTo(b.x1, b.y1);
ctx.lineTo(b.x2, b.y2);
ctx.stroke();
ctx.restore();
}
ctx.globalAlpha = 1;
}
// ===== 斩击绘制 =====
function drawSlashes() {
for (const s of game.slashes) {
if (s.dead) continue;
const alpha = s.life / s.maxLife;
ctx.save();
ctx.globalAlpha = alpha;
ctx.translate(s.x, s.y);
ctx.rotate(s.angle);
ctx.shadowColor = s.color;
ctx.shadowBlur = 20;
// 斩击弧线
ctx.strokeStyle = s.color;
ctx.lineWidth = 6;
ctx.beginPath();
ctx.arc(0, 0, s.size, -Math.PI * 0.4, Math.PI * 0.4);
ctx.stroke();
// 内芯白
ctx.strokeStyle = '#fff';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(0, 0, s.size, -Math.PI * 0.4, Math.PI * 0.4);
ctx.stroke();
ctx.restore();
}
ctx.globalAlpha = 1;
}
// ===== 命中火花 =====
function drawHitSparks() {
for (const s of game.hitSparks) {
if (s.dead) continue;
const alpha = s.life / s.maxLife;
ctx.globalAlpha = alpha;
ctx.fillStyle = s.color;
ctx.beginPath();
ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
function drawExecution() {
const ex = game.execution;
if (!ex) return;
if (ex.phase === 0) {
ctx.fillStyle = `rgba(0, 0, 0, ${Math.min(0.7, ex.phaseTimer / 1.5 * 0.7)})`;
ctx.fillRect(0, 0, W, H);
const alpha = Math.min(1, ex.phaseTimer / 0.5) * Math.min(1, (1.5 - ex.phaseTimer) / 0.3);
ctx.fillStyle = `rgba(255, 40, 60, ${alpha})`;
ctx.font = 'bold 72px Microsoft YaHei';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('处 决', W/2, H/2);
ctx.font = 'bold 20px Microsoft YaHei';
ctx.fillStyle = `rgba(255, 200, 0, ${alpha})`;
ctx.fillText('胜者将赐予败者最后一击...', W/2, H/2 + 70);
}
if (ex.phase >= 1 && ex.phase <= 3) {
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
ctx.fillRect(0, 0, W, H);
const e = ex.executioner;
const winnerColor = ex.winnerSide === 'player' ? '#3d9bff' : '#ff3d71';
ctx.save();
ctx.translate(e.x, e.y);
const dx = e.endX - e.startX, dy = e.endY - e.startY;
const ang = Math.atan2(dy, dx);
ctx.rotate(ang);
ctx.shadowColor = winnerColor; ctx.shadowBlur = 30;
ctx.fillStyle = '#e0e8f0';
ctx.beginPath();
ctx.moveTo(30, -8); ctx.lineTo(120, -4); ctx.lineTo(140, 0); ctx.lineTo(120, 4); ctx.lineTo(30, 8);
ctx.closePath(); ctx.fill();
ctx.fillStyle = '#4a5a70';
ctx.fillRect(-10, -6, 40, 12);
ctx.fillStyle = winnerColor;
ctx.fillRect(28, -14, 8, 28);
ctx.restore();
}
if (ex.phase === 2) {
const t = ex.phaseTimer / 0.5;
const flashAlpha = Math.sin(t * Math.PI) * 0.9;
ctx.fillStyle = `rgba(255, 255, 255, ${flashAlpha})`;
ctx.fillRect(0, 0, W, H);
ctx.save();
ctx.strokeStyle = `rgba(255, 60, 90, ${flashAlpha})`;
ctx.lineWidth = 6;
ctx.shadowColor = '#ff3d71'; ctx.shadowBlur = 40;
ctx.beginPath();
ctx.moveTo(ex.victimX - 200, ex.victimY - 150);
ctx.lineTo(ex.victimX + 200, ex.victimY + 150);
ctx.stroke();
ctx.restore();
}
if (ex.phase >= 3) {
for (const p of ex.bloodParticles) {
const a = Math.max(0, p.life / p.maxLife);
ctx.globalAlpha = a;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
if (ex.phaseTimer < 1) {
const t = ex.phaseTimer;
ctx.fillStyle = `rgba(140, 0, 30, ${0.6 * (1 - t)})`;
ctx.beginPath();
ctx.arc(ex.victimX, ex.victimY, 100 + t * 200, 0, Math.PI * 2);
ctx.fill();
}
if (ex.headFalling) {
ctx.save();
ctx.translate(ex.headFalling.x, ex.headFalling.y);
ctx.rotate(ex.headFalling.rot);
ctx.fillStyle = '#e0c8a0';
ctx.beginPath(); ctx.arc(0, 0, 18, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#000';
ctx.beginPath(); ctx.arc(-5, -3, 2, 0, Math.PI * 2); ctx.arc(5, -3, 2, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#8a0020';
ctx.beginPath();
ctx.arc(0, 12, 3, 0, Math.PI * 2);
ctx.arc(-8, 10, 2, 0, Math.PI * 2);
ctx.arc(8, 10, 2, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
if (ex.phase === 4 && ex.showTitle) {
const t = ex.phaseTimer;
const alpha = Math.min(1, t / 0.5);
ctx.fillStyle = `rgba(0, 0, 0, ${0.5 * alpha})`;
ctx.fillRect(0, 0, W, H);
ctx.save();
ctx.shadowColor = ex.winnerSide === 'player' ? '#00ffa0' : '#ff3d71';
ctx.shadowBlur = 30;
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
ctx.font = 'bold 60px Microsoft YaHei';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(ex.titleText, W/2, H/2 - 40);
ctx.restore();
ctx.fillStyle = `rgba(255, 200, 0, ${alpha})`;
ctx.font = 'bold 22px Microsoft YaHei';
ctx.fillText(ex.titleSub, W/2, H/2 + 30);
ctx.fillStyle = `rgba(160, 176, 192, ${alpha})`;
ctx.font = '14px Microsoft YaHei';
ctx.fillText('正在结束战斗...', W/2, H/2 + 80);
}
}
function drawField() {
ctx.fillStyle = 'rgba(60, 20, 50, 0.4)';
ctx.fillRect(0, FIELD_TOP, W, MID_Y - RIVER_H/2 - FIELD_TOP);
ctx.fillStyle = 'rgba(20, 40, 80, 0.5)';
ctx.fillRect(0, MID_Y + RIVER_H/2, W, FIELD_BOTTOM - MID_Y - RIVER_H/2);
ctx.setLineDash([6, 8]);
ctx.lineWidth = 1.5;
[W * 0.35, W * 0.65].forEach(lx => {
ctx.strokeStyle = 'rgba(100, 120, 150, 0.3)';
ctx.beginPath(); ctx.moveTo(lx, FIELD_TOP); ctx.lineTo(lx, FIELD_BOTTOM); ctx.stroke();
});
ctx.setLineDash([]);
ctx.font = 'bold 11px Microsoft YaHei';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillStyle = 'rgba(255, 255, 255, 0.25)';
ctx.fillText('左路', LANES.LEFT.x, FIELD_TOP + 20);
ctx.fillText('中路', LANES.MID.x, FIELD_TOP + 20);
ctx.fillText('右路', LANES.RIGHT.x, FIELD_TOP + 20);
ctx.fillText('左路', LANES.LEFT.x, FIELD_BOTTOM - 20);
ctx.fillText('中路', LANES.MID.x, FIELD_BOTTOM - 20);
ctx.fillText('右路', LANES.RIGHT.x, FIELD_BOTTOM - 20);
drawFlagOnGame(20, FIELD_TOP + 8, 32, 32, flagData2, '#ff3d71');
ctx.fillStyle = 'rgba(255, 61, 113, 0.9)';
ctx.font = 'bold 12px Microsoft YaHei'; ctx.textAlign = 'left';
ctx.fillText(ALLIANCE_NAMES.ally2, 60, FIELD_TOP + 24);
drawFlagOnGame(20, FIELD_BOTTOM - 40, 32, 32, flagData1, '#3d9bff');
ctx.fillStyle = 'rgba(61, 155, 255, 0.9)';
ctx.fillText(ALLIANCE_NAMES.ally1, 60, FIELD_BOTTOM - 24);
const riverGrad = ctx.createLinearGradient(0, RIVER_Y, 0, RIVER_Y + RIVER_H);
riverGrad.addColorStop(0, 'rgba(0, 150, 220, 0.7)');
riverGrad.addColorStop(0.5, 'rgba(0, 200, 255, 0.9)');
riverGrad.addColorStop(1, 'rgba(0, 150, 220, 0.7)');
ctx.fillStyle = riverGrad;
ctx.fillRect(0, RIVER_Y, W, RIVER_H);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
ctx.lineWidth = 1;
const t = game.totalTime;
for (let x = 0; x < W; x += 40) {
const offset = Math.sin(t * 2 + x * 0.05) * 5;
ctx.beginPath();
ctx.moveTo(x, RIVER_Y + RIVER_H/2 + offset);
ctx.lineTo(x + 20, RIVER_Y + RIVER_H/2 - offset);
ctx.stroke();
}
ctx.fillStyle = 'rgba(200, 180, 120, 0.25)';
ctx.fillRect(W/2 - MID_CROSS_W/2, RIVER_Y - 3, MID_CROSS_W, RIVER_H + 6);
ctx.strokeStyle = 'rgba(200, 180, 120, 0.6)';
ctx.lineWidth = 2;
ctx.strokeRect(W/2 - MID_CROSS_W/2, RIVER_Y - 3, MID_CROSS_W, RIVER_H + 6);
ctx.fillStyle = 'rgba(255, 220, 150, 0.7)';
ctx.font = 'bold 10px Microsoft YaHei'; ctx.textAlign = 'center';
ctx.fillText('浅滩', W/2, MID_Y + 3);
[BRIDGE_X1, BRIDGE_X2].forEach(bx => {
ctx.fillStyle = '#5a4530';
ctx.fillRect(bx - BRIDGE_W/2, RIVER_Y - 5, BRIDGE_W, RIVER_H + 10);
ctx.strokeStyle = '#8a6540'; ctx.lineWidth = 3;
ctx.strokeRect(bx - BRIDGE_W/2, RIVER_Y - 5, BRIDGE_W, RIVER_H + 10);
for (let y = RIVER_Y; y < RIVER_Y + RIVER_H; y += 10) {
ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
ctx.beginPath(); ctx.moveTo(bx - BRIDGE_W/2, y); ctx.lineTo(bx + BRIDGE_W/2, y); ctx.stroke();
}
});
}
function drawLaneWarnings() {
if (!game) return;
const t = game.totalTime;
for (const lane of ['LEFT', 'MID', 'RIGHT']) {
const timer = game.laneWarnings[lane];
if (timer > 0) {
const alpha = Math.min(1, timer) * (0.5 + Math.sin(t * 12) * 0.3);
const lx = LANES[lane].x;
const grad = ctx.createLinearGradient(lx - 100, 0, lx + 100, 0);
grad.addColorStop(0, 'rgba(255, 61, 113, 0)');
grad.addColorStop(0.5, `rgba(255, 61, 113, ${alpha * 0.4})`);
grad.addColorStop(1, 'rgba(255, 61, 113, 0)');
ctx.fillStyle = grad;
ctx.fillRect(lx - 100, FIELD_TOP, 200, FIELD_BOTTOM - FIELD_TOP);
ctx.fillStyle = `rgba(255, 61, 113, ${alpha})`;
ctx.font = 'bold 16px Microsoft YaHei';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('⚠️ ' + LANES[lane].name + '来袭', lx, FIELD_TOP - 10);
}
}
}
function drawTower(t, label, isBeingExecuted) {
if (t.hp <= 0) {
ctx.fillStyle = 'rgba(50, 50, 50, 0.5)';
ctx.fillRect(t.x - t.w/2, t.y - t.h/2, t.w, t.h);
ctx.fillStyle = '#333';
ctx.font = 'bold 20px Microsoft YaHei';
ctx.textAlign = 'center';
ctx.fillText('✕', t.x, t.y + 7);
return;
}
const isPlayer = t.side === 'player';
const baseColor = isPlayer ? '#3d9bff' : '#ff3d71';
const darkColor = isPlayer ? '#1a4b7f' : '#8a1a2f';
if (isBeingExecuted) {
ctx.save();
ctx.translate(t.x, t.y);
ctx.rotate(Math.sin(game.totalTime * 20) * 0.05);
ctx.translate(-t.x, -t.y);
}
// 攻击闪光
if (t.attackFlash > 0) {
ctx.save();
ctx.globalAlpha = t.attackFlash * 3;
ctx.shadowColor = baseColor;
ctx.shadowBlur = 30;
ctx.fillStyle = baseColor;
ctx.fillRect(t.x - t.w/2 - 5, t.y - t.h/2 - 5, t.w + 10, t.h + 10);
ctx.restore();
}
ctx.fillStyle = darkColor;
ctx.fillRect(t.x - t.w/2, t.y - t.h/2, t.w, t.h);
ctx.fillStyle = baseColor;
ctx.fillRect(t.x - t.w/2 + 3, t.y - t.h/2 + 3, t.w - 6, t.h - 6);
ctx.fillStyle = darkColor;
if (t.type === 'king') {
ctx.beginPath();
ctx.moveTo(t.x - t.w/2 - 5, t.y - t.h/2);
ctx.lineTo(t.x + t.w/2 + 5, t.y - t.h/2);
ctx.lineTo(t.x + t.w/2 - 5, t.y - t.h/2 - 15);
ctx.lineTo(t.x, t.y - t.h/2 - 5);
ctx.lineTo(t.x - t.w/2 + 5, t.y - t.h/2 - 15);
ctx.closePath(); ctx.fill();
ctx.fillStyle = '#ffdd00';
ctx.beginPath(); ctx.arc(t.x, t.y - t.h/2 - 5, 4, 0, Math.PI * 2); ctx.fill();
} else {
ctx.beginPath();
ctx.moveTo(t.x - t.w/2 - 5, t.y - t.h/2);
ctx.lineTo(t.x + t.w/2 + 5, t.y - t.h/2);
ctx.lineTo(t.x, t.y - t.h/2 - 20);
ctx.closePath(); ctx.fill();
}
const flag = isPlayer ? flagData1 : flagData2;
const flagW = t.type === 'king' ? 40 : 32;
const flagH = t.type === 'king' ? 40 : 32;
drawFlagOnGame(t.x - flagW/2, t.y - flagH/2, flagW, flagH, flag, isPlayer ? '#3d9bff' : '#ff3d71');
const hpPct = t.hp / t.maxHp;
const barW = t.w + 10;
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
ctx.fillRect(t.x - barW/2, t.y + t.h/2 + 5, barW, 6);
ctx.fillStyle = hpPct > 0.5 ? '#00ffa0' : hpPct > 0.25 ? '#ffaa00' : '#ff3d71';
ctx.fillRect(t.x - barW/2, t.y + t.h/2 + 5, barW * hpPct, 6);
if (isBeingExecuted) ctx.restore();
}
function drawUnit(u) {
const t = game.totalTime;
const bob = Math.sin(t * 4 + u.x * 0.1) * 2;
const size = u.big ? 1.4 : 1;
const laneColor = u.side === 'player' ? 'rgba(61, 155, 255, 0.15)' : 'rgba(255, 61, 113, 0.15)';
ctx.fillStyle = laneColor;
ctx.beginPath();
ctx.ellipse(u.x, u.y + 18 * size, 24 * size, 10 * size, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
ctx.beginPath();
ctx.ellipse(u.x, u.y + 18 * size, 18 * size, 6 * size, 0, 0, Math.PI * 2);
ctx.fill();
const flyOffset = u.flying ? Math.sin(t * 5 + u.x * 0.05) * 5 - 20 : 0;
// 攻击时前冲位移
let attackOffset = 0;
if (u.attackAnimTimer > 0) {
attackOffset = Math.sin((1 - u.attackAnimTimer / 0.25) * Math.PI) * 6 * u.facing;
}
ctx.save();
ctx.translate(u.x, u.y + bob + flyOffset + attackOffset);
ctx.scale(size, size);
ctx.strokeStyle = u.side === 'player' ? 'rgba(61, 155, 255, 0.6)' : 'rgba(255, 61, 113, 0.6)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(0, 0, 22, 0, Math.PI * 2);
ctx.stroke();
ctx.fillStyle = u.color;
if (u.big) {
roundRectPath(-16, -16, 32, 32, 6);
ctx.fill();
ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.stroke();
} else {
ctx.beginPath();
ctx.arc(0, 0, 14, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.stroke();
}
// 攻击发光
if (u.attackAnimTimer > 0) {
ctx.save();
ctx.globalAlpha = u.attackAnimTimer / 0.25 * 0.8;
ctx.shadowColor = u.atkColor;
ctx.shadowBlur = 20;
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(0, 0, 18, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
ctx.fillStyle = '#fff';
ctx.font = 'bold 14px Microsoft YaHei';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
const ownerFirst = (u.owner || '').charAt(0) || '?';
ctx.fillText(ownerFirst, 0, 0);
if (u.hitFlash > 0) {
ctx.globalAlpha = u.hitFlash * 7;
ctx.fillStyle = '#fff';
ctx.beginPath(); ctx.arc(0, 0, 24, 0, Math.PI * 2); ctx.fill();
ctx.globalAlpha = 1;
}
ctx.restore();
ctx.fillStyle = u.side === 'player' ? '#66d9ff' : '#ff8fa0';
ctx.font = 'bold 10px Microsoft YaHei';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
if (u.owner) ctx.fillText(u.owner, u.x, u.y - 32 * size + flyOffset);
const hpPct = u.hp / u.maxHp;
const barW = 36 * size;
const barY = u.y + flyOffset - 26 * size;
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
ctx.fillRect(u.x - barW/2, barY, barW, 4);
ctx.fillStyle = u.side === 'player' ? '#3d9bff' : '#ff3d71';
ctx.fillRect(u.x - barW/2, barY, barW * hpPct, 4);
}
function roundRectPath(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
function drawEffects() {
for (const e of game.effects) {
const alpha = Math.max(0, e.life / e.maxLife);
ctx.globalAlpha = alpha;
if (e.type === 'spell') {
const r = e.radius || 0;
const g = ctx.createRadialGradient(e.x, e.y, 0, e.x, e.y, r);
g.addColorStop(0, e.color);
g.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = g;
ctx.beginPath(); ctx.arc(e.x, e.y, r, 0, Math.PI * 2); ctx.fill();
ctx.strokeStyle = e.color; ctx.lineWidth = 3;
ctx.beginPath(); ctx.arc(e.x, e.y, r, 0, Math.PI * 2); ctx.stroke();
} else if (e.type === 'boom' || e.type === 'explosion') {
const r = e.radius || (1 - alpha) * 40;
const g = ctx.createRadialGradient(e.x, e.y, 0, e.x, e.y, r);
g.addColorStop(0, e.color);
g.addColorStop(1, 'rgba(255, 60, 113, 0)');
ctx.fillStyle = g;
ctx.beginPath(); ctx.arc(e.x, e.y, r, 0, Math.PI * 2); ctx.fill();
} else if (e.type === 'hit') {
ctx.fillStyle = e.color;
ctx.beginPath(); ctx.arc(e.x, e.y, 8 * (1 - alpha) + 3, 0, Math.PI * 2); ctx.fill();
}
}
ctx.globalAlpha = 1;
}
function drawFloatingTexts() {
for (const f of game.floatingTexts) {
ctx.globalAlpha = Math.max(0, f.life);
ctx.fillStyle = f.color;
ctx.font = 'bold 14px Microsoft YaHei';
ctx.textAlign = 'center';
ctx.fillText(f.text, f.x, f.y);
}
ctx.globalAlpha = 1;
}
function drawTopBar() {
ctx.fillStyle = 'rgba(255, 61, 113, 0.3)';
roundRectPath(10, 10, 300, 58, 8); ctx.fill();
ctx.strokeStyle = '#ff3d71'; ctx.lineWidth = 2;
roundRectPath(10, 10, 300, 58, 8); ctx.stroke();
drawFlagOnGame(18, 20, 38, 38, flagData2, '#ff3d71');
ctx.fillStyle = '#ff3d71';
ctx.font = 'bold 15px Microsoft YaHei';
ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
ctx.fillText(ALLIANCE_NAMES.ally2 + ' 👑 ' + game.enemyCrowns, 62, 28);
ctx.font = 'bold 10px Microsoft YaHei';
ctx.fillStyle = '#ffaa00';
ctx.fillText(NAMES.ally2.join('·'), 62, 52);
ctx.fillStyle = 'rgba(61, 155, 255, 0.3)';
roundRectPath(W - 310, 10, 300, 58, 8); ctx.fill();
ctx.strokeStyle = '#3d9bff'; ctx.lineWidth = 2;
roundRectPath(W - 310, 10, 300, 58, 8); ctx.stroke();
drawFlagOnGame(W - 300, 20, 38, 38, flagData1, '#3d9bff');
ctx.fillStyle = '#3d9bff';
ctx.font = 'bold 15px Microsoft YaHei';
ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
ctx.fillText('👑 ' + game.playerCrowns + ' ' + ALLIANCE_NAMES.ally1, W - 256, 28);
ctx.font = 'bold 10px Microsoft YaHei';
ctx.fillStyle = '#00ffa0';
ctx.fillText(NAMES.ally1.join('·'), W - 256, 52);
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(W/2 - 60, 10, 120, 40);
ctx.strokeStyle = game.overtime ? '#ff3d71' : '#00e5ff';
ctx.lineWidth = 2;
ctx.strokeRect(W/2 - 60, 10, 120, 40);
const timeLeft = game.overtime ? game.overtimeTime : game.time;
const min = Math.floor(timeLeft / 60);
const sec = Math.floor(timeLeft % 60);
ctx.fillStyle = game.overtime ? '#ff3d71' : '#fff';
ctx.font = 'bold 24px Microsoft YaHei';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(`${min}:${sec.toString().padStart(2,'0')}`, W/2, 30);
if (game.overtime) {
ctx.fillStyle = '#ff3d71';
ctx.font = 'bold 12px Microsoft YaHei';
ctx.fillText('加时赛', W/2, 58);
}
}
function drawCards() {
const cardW = 100, cardH = 70;
const gap = 8;
const totalW = 4 * cardW + 3 * gap;
const startX = (W - totalW) / 2;
const y = H - 90;
ctx.fillStyle = 'rgba(20, 30, 50, 0.8)';
roundRectPath(startX - cardW - gap - 10, y + 15, cardW * 0.7, cardH * 0.7, 5);
ctx.fill();
ctx.strokeStyle = '#4a5a70'; ctx.lineWidth = 1.5; ctx.stroke();
const nextDef = CARDS[game.playerNext];
if (nextDef) {
ctx.fillStyle = '#fff';
ctx.font = 'bold 10px Microsoft YaHei';
ctx.textAlign = 'center';
ctx.fillText('下一张', startX - cardW/2 - gap - 10, y + 10);
ctx.fillStyle = nextDef.color;
ctx.beginPath();
ctx.arc(startX - cardW * 0.65 - gap, y + 15 + cardH * 0.35, 12, 0, Math.PI * 2);
ctx.fill();
}
for (let i = 0; i < 4; i++) {
const key = game.playerHand[i];
if (!key) continue;
const def = CARDS[key];
const x = startX + i * (cardW + gap);
const canAfford = game.elixir >= def.cost;
const selected = game.selectedCard === i;
ctx.save();
if (selected) { ctx.shadowColor = '#00e5ff'; ctx.shadowBlur = 20; }
ctx.fillStyle = selected ? 'rgba(0, 229, 255, 0.3)' : 'rgba(20, 30, 50, 0.9)';
roundRectPath(x, y, cardW, cardH, 6); ctx.fill();
ctx.strokeStyle = selected ? '#00e5ff' : (canAfford ? '#4a9bdf' : '#4a5a70');
ctx.lineWidth = 2; ctx.stroke();
ctx.restore();
ctx.save();
ctx.translate(x + cardW/2, y + cardH/2 - 12);
ctx.fillStyle = canAfford ? def.color : '#555';
ctx.beginPath(); ctx.arc(0, 0, 16, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#fff';
ctx.font = 'bold 11px Microsoft YaHei';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
const ownerName = getCardOwner(key, 'player');
ctx.fillText(ownerName.charAt(0) || '?', 0, 0);
ctx.restore();
ctx.fillStyle = canAfford ? '#fff' : '#666';
ctx.font = 'bold 9px Microsoft YaHei';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(getCardDisplayName(key, 'player'), x + cardW/2, y + cardH - 18);
ctx.fillStyle = canAfford ? '#66d9ff' : '#555';
ctx.font = 'bold 9px Microsoft YaHei';
ctx.fillText('【' + ownerName + '】', x + cardW/2, y + cardH - 6);
ctx.fillStyle = canAfford ? '#ff69b4' : '#666';
ctx.beginPath(); ctx.arc(x + 12, y + 12, 9, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#fff';
ctx.font = 'bold 11px Microsoft YaHei';
ctx.fillText(def.cost, x + 12, y + 12);
}
}
function drawElixirBar() {
const barW = 400, barH = 20;
const x = (W - barW) / 2;
const y = H - 20;
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
roundRectPath(x, y, barW, barH, 10); ctx.fill();
const segW = barW / 10;
for (let i = 0; i < 10; i++) {
if (i < game.elixir) {
const grad = ctx.createLinearGradient(x + i*segW, 0, x + (i+1)*segW, 0);
grad.addColorStop(0, '#ff69b4');
grad.addColorStop(1, '#ff1493');
ctx.fillStyle = grad;
} else {
ctx.fillStyle = 'rgba(50, 50, 60, 0.6)';
}
roundRectPath(x + i*segW + 2, y + 2, segW - 4, barH - 4, 6); ctx.fill();
}
ctx.fillStyle = '#fff';
ctx.font = 'bold 12px Microsoft YaHei';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(game.elixir + ' / 10', W/2, y + barH/2);
}
function drawDeployPreview() {
if (game.selectedCard === null) return;
const cardKey = game.playerHand[game.selectedCard];
if (!cardKey) return;
const def = CARDS[cardKey];
const mx = game.mouseX, my = game.mouseY;
const isSpell = def.type === 'spell';
const validZone = isSpell ? (my >= FIELD_TOP && my <= FIELD_BOTTOM) : (my >= MID_Y + 10 && my <= FIELD_BOTTOM);
const canAfford = game.elixir >= def.cost;
const valid = validZone && canAfford;
ctx.fillStyle = valid ? 'rgba(0, 255, 160, 0.08)' : 'rgba(255, 61, 113, 0.08)';
if (isSpell) ctx.fillRect(0, FIELD_TOP, W, FIELD_BOTTOM - FIELD_TOP);
else ctx.fillRect(0, MID_Y + 10, W, FIELD_BOTTOM - MID_Y - 10);
ctx.save();
ctx.globalAlpha = 0.6;
ctx.fillStyle = valid ? '#00ffa0' : '#ff3d71';
ctx.beginPath(); ctx.arc(mx, my, isSpell ? def.radius : 25, 0, Math.PI * 2); ctx.fill();
ctx.strokeStyle = valid ? '#00ffa0' : '#ff3d71';
ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(mx, my, isSpell ? def.radius : 25, 0, Math.PI * 2); ctx.stroke();
ctx.restore();
}
function endGame() {
if (game.state === 'ended' || game.state === 'execution') return;
let winner;
if (game.playerKing.hp <= 0) winner = 'enemy';
else if (game.enemyKing.hp <= 0) winner = 'player';
else if (game.playerCrowns > game.enemyCrowns) winner = 'player';
else if (game.playerCrowns < game.enemyCrowns) winner = 'enemy';
else {
const pHp = game.playerKing.hp + game.playerTowerL.hp + game.playerTowerR.hp;
const eHp = game.enemyKing.hp + game.enemyTowerL.hp + game.enemyTowerR.hp;
if (pHp > eHp) winner = 'player';
else if (pHp < eHp) winner = 'enemy';
else winner = 'player';
}
const victim = winner === 'player' ? 'enemy' : 'player';
startExecution(victim, winner);
}
// ===================== 输入 =====================
canvas.addEventListener('mousemove', e => {
if (!game) return;
const rect = canvas.getBoundingClientRect();
const sx = canvas.width / rect.width;
const sy = canvas.height / rect.height;
game.mouseX = (e.clientX - rect.left) * sx;
game.mouseY = (e.clientY - rect.top) * sy;
});
canvas.addEventListener('click', e => {
if (!game || game.state !== 'playing') return;
initAudio();
const rect = canvas.getBoundingClientRect();
const sx = canvas.width / rect.width;
const sy = canvas.height / rect.height;
const mx = (e.clientX - rect.left) * sx;
const my = (e.clientY - rect.top) * sy;
const cardW = 100, cardH = 70, gap = 8;
const totalW = 4 * cardW + 3 * gap;
const startX = (W - totalW) / 2;
const y = H - 90;
for (let i = 0; i < 4; i++) {
const x = startX + i * (cardW + gap);
if (mx >= x && mx <= x + cardW && my >= y && my <= y + cardH) {
const key = game.playerHand[i];
if (!key) return;
const def = CARDS[key];
if (game.elixir < def.cost) return;
game.selectedCard = game.selectedCard === i ? null : i;
return;
}
}
if (game.selectedCard !== null) deployCard(game.selectedCard, mx, my);
});
canvas.addEventListener('contextmenu', e => {
e.preventDefault();
if (game) game.selectedCard = null;
});
window.addEventListener('keydown', e => {
if (e.code === 'Escape' && game) game.selectedCard = null;
});
function renderMenuBackground() {
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, '#0a1220');
grad.addColorStop(1, '#0d1a2d');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
}
buildNameInputs();
initFlagEditor();
renderMenuBackground();
</script>
</body>
</html>Game Source: 矛盾升级:皇室战争
Creator: HyperShark77
Libraries: none
Complexity: complex (2145 lines, 85.5 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: game-hypershark77" to link back to the original. Then publish at arcadelab.ai/publish.