经典大战 · 选人与决斗
by ApexFalcon66834 lines25.4 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>经典大战 · 选人与决斗</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; }
body {
background: radial-gradient(ellipse at 50% 40%, #0b1a24 0%, #05090d 55%, #010204 100%);
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
color: #d7f6ff;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
}
.screen {
position: fixed; inset: 0;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
transition: opacity .35s ease, transform .35s ease;
}
.screen.hidden {
opacity: 0; pointer-events: none;
transform: scale(1.02);
}
/* ---------- 开始界面 ---------- */
#screen-start { gap: 28px; }
.title-main {
font-size: clamp(32px, 7vw, 64px);
letter-spacing: 10px;
font-weight: 900;
color: #eafffd;
text-shadow: 0 0 18px rgba(0,255,224,.6), 0 0 60px rgba(0,200,180,.35);
}
.title-sub {
font-size: 13px; letter-spacing: 4px;
color: rgba(0,255,224,.55);
}
.start-btn {
margin-top: 8px;
padding: 16px 64px;
font-size: 18px; letter-spacing: 6px;
color: #031412;
background: linear-gradient(180deg, #9ffff2, #22e3c5);
border: none; border-radius: 6px;
box-shadow: 0 0 22px rgba(0,255,224,.45), inset 0 1px 0 rgba(255,255,255,.6);
cursor: pointer;
transition: transform .15s, box-shadow .15s;
}
.start-btn:hover { transform: translateY(-2px); box-shadow: 0 0 34px rgba(0,255,224,.7); }
.start-btn:active { transform: translateY(0); }
/* ---------- 选人界面 ---------- */
#screen-select { gap: 16px; padding: 20px; }
.select-title { font-size: 15px; letter-spacing: 5px; color: rgba(0,255,224,.7); }
.select-hint { font-size: 12px; color: rgba(215,246,255,.45); margin-top: -6px; }
.roster {
display: flex; gap: 10px;
flex-wrap: wrap; justify-content: center;
max-width: 760px;
}
.card {
width: 128px;
background: linear-gradient(170deg, rgba(14,32,44,.95), rgba(6,14,20,.98));
border: 1px solid rgba(0,255,224,.16);
border-radius: 8px;
padding: 9px 10px 8px;
cursor: pointer;
transition: border-color .18s, box-shadow .18s, transform .18s;
position: relative;
}
.card:hover { border-color: rgba(0,255,224,.45); transform: translateY(-2px); }
.card.selected {
border-color: #3dffe4;
box-shadow: 0 0 0 1px rgba(61,255,228,.5), 0 0 18px rgba(0,255,224,.25);
}
.card.selected::after {
content: "✓";
position: absolute; top: 5px; right: 6px;
width: 14px; height: 14px;
font-size: 10px; line-height: 14px; text-align: center;
color: #031412; background: #3dffe4;
border-radius: 50%;
font-weight: 700;
}
.char-name { font-size: 15px; font-weight: 800; letter-spacing: 1px; }
.char-source { font-size: 9px; color: rgba(215,246,255,.4); margin-top: 1px; }
.tag-row { display: flex; gap: 4px; margin: 6px 0 7px; }
.tag {
font-size: 9px; letter-spacing: 1px;
padding: 1px 5px; border-radius: 3px;
border: 1px solid rgba(0,255,224,.25);
color: rgba(0,255,224,.8);
}
.tag.ranged { border-color: rgba(255,190,90,.35); color: rgba(255,200,120,.9); }
.stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2px 8px;
font-size: 9px;
color: rgba(215,246,255,.5);
}
.stats b { color: rgba(215,246,255,.85); font-weight: 600; margin-left: 3px; }
.select-actions { display: flex; gap: 14px; margin-top: 6px; }
.btn {
padding: 12px 34px; font-size: 14px; letter-spacing: 3px;
border-radius: 5px; border: 1px solid rgba(0,255,224,.3);
background: rgba(0,255,224,.06);
color: rgba(0,255,224,.8);
cursor: pointer; transition: .18s;
}
.btn:hover:not(:disabled) { background: rgba(0,255,224,.14); }
.btn:disabled { opacity: .28; cursor: not-allowed; }
.btn-primary {
background: linear-gradient(180deg, #9ffff2, #22e3c5);
color: #031412; border: none; font-weight: 800;
}
.btn-primary:disabled { background: rgba(120,180,170,.2); color: rgba(215,246,255,.3); }
/* ---------- 地图界面 ---------- */
#screen-map { gap: 12px; }
.map-hud {
display: flex; gap: 40px; align-items: center;
font-size: 12px; letter-spacing: 1px;
color: rgba(215,246,255,.6);
}
.hud-name { font-weight: 700; color: #eafffd; font-size: 14px; }
.hud-hp { color: #3dffe4; font-weight: 700; }
.hud-vs { color: rgba(255,255,255,.2); font-size: 16px; letter-spacing: 4px; }
canvas { display: block; touch-action: none; }
.back-link {
font-size: 11px; color: rgba(0,255,224,.4);
cursor: pointer; letter-spacing: 2px;
margin-top: 4px;
}
.back-link:hover { color: rgba(0,255,224,.8); }
/* ---------- 获胜界面 ---------- */
#screen-result {
gap: 18px;
background: radial-gradient(ellipse at 50% 50%, rgba(0,40,36,.85), rgba(1,2,4,.96) 70%);
}
.result-label {
font-size: 13px; letter-spacing: 8px;
color: rgba(0,255,224,.55);
animation: fadeUp .5s ease;
}
.result-name {
font-size: clamp(40px, 10vw, 76px);
font-weight: 900;
letter-spacing: 8px;
color: #eafffd;
text-shadow: 0 0 24px rgba(0,255,224,.75), 0 0 90px rgba(0,200,180,.45);
animation: resultPop .55s cubic-bezier(.2,1.5,.5,1);
}
.result-sub {
font-size: 12px; letter-spacing: 3px;
color: rgba(215,246,255,.35);
margin-bottom: 8px;
animation: fadeUp .7s ease .1s both;
}
#btn-result-back {
animation: fadeUp .7s ease .18s both;
}
@keyframes resultPop {
0% { transform: scale(.6); opacity: 0; }
60% { transform: scale(1.08); opacity: 1; }
100% { transform: scale(1); opacity: 1; }
}
@keyframes fadeUp {
0% { transform: translateY(10px); opacity: 0; }
100% { transform: translateY(0); opacity: 1; }
}
</style>
</head>
<body>
<section class="screen" id="screen-start">
<div class="title-main">经典大战</div>
<div class="title-sub">CLASSIC ARENA · 文本作证 · 规则统一</div>
<button class="start-btn" id="btn-to-select">开 始 游 戏</button>
</section>
<section class="screen hidden" id="screen-select">
<div class="select-title">选择参战角色</div>
<div class="select-hint">至少选中 2 名角色,点击“开始决斗”进入地图</div>
<div class="roster" id="roster"></div>
<div class="select-actions">
<button class="btn" id="btn-back-start">返回</button>
<button class="btn btn-primary" id="btn-start-duel" disabled>开始决斗</button>
</div>
</section>
<section class="screen hidden" id="screen-map">
<div class="map-hud" id="map-hud"></div>
<canvas id="game"></canvas>
<div class="back-link" id="btn-back-select">← 返回选人</div>
</section>
<section class="screen hidden" id="screen-result">
<div class="result-label">胜 者</div>
<div class="result-name" id="result-name">—</div>
<div class="result-sub" id="result-sub">经典大战</div>
<button class="btn btn-primary" id="btn-result-back">返回重新选择</button>
</section>
<script>
(function () {
'use strict';
/* ============================================================
0. 角色数据
============================================================ */
const CHARACTERS = [
{
id: 'xiangzi', name: '祥子', source: '《骆驼祥子》',
type: 'melee', typeLabel: '近战',
hp: 100, sta: 100, mp: 0,
speed: 1.6, atk: 9,
atkRange: 1.4, detectRange: 5,
atkCd: 0.7, color: '#3dffe4',
},
{
id: 'ruifeng', name: '瑞丰', source: '《四世同堂》',
type: 'ranged', typeLabel: '远程',
hp: 72, sta: 80, mp: 100,
speed: 1.15, atk: 6,
atkRange: 6.5, detectRange: 0,
atkCd: 1.1, color: '#ffd76a',
},
{
id: 'fushan', name: '富善先生', source: '《四世同堂》',
type: 'ranged', typeLabel: '远程',
hp: 85, sta: 70, mp: 100,
speed: 1.0, atk: 7,
atkRange: 7.2, detectRange: 0,
atkCd: 1.25, color: '#7ec8ff',
},
];
/* ============================================================
1. 地图常量与物理
============================================================ */
const COLS = 20, ROWS = 20, CELL = 32, WALL = 16;
const GRID_W = COLS * CELL, GRID_H = ROWS * CELL;
const WORLD_W = GRID_W + WALL * 2, WORLD_H = GRID_H + WALL * 2;
const WALLS = [
{ x: 0, y: 0, w: WORLD_W, h: WALL },
{ x: 0, y: WORLD_H - WALL, w: WORLD_W, h: WALL },
{ x: 0, y: 0, w: WALL, h: WORLD_H },
{ x: WORLD_W - WALL, y: 0, w: WALL, h: WORLD_H },
];
function aabbHit(a, b) {
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
}
function moveBody(body, dx, dy) {
body.x += dx;
for (const wall of WALLS) {
if (!aabbHit(body, wall)) continue;
if (dx > 0) body.x = wall.x - body.w;
else if (dx < 0) body.x = wall.x + wall.w;
}
body.y += dy;
for (const wall of WALLS) {
if (!aabbHit(body, wall)) continue;
if (dy > 0) body.y = wall.y - body.h;
else if (dy < 0) body.y = wall.y + wall.h;
}
return body;
}
function cellCenter(col, row) {
return { x: WALL + col * CELL + CELL / 2, y: WALL + row * CELL + CELL / 2 };
}
/* ============================================================
2. 选人界面
============================================================ */
const rosterEl = document.getElementById('roster');
const btnStartDuel = document.getElementById('btn-start-duel');
const selected = new Set();
function renderRoster() {
rosterEl.innerHTML = CHARACTERS.map(c => {
const typeClass = c.type === 'ranged' ? 'tag ranged' : 'tag';
return `
<div class="card" data-id="${c.id}">
<div class="char-name">${c.name}</div>
<div class="char-source">${c.source}</div>
<div class="tag-row"><span class="${typeClass}">${c.typeLabel}</span></div>
<div class="stats">
<span>生命<b>${c.hp}</b></span>
<span>攻击<b>${c.atk}</b></span>
<span>速度<b>${c.speed}</b></span>
<span>射程<b>${c.atkRange}</b></span>
</div>
</div>
`;
}).join('');
rosterEl.querySelectorAll('.card').forEach(card => {
card.addEventListener('click', () => {
const id = card.dataset.id;
if (selected.has(id)) { selected.delete(id); card.classList.remove('selected'); }
else { selected.add(id); card.classList.add('selected'); }
btnStartDuel.disabled = selected.size < 2;
});
});
}
/* ============================================================
3. 界面切换
============================================================ */
const screens = {
start: document.getElementById('screen-start'),
select: document.getElementById('screen-select'),
map: document.getElementById('screen-map'),
result: document.getElementById('screen-result'),
};
function show(name) {
Object.values(screens).forEach(s => s.classList.add('hidden'));
screens[name].classList.remove('hidden');
}
document.getElementById('btn-to-select').addEventListener('click', () => {
renderRoster();
selected.clear();
btnStartDuel.disabled = true;
show('select');
});
document.getElementById('btn-back-start').addEventListener('click', () => show('start'));
document.getElementById('btn-back-select').addEventListener('click', () => {
stopDuel();
show('select');
});
/* ============================================================
4. 地图绘制
============================================================ */
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const mapHud = document.getElementById('map-hud');
function resizeCanvas() {
const size = Math.min(window.innerWidth * 0.9, window.innerHeight * 0.72, 640);
const dpr = window.devicePixelRatio || 1;
canvas.style.width = size + 'px';
canvas.style.height = size + 'px';
canvas.width = Math.round(size * dpr);
canvas.height = Math.round(size * dpr);
}
function drawArena(time) {
const scale = canvas.width / WORLD_W;
ctx.setTransform(scale, 0, 0, scale, 0, 0);
ctx.clearRect(0, 0, WORLD_W, WORLD_H);
const bg = ctx.createLinearGradient(WALL, WALL, WALL + GRID_W, WALL + GRID_H);
bg.addColorStop(0, '#0b1927');
bg.addColorStop(0.5, '#0d1e2e');
bg.addColorStop(1, '#061018');
ctx.fillStyle = bg;
ctx.fillRect(WALL, WALL, GRID_W, GRID_H);
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba(80,200,255,.10)';
ctx.beginPath();
for (let c = 0; c <= COLS; c++) {
const x = WALL + c * CELL;
ctx.moveTo(x, WALL); ctx.lineTo(x, WALL + GRID_H);
}
for (let r = 0; r <= ROWS; r++) {
const y = WALL + r * CELL;
ctx.moveTo(WALL, y); ctx.lineTo(WALL + GRID_W, y);
}
ctx.stroke();
const pulse = 0.5 + 0.5 * Math.sin(time * 0.002);
ctx.save();
ctx.shadowColor = 'rgba(0,255,224,.85)';
ctx.shadowBlur = 8 + pulse * 16;
ctx.beginPath();
ctx.rect(0, 0, WORLD_W, WORLD_H);
ctx.rect(WALL, WALL, GRID_W, GRID_H);
ctx.fillStyle = '#00cdb2';
ctx.fill('evenodd');
ctx.restore();
ctx.strokeStyle = `rgba(190,255,248,${0.4 + pulse * 0.45})`;
ctx.lineWidth = 2;
ctx.strokeRect(WALL + 1, WALL + 1, GRID_W - 2, GRID_H - 2);
}
/* ============================================================
5. 决斗逻辑
============================================================ */
let fighters = [];
let projectiles = []; // 远程投射物
let duelActive = false;
let rafId = null;
let lastTime = 0;
let duelSession = 0; // 防止旧计时器误弹结算
function assignWanderLeg(f) {
f.wanderAngle = Math.random() * Math.PI * 2;
f.wanderSpeedBoost = 1.15 + Math.random() * 1.05;
f.wanderDistLeft = (2.5 + Math.random() * 5) * CELL;
}
function stopDuel() {
duelActive = false;
if (rafId) cancelAnimationFrame(rafId);
rafId = null;
fighters = [];
projectiles = [];
}
function startDuel() {
stopDuel();
duelSession++;
resizeCanvas();
show('map');
const chosen = CHARACTERS.filter(c => selected.has(c.id));
const duo = chosen.slice(0, 2);
const spawnA = cellCenter(3, 17);
const spawnB = cellCenter(16, 2);
fighters = duo.map((c, i) => {
const f = {
def: c,
x: (i === 0 ? spawnA : spawnB).x - 13,
y: (i === 0 ? spawnA : spawnB).y - 13,
w: 26, h: 26,
hp: c.hp, maxHp: c.hp,
sta: c.sta, maxSta: c.sta,
mp: c.mp, maxMp: c.mp,
atkTimer: 0,
shield: 0, shieldTimer: 0, shieldInit: false,
marked: 0,
alive: true,
speedMult: 1,
wanderAngle: 0,
wanderSpeedBoost: 1,
wanderDistLeft: 0,
dashTimer: 0,
dashCooldown: 0,
hitFlash: 0, // 受击闪白
atkFlash: 0, // 攻击闪光
};
assignWanderLeg(f);
return f;
});
updateHud();
duelActive = true;
lastTime = performance.now();
rafId = requestAnimationFrame(duelLoop);
}
function duelLoop(now) {
if (!duelActive) return;
const dt = Math.min((now - lastTime) / 1000, 0.05);
lastTime = now;
update(dt);
render(now);
if (duelActive) rafId = requestAnimationFrame(duelLoop);
}
/* ---- 发射投射物 ---- */
function fireProjectile(owner, target, dmg, opts) {
projectiles.push({
owner,
target,
x: owner.x + owner.w / 2,
y: owner.y + owner.h / 2,
speed: opts.speed || 360,
dmg,
color: opts.color || '#fff',
radius: opts.radius || 5,
life: 3,
isRuifeng: !!opts.isRuifeng,
trail: [],
});
owner.atkFlash = 0.12;
}
/* ---- 统一伤害结算(技能在这里生效) ---- */
function applyDamage(attacker, target, dmg, meta) {
let finalDmg = dmg;
// 瑞丰标记:对被标记目标增伤,并刷新标记
if (meta.isRuifeng) {
if (target.marked > 0) finalDmg *= 1.2;
target.marked = 3;
}
// 富善护盾:抵挡 60%
if (target.shield > 0) {
finalDmg *= 0.4;
target.shield = 0;
target.shieldTimer = 0;
}
target.hp = Math.max(0, target.hp - finalDmg);
target.hitFlash = 0.15;
if (target.hp <= 0) {
target.alive = false;
endDuel(attacker);
return;
}
updateHud();
}
function update(dt) {
const aliveList = fighters.filter(f => f.alive);
if (aliveList.length < 2) return;
/* ---- 祥子:体力透支 ---- */
const xz = fighters.find(f => f.def.id === 'xiangzi' && f.alive);
if (xz) {
if (xz.hp / xz.maxHp < 0.4 && xz.sta > 0) {
xz.sta = Math.max(0, xz.sta - 2 * dt);
xz.speedMult = xz.sta > 0 ? 1.3 : 0.6;
} else {
xz.speedMult = 1;
}
}
/* ---- 富善先生:旧物屏障 ---- */
const fs = fighters.find(f => f.def.id === 'fushan' && f.alive);
if (fs) {
if (!fs.shieldInit) { fs.shield = 1; fs.shieldInit = true; }
fs.shieldTimer += dt;
if (fs.shieldTimer >= 12) { fs.shieldTimer = 0; fs.shield = 1; }
}
/* ---- 冷却、标记、闪烁倒计时 ---- */
for (const f of fighters) {
if (!f.alive) continue;
f.atkTimer = Math.max(0, f.atkTimer - dt);
if (f.marked > 0) f.marked = Math.max(0, f.marked - dt);
if (f.dashTimer > 0) f.dashTimer = Math.max(0, f.dashTimer - dt);
if (f.dashCooldown > 0) f.dashCooldown = Math.max(0, f.dashCooldown - dt);
if (f.hitFlash > 0) f.hitFlash = Math.max(0, f.hitFlash - dt);
if (f.atkFlash > 0) f.atkFlash = Math.max(0, f.atkFlash - dt);
}
/* ---- 每个单位:移动 / 攻击 ---- */
for (const f of fighters) {
if (!f.alive) continue;
const target = fighters.find(o => o !== f && o.alive);
if (!target) continue;
const dx = (target.x + target.w / 2) - (f.x + f.w / 2);
const dy = (target.y + target.h / 2) - (f.y + f.h / 2);
const targetDist = Math.hypot(dx, dy);
let moveAngle = f.wanderAngle;
let shouldMove = true;
let tracking = false;
if (f.def.type === 'melee' && targetDist <= f.def.detectRange * CELL) {
moveAngle = Math.atan2(dy, dx);
tracking = true;
if (targetDist <= f.def.atkRange * CELL) shouldMove = false;
}
// 随机冲刺
if (!tracking && f.dashCooldown <= 0 && Math.random() < 0.008) {
f.dashTimer = 0.15 + Math.random() * 0.2;
f.dashCooldown = 0.8 + Math.random() * 1.0;
}
// 移动
if (shouldMove) {
let speedMul = tracking ? 1.0 : f.wanderSpeedBoost;
if (f.dashTimer > 0) speedMul *= 2.5;
const speed = f.def.speed * CELL * (f.speedMult || 1) * speedMul * dt;
const oldX = f.x, oldY = f.y;
moveBody(f, Math.cos(moveAngle) * speed, Math.sin(moveAngle) * speed);
const moved = Math.hypot(f.x - oldX, f.y - oldY);
if (moved < speed * 0.4) {
if (!tracking) {
assignWanderLeg(f);
f.dashTimer = 0;
}
} else if (!tracking) {
f.wanderDistLeft -= moved;
if (f.wanderDistLeft <= 0) assignWanderLeg(f);
}
}
// 攻击
if (targetDist <= f.def.atkRange * CELL && f.atkTimer <= 0) {
f.atkTimer = f.def.atkCd;
if (f.def.type === 'melee') {
// 近战:立即命中
f.atkFlash = 0.15;
applyDamage(f, target, f.def.atk, { isRuifeng: false });
} else {
// 远程:发射投射物
let dmg = f.def.atk;
const isRuifeng = f.def.id === 'ruifeng';
if (isRuifeng && targetDist < CELL * 2) dmg *= 0.65; // 被贴身削弱
fireProjectile(f, target, dmg, {
speed: 360,
color: f.def.color,
radius: 5,
isRuifeng,
});
}
}
}
/* ---- 投射物更新 ---- */
for (let i = projectiles.length - 1; i >= 0; i--) {
const p = projectiles[i];
p.life -= dt;
// 过期或目标已死
if (p.life <= 0 || !p.target || !p.target.alive) {
projectiles.splice(i, 1);
continue;
}
const tx = p.target.x + p.target.w / 2;
const ty = p.target.y + p.target.h / 2;
const dx = tx - p.x;
const dy = ty - p.y;
const dist = Math.hypot(dx, dy);
const hitR = p.target.w / 2 + p.radius;
// 命中判定
if (dist <= hitR) {
applyDamage(p.owner, p.target, p.dmg, { isRuifeng: p.isRuifeng });
projectiles.splice(i, 1);
continue;
}
const step = p.speed * dt;
if (step >= dist - hitR) {
// 本帧会到达,直接命中
applyDamage(p.owner, p.target, p.dmg, { isRuifeng: p.isRuifeng });
projectiles.splice(i, 1);
continue;
}
// 飞行
p.x += (dx / dist) * step;
p.y += (dy / dist) * step;
// 拖尾
p.trail.push({ x: p.x, y: p.y });
if (p.trail.length > 7) p.trail.shift();
}
}
function endDuel(winner) {
duelActive = false;
if (rafId) cancelAnimationFrame(rafId);
rafId = null;
const mySession = duelSession;
setTimeout(() => {
// 会话已切换,说明玩家已经返回重开,不再弹结算
if (mySession !== duelSession) return;
if (screens.map.classList.contains('hidden')) return;
document.getElementById('result-name').textContent = winner.def.name;
document.getElementById('result-sub').textContent = winner.def.source + ' · 经典大战';
show('result');
}, 550);
}
function updateHud() {
if (fighters.length < 2) return;
const [a, b] = fighters;
mapHud.innerHTML = `
<span><span class="hud-name">${a.def.name}</span> <span class="hud-hp">${Math.ceil(a.hp)}</span>/${a.maxHp}</span>
<span class="hud-vs">VS</span>
<span><span class="hud-name">${b.def.name}</span> <span class="hud-hp">${Math.ceil(b.hp)}</span>/${b.maxHp}</span>
`;
}
/* ---- 渲染 ---- */
function render(time) {
drawArena(time);
// 角色
for (const f of fighters) {
if (!f.alive) continue;
const cx = f.x + f.w / 2;
const cy = f.y + f.h / 2;
// 近战探测范围
if (f.def.type === 'melee' && f.def.detectRange > 0) {
ctx.save();
ctx.setLineDash([5, 5]);
ctx.strokeStyle = 'rgba(61,255,228,.18)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(cx, cy, f.def.detectRange * CELL, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
// 身体
const dashing = f.dashTimer > 0;
const hit = f.hitFlash > 0;
const atk = f.atkFlash > 0;
ctx.save();
ctx.shadowColor = f.def.color;
ctx.shadowBlur = hit ? 28 : (atk ? 22 : (dashing ? 26 : 12));
ctx.fillStyle = hit ? '#ffffff' : (atk ? '#ffffff' : (dashing ? '#ffffff' : f.def.color));
ctx.beginPath();
ctx.arc(cx, cy, f.w / 2, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// 名字
ctx.fillStyle = 'rgba(255,255,255,.85)';
ctx.font = '11px -apple-system, "PingFang SC", sans-serif';
ctx.textAlign = 'center';
ctx.fillText(f.def.name, cx, f.y - 8);
// 血条
const barW = 34, barH = 4;
const bx = cx - barW / 2, by = f.y - 20;
ctx.fillStyle = 'rgba(0,0,0,.5)';
ctx.fillRect(bx, by, barW, barH);
ctx.fillStyle = f.def.color;
ctx.fillRect(bx, by, barW * (f.hp / f.maxHp), barH);
// 富善护盾
if (f.shield > 0) {
ctx.strokeStyle = 'rgba(126,200,255,.8)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(cx, cy, f.w / 2 + 5, 0, Math.PI * 2);
ctx.stroke();
}
// 瑞丰标记
if (f.marked > 0) {
ctx.fillStyle = 'rgba(255,215,106,.9)';
ctx.beginPath();
ctx.moveTo(cx, f.y - 28);
ctx.lineTo(cx - 4, f.y - 34);
ctx.lineTo(cx + 4, f.y - 34);
ctx.closePath();
ctx.fill();
}
}
// 投射物
for (const p of projectiles) {
// 拖尾
for (let i = 0; i < p.trail.length; i++) {
const t = p.trail[i];
const k = (i + 1) / p.trail.length;
ctx.globalAlpha = k * 0.45;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(t.x, t.y, p.radius * (0.25 + k * 0.75), 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
// 光晕主体
ctx.save();
ctx.shadowColor = p.color;
ctx.shadowBlur = 16;
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// 彩色外圈
ctx.strokeStyle = p.color;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.stroke();
}
}
/* ============================================================
6. 启动
============================================================ */
document.getElementById('btn-start-duel').addEventListener('click', startDuel);
document.getElementById('btn-result-back').addEventListener('click', () => {
stopDuel();
duelSession++; // 让任何挂起的结算计时器失效
renderRoster();
selected.clear();
btnStartDuel.disabled = true;
show('select');
});
window.addEventListener('resize', () => {
if (!screens.map.classList.contains('hidden')) resizeCanvas();
});
show('start');
})();
</script>
</body>
</html>Game Source: 经典大战 · 选人与决斗
Creator: ApexFalcon66
Libraries: none
Complexity: complex (834 lines, 25.4 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-apexfalcon66-mvdey5al" to link back to the original. Then publish at arcadelab.ai/publish.