🎮ArcadeLab

电子斗蛐蛐

by RocketFlare63
1054 lines29.9 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>电子斗蛐蛐</title>
<style>
  html, body {
    margin: 0; padding: 0; height: 100%; background: #141414;
    display: flex; align-items: center; justify-content: center;
    overflow: hidden; font-family: sans-serif;
  }
  canvas {
    width: 94vw; height: 94vw;
    max-width: 800px; max-height: 800px;
    display: block; border-radius: 8px;
    box-shadow: 0 0 40px rgba(0,0,0,.8);
    touch-action: none; user-select: none;
    background: #1a1a1a; 
  }
</style>
</head>
<body>
<canvas id="game" width="800" height="800"></canvas>
<script>
'use strict';

// ==================== 超级报错拦截器 ====================
window.onerror = function(msg, url, line, col, error) {
  try {
    const cvs = document.getElementById('game');
    if (!cvs) return;
    const c = cvs.getContext('2d');
    if (!c) return;
    c.fillStyle = '#550000';
    c.fillRect(0, 0, 800, 800);
    c.fillStyle = '#ffffff';
    c.font = 'bold 24px sans-serif';
    c.textAlign = 'left';
    c.fillText('❌ 游戏代码崩溃', 30, 60);
    c.font = '18px sans-serif';
    c.fillText('原因: ' + msg, 30, 110);
    c.fillText('行号: ' + line + ':' + col, 30, 140);
  } catch (e) {}
  return true;
};

// ==================== 基础配置 ====================
const CELL = 80;
// 场地尺寸:1400(宽) X 2800(长)
const WORLD_W = 1400; 
const WORLD_H = 2800; 
const BORDER = 80; // 边界厚度
const PLAY = {
  minX: BORDER, maxX: WORLD_W - BORDER,
  minY: BORDER, maxY: WORLD_H - BORDER,
};

const VIEW = 800;
// 按比例缩放,保证整个场地能完整居中显示在 800x800 画布中
const SCALE = Math.min(VIEW / WORLD_W, VIEW / WORLD_H); 
const OFFSET_X = (VIEW - WORLD_W * SCALE) / 2;
const OFFSET_Y = (VIEW - WORLD_H * SCALE) / 2;

const G = CELL; // 基础单位

const canvas = document.getElementById('game');
canvas.width = VIEW; canvas.height = VIEW;
const ctx = canvas.getContext('2d');

// ==================== 工具函数 ====================
// 世界坐标转屏幕坐标,包含居中偏移
function w2s(x, y) { 
  return { 
    x: x * SCALE + OFFSET_X, 
    y: y * SCALE + OFFSET_Y 
  }; 
}
function dist(a, b) { return Math.hypot(a.x - b.x, a.y - b.y); }
function rand(a, b) { return a + Math.random() * (b - a); }
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }

function roundRect(c, x, y, w, h, r) {
  r = Math.min(r, w / 2, h / 2);
  c.beginPath();
  c.moveTo(x + r, y);
  c.arcTo(x + w, y, x + w, y + h, r);
  c.arcTo(x + w, y + h, x, y + h, r);
  c.arcTo(x, y + h, x, y, r);
  c.arcTo(x, y, x + w, y, r);
  c.closePath();
}

function wrapText(text, maxWidth, font) {
  ctx.font = font;
  const lines = [];
  let line = '';
  for (const ch of text) {
    const test = line + ch;
    if (ctx.measureText(test).width > maxWidth && line) {
      lines.push(line); line = ch;
    } else {
      line = test;
    }
  }
  if (line) lines.push(line);
  return lines;
}

// ==================== 状态 ====================
const STATE = { MENU: 0, BATTLE: 1, RESULT: 2 };
let state = STATE.MENU;
let winnerText = '';
let resultBtn = null;
let lastBattleIds = null;

// ==================== 世界 ====================
const world = {
  fighters: [], minions: [], bullets: [], grenades: [],
  zones: [], damageTexts: [], explosions: [],
  reset() {
    this.fighters = []; this.minions = []; this.bullets = [];
    this.grenades = []; this.zones = []; this.damageTexts = [];
    this.explosions = [];
  }
};

// ==================== 实体基类 ====================
class Entity {
  constructor(x, y, radius) {
    this.x = x; this.y = y; this.radius = radius; this.dead = false;
  }
  get isOut() {
    return this.x < PLAY.minX || this.x > PLAY.maxX ||
           this.y < PLAY.minY || this.y > PLAY.maxY;
  }
  clampToArena() {
    let hit = false;
    if (this.x - this.radius < PLAY.minX) { this.x = PLAY.minX + this.radius; hit = true; }
    else if (this.x + this.radius > PLAY.maxX) { this.x = PLAY.maxX - this.radius; hit = true; }
    if (this.y - this.radius < PLAY.minY) { this.y = PLAY.minY + this.radius; hit = true; }
    else if (this.y + this.radius > PLAY.maxY) { this.y = PLAY.maxY - this.radius; hit = true; }
    return hit;
  }
}

// ==================== 伤害飘字 ====================
class DamageText {
  constructor(x, y, value, big = false) {
    this.x = x + rand(-18, 18);
    this.y = y - 36;
    this.label = value < 10 ? value.toFixed(1) : String(Math.round(value));
    this.life = 0.9;
    this.big = big;
    this.vy = -72;
  }
  update(dt) {
    this.life -= dt;
    this.y += this.vy * dt;
    this.vy *= 0.96;
  }
  draw() {
    const p = w2s(this.x, this.y);
    const alpha = clamp(this.life / 0.32, 0, 1);
    ctx.globalAlpha = alpha;
    ctx.font = this.big ? 'bold 18px sans-serif' : 'bold 13px sans-serif';
    ctx.textAlign = 'center';
    ctx.lineWidth = 3;
    ctx.strokeStyle = 'rgba(0,0,0,0.85)';
    ctx.strokeText(this.label, p.x, p.y);
    ctx.fillStyle = this.big ? '#ffb020' : '#ff6b6b';
    ctx.fillText(this.label, p.x, p.y);
    ctx.globalAlpha = 1;
  }
}

// ==================== 子弹 ====================
class Bullet extends Entity {
  constructor(owner, x, y, tx, ty, speedPx, damage, radius = 12) {
    super(x, y, radius);
    this.owner = owner;
    this.damage = damage;
    const d = Math.hypot(tx - x, ty - y) || 1;
    this.vx = (tx - x) / d * speedPx;
    this.vy = (ty - y) / d * speedPx;
  }
  update(dt) {
    this.x += this.vx * dt;
    this.y += this.vy * dt;
    if (this.isOut) this.dead = true;
  }
  draw() {
    const p = w2s(this.x, this.y);
    ctx.beginPath();
    ctx.arc(p.x, p.y, this.radius * SCALE, 0, Math.PI * 2);
    ctx.fillStyle = this.owner.color || '#fff';
    ctx.fill();
  }
}

// ==================== 手雷 ====================
class Grenade extends Entity {
  constructor(owner, x, y, tx, ty, speedPx, damage, aoeRadius) {
    super(x, y, 14);
    this.owner = owner;
    this.damage = damage;
    this.aoeRadius = aoeRadius;
    this.tx = tx; this.ty = ty;
    const d = Math.hypot(tx - x, ty - y) || 1;
    this.vx = (tx - x) / d * speedPx;
    this.vy = (ty - y) / d * speedPx;
    this.rot = 0;
  }
  update(dt) {
    if (this.dead) return;
    this.x += this.vx * dt;
    this.y += this.vy * dt;
    this.rot += dt * 14;

    const dx = this.tx - this.x, dy = this.ty - this.y;
    const dot = dx * this.vx + dy * this.vy;
    if (dot <= 0 || Math.hypot(dx, dy) < 8) { this.explode(); return; }

    if (this.isOut) this.dead = true;
  }
  explode() {
    this.dead = true;
    world.explosions.push(new Explosion(this.x, this.y, this.aoeRadius));
    for (const f of world.fighters) {
      if (f.dead || f === this.owner || f.team === this.owner.team) continue;
      if (dist(this, f) <= this.aoeRadius + f.radius) f.takeDamage(this.damage, this.owner, { big: true });
    }
    for (const m of world.minions) {
      if (m.dead || m.team === this.owner.team) continue;
      if (dist(this, m) <= this.aoeRadius + m.radius) m.takeDamage(this.damage, this.owner, { big: true });
    }
  }
  draw() {
    const t = w2s(this.tx, this.ty);
    ctx.beginPath();
    ctx.arc(t.x, t.y, this.aoeRadius * SCALE, 0, Math.PI * 2);
    ctx.strokeStyle = 'rgba(255,90,40,0.4)';
    ctx.lineWidth = 1.5;
    ctx.setLineDash([5, 5]);
    ctx.stroke();
    ctx.setLineDash([]);

    const p = w2s(this.x, this.y);
    const r = this.radius * SCALE;
    ctx.save();
    ctx.translate(p.x, p.y);
    ctx.rotate(this.rot);
    ctx.beginPath();
    ctx.ellipse(0, 0, r, r * 0.72, 0, 0, Math.PI * 2);
    ctx.fillStyle = '#2b2b2b';
    ctx.fill();
    ctx.strokeStyle = '#ff6b3d';
    ctx.lineWidth = 1.5;
    ctx.stroke();
    ctx.beginPath();
    ctx.arc(0, -r * 0.72, r * 0.32, 0, Math.PI * 2);
    ctx.fillStyle = '#ff6b3d';
    ctx.fill();
    ctx.restore();
  }
}

// ==================== 爆炸特效 ====================
class Explosion {
  constructor(x, y, radius) {
    this.x = x; this.y = y; this.radius = radius;
    this.life = 0.45; this.maxLife = 0.45;
  }
  update(dt) { this.life -= dt; }
  draw() {
    const p = w2s(this.x, this.y);
    const t = 1 - this.life / this.maxLife;
    const r = this.radius * SCALE * (0.45 + 0.55 * t);
    const g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, Math.max(r, 0.1));
    g.addColorStop(0, `rgba(255,225,130,${0.8 * (1 - t)})`);
    g.addColorStop(0.6, `rgba(255,120,40,${0.55 * (1 - t)})`);
    g.addColorStop(1, 'rgba(255,60,20,0)');
    ctx.fillStyle = g;
    ctx.beginPath();
    ctx.arc(p.x, p.y, Math.max(r, 0.1), 0, Math.PI * 2);
    ctx.fill();
  }
}

// ==================== 红日区域 ====================
class RedSunZone extends Entity {
  constructor(owner, life = 3) {
    super(0, 0, 0);
    this.owner = owner;
    this.life = life;
    this.tickTimer = 0;
  }
  update(dt, fighters) {
    this.life -= dt;
    if (this.life <= 0) { this.dead = true; return; }
    this.tickTimer += dt;
    while (this.tickTimer >= 0.1) {
      this.tickTimer -= 0.1;
      for (const f of fighters) {
        if (f.dead || f.team === this.owner.team) continue;
        f.takeDamage(1.5, this.owner, { silent: true });
      }
    }
  }
  draw() {
    const c = w2s(WORLD_W / 2, WORLD_H / 2);
    const r = VIEW * 0.85;
    const g = ctx.createRadialGradient(c.x, c.y, 0, c.x, c.y, r);
    g.addColorStop(0, 'rgba(255,70,30,0.30)');
    g.addColorStop(1, 'rgba(255,70,30,0)');
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, VIEW, VIEW);
  }
}

// ==================== 角色基类 ====================
class Fighter extends Entity {
  constructor(name, x, y, hp, speedGrid, color) {
    super(x, y, CELL / 2);
    this.name = name;
    this.maxHp = hp; this.hp = hp;
    this.speedGrid = speedGrid;
    this.color = color;
    this.team = name;
    this.atkTimer = 0;
    this.dir = { x: 1, y: 0 };
    this.dirTimer = 0;
    this.damageMult = 1;
    this.setRandomDir();
  }
  get speed() { return this.speedGrid * G; }

  setRandomDir() {
    const a = Math.random() * Math.PI * 2;
    this.dir = { x: Math.cos(a), y: Math.sin(a) };
  }
  moveRandom(dt) {
    this.dirTimer -= dt;
    if (this.dirTimer <= 0) {
      this.setRandomDir();
      this.dirTimer = rand(1, 2);
    }
    this.x += this.dir.x * this.speed * dt;
    this.y += this.dir.y * this.speed * dt;
    if (this.clampToArena()) this.setRandomDir();
  }

  // 智能索敌:支持混合传入英雄和人民的集合
  nearestEnemy(all, excludeTeam) {
    let best = null, bd = Infinity;
    for (const f of all) {
      if (f.dead || f === this) continue;
      if (excludeTeam && f.team === excludeTeam) continue;
      const d = dist(this, f);
      if (d < bd) { bd = d; best = f; }
    }
    return best;
  }

  takeDamage(v, src, opts = {}) {
    if (this.dead) return;
    this.hp -= v;
    if (!opts.silent) {
      world.damageTexts.push(new DamageText(this.x, this.y, v, opts.big));
    }
    if (this.hp <= 0) { this.hp = 0; this.dead = true; this.onDeath(); }
  }
  onDeath() {}
  update(dt, world) {}

  draw() {
    const p = w2s(this.x, this.y);
    const r = this.radius * SCALE;
    ctx.beginPath();
    ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
    ctx.fillStyle = this.color;
    ctx.fill();
    ctx.strokeStyle = '#fff'; ctx.lineWidth = 1; ctx.stroke();

    const ratio = this.hp / this.maxHp;
    const bw = r * 2, bh = 4;
    ctx.fillStyle = '#000';
    ctx.fillRect(p.x - bw / 2, p.y - r - 8, bw, bh);
    ctx.fillStyle = ratio > 0.5 ? '#2ecc71' : ratio > 0.25 ? '#f1c40f' : '#e74c3c';
    ctx.fillRect(p.x - bw / 2, p.y - r - 8, bw * ratio, bh);

    ctx.fillStyle = '#fff';
    ctx.font = '11px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(this.name, p.x, p.y - r - 12);
  }
}

// ==================== 角色:一封普通的信 ====================
class LetterFighter extends Fighter {
  constructor(x, y) {
    super('一封普通的信', x, y, 1000, 4.5, '#dcdcdc');
    this.stage = 1;
    this.stageTimer = 0;
    this.bulletSpeed = 10 * G;
  }
  get atkCD()     { return this.stage === 1 ? 1 : this.stage === 2 ? 0.8 : 1.25; }
  get atkDamage() { return this.stage === 1 ? 50 : this.stage === 2 ? 60 : 75; }

  updateStage(dt) {
    this.stageTimer += dt;
    if (this.stage === 1 && this.stageTimer >= 20) {
      this.stage = 2; this.stageTimer = 0; this.speedGrid = 5;
    } else if (this.stage === 2 && this.stageTimer >= 25) {
      this.stage = 3; this.stageTimer = 0; this.speedGrid = 2;
    }
  }

  update(dt, world) {
    if (this.dead) return;
    this.updateStage(dt);
    this.moveRandom(dt);

    this.atkTimer -= dt;
    if (this.atkTimer <= 0) {
      // 锁定最近单位(包括英雄和人民)
      const allEnemies = [...world.fighters, ...world.minions];
      const enemy = this.nearestEnemy(allEnemies, this.team);
      if (!enemy) return;
      this.atkTimer = this.atkCD;

      if (this.stage === 3 && Math.random() < 0.05) return;
      if (this.stage === 3 && Math.random() < 0.03) {
        this.takeDamage(this.hp, this);
        return;
      }
      if (Math.random() < 0.05) {
        world.grenades.push(new Grenade(
          this, this.x, this.y, enemy.x, enemy.y,
          12 * G, 250, 4 * CELL
        ));
        return;
      }

      let dmg = this.atkDamage;
      if (this.stage === 2 && Math.random() < 0.15) dmg *= 1.2;
      dmg *= this.damageMult;

      world.bullets.push(new Bullet(
        this, this.x, this.y, enemy.x, enemy.y,
        this.bulletSpeed, dmg
      ));
    }
  }

  draw() {
    super.draw();
    const p = w2s(this.x, this.y);
    const names = ['待我回家', '带我回家', '代我回家'];
    ctx.fillStyle = '#ffeaa0';
    ctx.font = '10px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText('[' + names[this.stage - 1] + ']', p.x, p.y + this.radius * SCALE + 13);
  }
}

// ==================== 召唤物:人民 ====================
class Minion extends Fighter {
  constructor(x, y, owner) {
    // HP 改为 70
    super('人民', x, y, 70, 3, '#ff6b6b');
    this.owner = owner;
    this.team = owner.team;
    this.atkTimer = 0;
  }
  update(dt, world) {
    if (this.dead) return;
    // 人民也锁定所有最近的敌方单位(包括人民和英雄)
    const allEnemies = [...world.fighters, ...world.minions];
    const enemy = this.nearestEnemy(allEnemies, this.owner.team);
    if (!enemy) { this.moveRandom(dt); return; }

    const d = dist(this, enemy);
    if (d > this.radius + enemy.radius) {
      const nx = (enemy.x - this.x) / d;
      const ny = (enemy.y - this.y) / d;
      this.x += nx * this.speed * dt;
      this.y += ny * this.speed * dt;
      this.clampToArena();
    } else {
      this.atkTimer -= dt;
      if (this.atkTimer <= 0) {
        this.atkTimer = 1;
        enemy.takeDamage(25, this); // 伤害改为 25
      }
    }
  }
}

// ==================== 角色:红日 ====================
class RedSunFighter extends Fighter {
  constructor(x, y) {
    super('红日', x, y, 1050, 3, '#ff3b1f');
    this.produceTimer = 0;
    this.peopleTimer = 0;
    this.myMinions = [];
  }
  get atkCD() { return 1.5; }
  get atkDamage() { return 85; }

  update(dt, world) {
    if (this.dead) return;
    this.moveRandom(dt);

    this.produceTimer += dt;
    if (this.produceTimer >= 5) {
      this.produceTimer -= 5;
      this.hp = Math.min(this.maxHp, this.hp + 15);
      this.damageMult *= 1.05;
    }

    this.peopleTimer += dt;
    if (this.peopleTimer >= 8) {
      this.peopleTimer -= 8;
      for (let i = 0; i < 2; i++) {
        const a = Math.random() * Math.PI * 2;
        const r = Math.random() * 4 * CELL;
        let mx = this.x + Math.cos(a) * r;
        let my = this.y + Math.sin(a) * r;
        mx = clamp(mx, PLAY.minX + CELL / 2, PLAY.maxX - CELL / 2);
        my = clamp(my, PLAY.minY + CELL / 2, PLAY.maxY - CELL / 2);
        const m = new Minion(mx, my, this);
        world.minions.push(m);
        this.myMinions.push(m);
      }
    }

    this.atkTimer -= dt;
    if (this.atkTimer <= 0) {
      // 锁定最近单位(包括英雄和人民)
      const allEnemies = [...world.fighters, ...world.minions];
      const enemy = this.nearestEnemy(allEnemies, this.team);
      if (!enemy) return;
      this.atkTimer = this.atkCD;

      let dmg = this.atkDamage * this.damageMult;
      if (Math.random() < 0.05) dmg *= 1.1;

      world.bullets.push(new Bullet(
        this, this.x, this.y, enemy.x, enemy.y, 8 * G, dmg, 14
      ));

      if (Math.random() < 0.08) {
        world.zones.push(new RedSunZone(this, 3));
      }
    }
  }

  onDeath() {
    for (const m of this.myMinions) m.dead = true;
    this.myMinions.length = 0;
  }
}

// ==================== 角色数据库 ====================
const CHARACTER_DB = [
  {
    id: 'letter',
    name: '一封普通的信',
    brief: '在战争中,一个人的死亡显得如此普通。',
    color: '#dcdcdc',
    create: (x, y) => new LetterFighter(x, y),
  },
  {
    id: 'redsun',
    name: '红日',
    brief: '在遥远的东方,一轮红日冉冉升起。',
    color: '#ff3b1f',
    create: (x, y) => new RedSunFighter(x, y),
  },
];

// ==================== 选角界面 ====================
const menu = {
  selected: new Set(),
  hovered: null,
  hoverStart: false,
  pressedCard: null,
  pressStart: 0,
  longPressFired: false,
  briefId: null,
  toastText: '',
  toastTimer: 0,
  cards: [],
  startBtn: { x: 280, y: 360, w: 240, h: 60 },
  briefBox: { x: 100, y: 460, w: 600, h: 280 },
};

function layoutCards() {
  const n = CHARACTER_DB.length;
  const cols = Math.min(n, 3);
  const cardW = 190, cardH = 160, gap = 24;
  const totalW = cols * cardW + (cols - 1) * gap;
  const startX = (VIEW - totalW) / 2;
  const startY = 120;
  menu.cards = [];
  for (let i = 0; i < n; i++) {
    const r = Math.floor(i / cols);
    const c = i % cols;
    menu.cards.push({
      id: CHARACTER_DB[i].id,
      x: startX + c * (cardW + gap),
      y: startY + r * (cardH + gap),
      w: cardW, h: cardH,
    });
  }
}
layoutCards();

function showToast(text) {
  menu.toastText = text;
  menu.toastTimer = 1.6;
}

function updateMenu(dt) {
  if (menu.toastTimer > 0) menu.toastTimer -= dt;
  if (menu.pressedCard && !menu.longPressFired) {
    if (performance.now() - menu.pressStart >= 450) {
      menu.longPressFired = true;
      menu.briefId = menu.pressedCard;
    }
  }
}

function drawMenu() {
  ctx.fillStyle = '#1e1e1e';
  ctx.fillRect(0, 0, VIEW, VIEW);

  ctx.textAlign = 'center';
  ctx.fillStyle = '#f5e9c9';
  ctx.font = 'bold 34px sans-serif';
  ctx.fillText('电 子 斗 蛐 蛐', VIEW / 2, 58);

  ctx.fillStyle = '#aaaaaa';
  ctx.font = '15px sans-serif';
  ctx.fillText('选择 2 ~ 5 名角色进入竞技场', VIEW / 2, 90);

  for (const card of menu.cards) {
    const def = CHARACTER_DB.find(c => c.id === card.id);
    if (!def) continue;
    const selected = menu.selected.has(card.id);
    const hovered = menu.hovered === card.id;

    roundRect(ctx, card.x, card.y, card.w, card.h, 12);
    ctx.fillStyle = selected ? '#26303d' : (hovered ? '#2a2a2a' : '#252525');
    ctx.fill();
    ctx.lineWidth = selected ? 3 : 1.5;
    ctx.strokeStyle = selected ? '#5fc4ff' : '#444444';
    ctx.stroke();

    const cx = card.x + card.w / 2;
    const cy = card.y + 62;
    ctx.beginPath();
    ctx.arc(cx, cy, 30, 0, Math.PI * 2);
    ctx.fillStyle = def.color;
    ctx.fill();
    ctx.strokeStyle = 'rgba(255,255,255,0.55)';
    ctx.lineWidth = 1.5;
    ctx.stroke();

    ctx.fillStyle = '#eee';
    ctx.font = 'bold 16px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(def.name, cx, card.y + 122);

    if (selected) {
      ctx.beginPath();
      ctx.arc(card.x + card.w - 18, card.y + 18, 12, 0, Math.PI * 2);
      ctx.fillStyle = '#5fc4ff';
      ctx.fill();
      ctx.strokeStyle = '#fff';
      ctx.lineWidth = 2.2;
      ctx.beginPath();
      ctx.moveTo(card.x + card.w - 24, card.y + 18);
      ctx.lineTo(card.x + card.w - 19, card.y + 23);
      ctx.lineTo(card.x + card.w - 12, card.y + 13);
      ctx.stroke();
    }
  }

  ctx.fillStyle = '#8d8d8d';
  ctx.font = '14px sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText(`已选 ${menu.selected.size} / 5`, VIEW / 2, menu.startBtn.y - 14);

  const sb = menu.startBtn;
  roundRect(ctx, sb.x, sb.y, sb.w, sb.h, 10);
  ctx.fillStyle = menu.hoverStart ? '#4aa8e8' : '#3498db';
  ctx.fill();
  ctx.strokeStyle = '#7fd0ff';
  ctx.lineWidth = 1.5;
  ctx.stroke();
  ctx.fillStyle = '#fff';
  ctx.font = 'bold 22px sans-serif';
  ctx.fillText('开 始', sb.x + sb.w / 2, sb.y + sb.h / 2 + 8);

  const bb = menu.briefBox;
  roundRect(ctx, bb.x, bb.y, bb.w, bb.h, 12);
  ctx.fillStyle = '#222222';
  ctx.fill();
  ctx.strokeStyle = '#444444';
  ctx.lineWidth = 1.5;
  ctx.stroke();

  if (menu.briefId) {
    const def = CHARACTER_DB.find(c => c.id === menu.briefId);
    if (def) {
      ctx.textAlign = 'left';
      ctx.fillStyle = def.color;
      ctx.font = 'bold 22px sans-serif';
      ctx.fillText(def.name, bb.x + 26, bb.y + 44);

      ctx.fillStyle = '#c9c9c9';
      const font = '16px sans-serif';
      const lines = wrapText(def.brief, bb.w - 52, font);
      ctx.font = font;
      lines.forEach((line, i) => {
        ctx.fillText(line, bb.x + 26, bb.y + 88 + i * 28);
      });
    }
  } else {
    ctx.textAlign = 'center';
    ctx.fillStyle = '#777777';
    ctx.font = '15px sans-serif';
    ctx.fillText('长按角色卡片查看简介', bb.x + bb.w / 2, bb.y + bb.h / 2 + 5);
  }

  if (menu.toastTimer > 0) {
    const alpha = clamp(menu.toastTimer / 0.3, 0, 1);
    ctx.globalAlpha = alpha;
    const tw = 260, th = 96;
    const tx = VIEW / 2 - tw / 2;
    const ty = VIEW / 2 - th / 2;
    roundRect(ctx, tx, ty, tw, th, 14);
    ctx.fillStyle = 'rgba(32,10,10,0.96)';
    ctx.fill();
    ctx.strokeStyle = '#e74c3c';
    ctx.lineWidth = 2;
    ctx.stroke();

    ctx.beginPath();
    ctx.moveTo(VIEW / 2, ty + 22);
    ctx.lineTo(VIEW / 2 - 15, ty + 50);
    ctx.lineTo(VIEW / 2 + 15, ty + 50);
    ctx.closePath();
    ctx.strokeStyle = '#e74c3c';
    ctx.lineWidth = 2;
    ctx.stroke();

    ctx.fillStyle = '#e74c3c';
    ctx.font = 'bold 20px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(menu.toastText, VIEW / 2, ty + 78);
    ctx.globalAlpha = 1;
  }
}

// ==================== 开始战斗 ====================
function tryStart() {
  const n = menu.selected.size;
  if (n < 2) { showToast('角色过少'); return; }
  if (n > 5) { showToast('角色过多'); return; }
  const ids = CHARACTER_DB.filter(c => menu.selected.has(c.id)).map(c => c.id);
  startBattle(ids);
}

function startBattle(ids) {
  world.reset();
  winnerText = '';
  resultBtn = null;
  lastBattleIds = ids.slice();

  const defs = ids.map(id => CHARACTER_DB.find(c => c.id === id)).filter(Boolean);
  const n = defs.length;
  defs.forEach((def, i) => {
    const angle = (i / n) * Math.PI * 2 + rand(-0.25, 0.25);
    // 出生半径适应长方形场地
    const r = rand(300, 500); 
    let x = WORLD_W / 2 + Math.cos(angle) * r;
    let y = WORLD_H / 2 + Math.sin(angle) * r;
    x = clamp(x, PLAY.minX + 60, PLAY.maxX - 60);
    y = clamp(y, PLAY.minY + 60, PLAY.maxY - 60);
    world.fighters.push(def.create(x, y));
  });

  state = STATE.BATTLE;
  last = performance.now();
}

// ==================== 战斗逻辑 ====================
function resolveFighterCollisions(list) {
  for (let i = 0; i < list.length; i++) {
    for (let j = i + 1; j < list.length; j++) {
      const a = list[i], b = list[j];
      if (a.dead || b.dead) continue;
      const dx = b.x - a.x, dy = b.y - a.y;
      const d = Math.hypot(dx, dy);
      const minD = a.radius + b.radius;
      if (d < minD && d > 0) {
        const overlap = minD - d;
        const nx = dx / d, ny = dy / d;
        a.x -= nx * overlap / 2; a.y -= ny * overlap / 2;
        b.x += nx * overlap / 2; b.y += ny * overlap / 2;
        a.setRandomDir(); b.setRandomDir();
        a.clampToArena(); b.clampToArena();
      }
    }
  }
}

function bulletHits(b) {
  for (const f of world.fighters) {
    if (f.dead || f === b.owner || f.team === b.owner.team) continue;
    if (dist(b, f) < b.radius + f.radius) {
      f.takeDamage(b.damage, b.owner);
      return true;
    }
  }
  for (const m of world.minions) {
    if (m.dead || m.owner.team === b.owner.team) continue;
    if (dist(b, m) < b.radius + m.radius) {
      m.takeDamage(b.damage, b.owner);
      return true;
    }
  }
  return false;
}

function checkVictory() {
  if (state !== STATE.BATTLE) return;
  const alive = world.fighters.filter(f => !f.dead);
  if (alive.length === 1) {
    state = STATE.RESULT;
    winnerText = alive[0].name + ' 胜利!';
  } else if (alive.length === 0) {
    state = STATE.RESULT;
    winnerText = '同归于尽!';
  }
}

function updateBattle(dt) {
  for (const f of world.fighters) f.update(dt, world);
  world.fighters = world.fighters.filter(f => !f.dead);

  world.minions = world.minions.filter(m => !m.dead);
  for (const m of world.minions) m.update(dt, world);

  resolveFighterCollisions(world.fighters);

  for (const b of world.bullets) {
    b.update(dt);
    if (!b.dead && bulletHits(b)) b.dead = true;
  }
  world.bullets = world.bullets.filter(b => !b.dead);

  for (const g of world.grenades) g.update(dt);
  world.grenades = world.grenades.filter(g => !g.dead);

  for (const z of world.zones) z.update(dt, world.fighters);
  world.zones = world.zones.filter(z => !z.dead);

  for (const ex of world.explosions) ex.update(dt);
  world.explosions = world.explosions.filter(e => e.life > 0);

  for (const t of world.damageTexts) t.update(dt);
  world.damageTexts = world.damageTexts.filter(t => t.life > 0);

  checkVictory();
}

// ==================== 战斗渲染 ====================
function drawArena() {
  ctx.fillStyle = '#1a1a1a';
  ctx.fillRect(0, 0, VIEW, VIEW); // 整个画布底色(包括留白处)
  
  const p1 = w2s(PLAY.minX, PLAY.minY);
  const p2 = w2s(PLAY.maxX, PLAY.maxY);
  
  // 场地底色
  ctx.fillStyle = '#252525';
  ctx.fillRect(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y);

  // 绘制内部网格线
  ctx.strokeStyle = '#333333';
  ctx.lineWidth = 1;
  ctx.beginPath();
  for (let x = PLAY.minX; x <= PLAY.maxX; x += CELL) {
    const px = w2s(x, 0).x;
    ctx.moveTo(px, p1.y);
    ctx.lineTo(px, p2.y);
  }
  for (let y = PLAY.minY; y <= PLAY.maxY; y += CELL) {
    const py = w2s(0, y).y;
    ctx.moveTo(p1.x, py);
    ctx.lineTo(p2.x, py);
  }
  ctx.stroke();

  // 场地边界
  ctx.strokeStyle = '#888';
  ctx.lineWidth = 4;
  ctx.strokeRect(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y);
}

function drawBattle() {
  drawArena();
  for (const z of world.zones) z.draw();
  for (const ex of world.explosions) ex.draw();
  for (const g of world.grenades) g.draw();
  for (const m of world.minions) m.draw();
  for (const b of world.bullets) b.draw();
  for (const f of world.fighters) f.draw();
  for (const t of world.damageTexts) t.draw();

  if (state === STATE.RESULT) drawResult();
}

function drawResult() {
  ctx.fillStyle = 'rgba(0,0,0,0.7)';
  ctx.fillRect(0, 0, VIEW, VIEW);

  ctx.textAlign = 'center';
  ctx.fillStyle = '#ffe9a8';
  ctx.font = 'bold 38px sans-serif';
  ctx.fillText(winnerText, VIEW / 2, VIEW / 2 - 30);

  const bw = 220, bh = 58;
  const bx = VIEW / 2 - bw / 2, by = VIEW / 2 + 30;
  roundRect(ctx, bx, by, bw, bh, 10);
  ctx.fillStyle = '#3498db';
  ctx.fill();
  ctx.strokeStyle = '#7fd0ff';
  ctx.lineWidth = 1.5;
  ctx.stroke();
  ctx.fillStyle = '#fff';
  ctx.font = 'bold 20px sans-serif';
  ctx.fillText('再来一局', VIEW / 2, by + 37);

  resultBtn = { x: bx, y: by, w: bw, h: bh };
}

// ==================== 输入 ====================
function getPointerPos(e) {
  const rect = canvas.getBoundingClientRect();
  return {
    x: (e.clientX - rect.left) * (canvas.width / rect.width),
    y: (e.clientY - rect.top) * (canvas.height / rect.height),
  };
}
function hitCard(p) {
  for (const c of menu.cards) {
    if (p.x >= c.x && p.x <= c.x + c.w && p.y >= c.y && p.y <= c.y + c.h) return c;
  }
  return null;
}
function hitStart(p) {
  const b = menu.startBtn;
  return p.x >= b.x && p.x <= b.x + b.w && p.y >= b.y && p.y <= b.y + b.h;
}
function hitResult(p) {
  if (!resultBtn) return false;
  const b = resultBtn;
  return p.x >= b.x && p.x <= b.x + b.w && p.y >= b.y && p.y <= b.y + b.h;
}

canvas.addEventListener('pointerdown', (e) => {
  try { if (e.cancelable) e.preventDefault(); } catch(err){}
  const p = getPointerPos(e);
  if (state === STATE.MENU) {
    const card = hitCard(p);
    if (card) {
      menu.pressedCard = card.id;
      menu.pressStart = performance.now();
      menu.longPressFired = false;
    }
  }
});

canvas.addEventListener('pointerup', (e) => {
  try { if (e.cancelable) e.preventDefault(); } catch(err){}
  const p = getPointerPos(e);
  if (state === STATE.MENU) {
    const card = hitCard(p);
    if (card && menu.pressedCard === card.id && !menu.longPressFired) {
      if (menu.selected.has(card.id)) menu.selected.delete(card.id);
      else menu.selected.add(card.id);
    }
    menu.pressedCard = null;
    menu.longPressFired = false;

    if (hitStart(p)) tryStart();
  } else if (state === STATE.RESULT) {
    if (hitResult(p)) {
      if (lastBattleIds && lastBattleIds.length >= 2) {
        startBattle(lastBattleIds);
      } else {
        state = STATE.MENU;
        resultBtn = null;
      }
    }
  }
});

canvas.addEventListener('pointermove', (e) => {
  try { if (e.cancelable) e.preventDefault(); } catch(err){}
  const p = getPointerPos(e);
  if (state === STATE.MENU) {
    const card = hitCard(p);
    menu.hovered = card ? card.id : null;
    menu.hoverStart = hitStart(p);
    canvas.style.cursor = (card || menu.hoverStart) ? 'pointer' : 'default';
  } else if (state === STATE.RESULT) {
    canvas.style.cursor = hitResult(p) ? 'pointer' : 'default';
  } else {
    canvas.style.cursor = 'default';
  }
});

canvas.addEventListener('pointerleave', () => {
  menu.pressedCard = null;
  menu.longPressFired = false;
});

// ==================== 主循环 ====================
let last = performance.now();
function loop(now) {
  if (typeof now !== 'number') now = performance.now();
  const dt = Math.min((now - last) / 1000, 0.05);
  last = now;

  if (state === STATE.MENU) {
    updateMenu(dt);
    drawMenu();
  } else {
    if (state === STATE.BATTLE) updateBattle(dt);
    drawBattle();
  }

  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body>
</html>

Game Source: 电子斗蛐蛐

Creator: RocketFlare63

Libraries: none

Complexity: complex (1054 lines, 29.9 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-rocketflare63-mvbxb0zb" to link back to the original. Then publish at arcadelab.ai/publish.