🎮ArcadeLab

僵尸生存模拟 伤害浮动文字+警察熟练度机制【支援警察+射程限制】

by EpicCoder88
1295 lines45.3 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>僵尸生存模拟 伤害浮动文字+警察熟练度机制【支援警察+射程限制】</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            background:#222;
            display:flex;
            flex-direction:column;
            align-items:center;
            justify-content:center;
            min-height:100vh;
            overflow:hidden;
        }
        canvas {
            background:#ffffff;
            display:block;
            max-width:100vw;
            max-height:100vh;
            touch-action:none;
        }
    </style>
</head>
<body>
<canvas id="game" width="900" height="620"></canvas>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');

function resizeCanvas() {
    const ratio = canvas.width / canvas.height;
    const winW = window.innerWidth;
    const winH = window.innerHeight;
    let w = winW, h = winW / ratio;
    if(h > winH){ h = winH; w = winH * ratio; }
    canvas.style.width = w + 'px';
    canvas.style.height = h + 'px';
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);

// ========== 参数配置 ==========
const WORLD_WIDTH = 1800;
const WORLD_HEIGHT = 1400;
const MAX_HP = 10;

const ZOMBIE_SPEED = 1.3;
const ZOMBIE_DETECT_RANGE = 160;

const RUNNER_SPEED = 2.8;
const RUNNER_DETECT_RANGE = 200;
const RUNNER_COLOR = "#c02020";

const HUMAN_VISION_RANGE = 140;
const POLICE_VISION_RANGE = 180;
const ATTACK_RANGE = 28;
const DAMAGE_ZOMBIE = 1;
const BULLET_DAMAGE = 2;
const BULLET_SPEED = 6;
const SHOOT_COOLDOWN = 500;

const MAG_SIZE = 8;
const RELOAD_TIME = 1500;
const POLICE_RETREAT_RANGE = 60;
const POLICE_RETREAT_SPEED = 0.8;
const GUN_LENGTH = 10;

const HELP_TEXT_DURATION = 1000;
const HELP_FADE_DURATION = 500;
const HELP_TRIGGER_RANGE = 160;
const HELP_RESPONSE_RANGE = 700;
const POLICE_MOVE_TO_HELP_SPEED = 2.5;

const HUMAN_NORMAL_SPEED = 1.6;
const HUMAN_TIRED_SPEED = 0.9;
const HUMAN_POLICE_BOOST_SPEED = 2.6;
const HUMAN_BOOST_VISION_RANGE = 200;

const FATIGUE_TRIGGER_TIME = 180;
const REST_RECOVER_TIME = 120;
const RADIUS = 9;
const COLLISION_RADIUS = 10;
const SELECT_RADIUS = 20;

// 警察熟练度概率参数
const BASE_HIT_CHANCE = 0.3;
const MAX_HIT_CHANCE = 0.85;
const CHANCE_PER_KILL = 0.07;

// 僵尸减速配置
const SLOW_DURATION = 800;
const SLOW_FACTOR = 0.45;

// 浮动伤害文字
const DAMAGE_TEXT_LIFETIME = 1000;

// 僵尸攻击冷却配置
const ZOMBIE_ATTACK_INTERVAL = 800;
const RUNNER_ATTACK_INTERVAL = 500;

// 支援警察 霰弹枪配置
const SHOTGUN_MAG_SIZE = 3;
const SHOTGUN_RELOAD_TIME = 1500;         // 【修复】1.5秒装3发,真实节奏
const SHOTGUN_SHOOT_COOLDOWN = 1000;
const SHOTGUN_PELLET_COUNT = 5;
const SHOTGUN_SPREAD_ANGLE = Math.PI / 3; // 60度散射
const SHOTGUN_MAX_RANGE = 150;            // 【新增】最大有效射程150像素

let gameSpeed = 1;

// ========== 浮动伤害文字类 ==========
class DamageText {
    constructor(x, y, text, isInvalid) {
        this.x = x;
        this.y = y;
        this.startY = y;
        this.text = text;
        this.isInvalid = isInvalid;
        this.born = performance.now();
        this.life = DAMAGE_TEXT_LIFETIME;
    }
    update() {
        const age = performance.now() - this.born;
        const progress = age / this.life;
        this.y = this.startY - progress * 35;
        return progress < 1;
    }
    draw() {
        const age = performance.now() - this.born;
        const alpha = 1 - age / this.life;
        const sx = this.x - camera.x;
        const sy = this.y - camera.y;
        ctx.save();
        ctx.globalAlpha = alpha;
        ctx.font = "bold 16px Arial";
        ctx.textAlign = "center";
        if(this.isInvalid){
            ctx.fillStyle = "#888888";
        }else{
            ctx.fillStyle = "#ff2222";
        }
        ctx.strokeStyle = "#ffffff";
        ctx.lineWidth = 3;
        ctx.strokeText(this.text, sx, sy);
        ctx.fillText(this.text, sx, sy);
        ctx.restore();
    }
}
const damageTexts = [];

// ========== 选中单位 & 视角跟随 ==========
let selectedUnit = null;
let cameraFollow = false;

document.addEventListener('keydown', (e) => {
    if(e.key === '1') gameSpeed = 1;
    if(e.key === '2') gameSpeed = 2;
    if(e.key === '3') gameSpeed = 3;
    if(e.key === '4') gameSpeed = 4;
    if(e.key === 'Escape') { selectedUnit = null; cameraFollow = false; }
});

// ========== 底部加速按钮 ==========
const speedButtons = [
    { label: '1×', speed: 1, x: 0, y: 0, w: 70, h: 52 },
    { label: '2×', speed: 2, x: 0, y: 0, w: 70, h: 52 },
    { label: '3×', speed: 3, x: 0, y: 0, w: 70, h: 52 },
    { label: '4×', speed: 4, x: 0, y: 0, w: 70, h: 52 },
];

function layoutSpeedButtons() {
    const totalW = speedButtons.length * speedButtons[0].w + (speedButtons.length - 1) * 10;
    const startX = (canvas.width - totalW) / 2;
    const btnY = canvas.height - 65;
    for(let i = 0; i < speedButtons.length; i++){
        speedButtons[i].x = startX + i * (speedButtons[i].w + 10);
        speedButtons[i].y = btnY;
    }
}
layoutSpeedButtons();

function getCanvasPos(clientX, clientY) {
    const rect = canvas.getBoundingClientRect();
    return {
        x: (clientX - rect.left) * (canvas.width / rect.width),
        y: (clientY - rect.top) * (canvas.height / rect.height)
    };
}

function isOnSpeedButton(pos) {
    for(const btn of speedButtons){
        if(pos.x >= btn.x && pos.x <= btn.x + btn.w &&
           pos.y >= btn.y && pos.y <= btn.y + btn.h){
            return btn;
        }
    }
    return null;
}

function findUnitAtScreenPos(sx, sy) {
    const wx = sx + camera.x;
    const wy = sy + camera.y;
    let nearest = null;
    let minDist = SELECT_RADIUS;
    for(const u of units){
        if(u.hp <= 0) continue;
        const d = Math.hypot(u.x - wx, u.y - wy);
        if(d < minDist){
            minDist = d;
            nearest = u;
        }
    }
    return nearest;
}

// ========== 相机系统 ==========
const camera = {
    x: WORLD_WIDTH / 2 - canvas.width / 2,
    y: WORLD_HEIGHT / 2 - canvas.height / 2,
    dragging: false,
    lastX: 0,
    lastY: 0,
    dragMoved: false,
    downX: 0,
    downY: 0
};

function handlePointerDown(clientX, clientY) {
    const pos = getCanvasPos(clientX, clientY);
    const btn = isOnSpeedButton(pos);
    if(btn){ gameSpeed = btn.speed; return; }

    camera.dragging = true;
    camera.dragMoved = false;
    camera.lastX = clientX;
    camera.lastY = clientY;
    camera.downX = pos.x;
    camera.downY = pos.y;
    cameraFollow = false;
}

function handlePointerMove(clientX, clientY) {
    if(camera.dragging){
        const dx = clientX - camera.lastX;
        const dy = clientY - camera.lastY;
        if(Math.abs(dx) > 2 || Math.abs(dy) > 2) camera.dragMoved = true;
        camera.x -= dx;
        camera.y -= dy;
        camera.lastX = clientX;
        camera.lastY = clientY;
    }
}

function handlePointerUp(clientX, clientY) {
    if(!camera.dragging) return;
    camera.dragging = false;

    if(!camera.dragMoved){
        const pos = getCanvasPos(clientX, clientY);
        const u = findUnitAtScreenPos(pos.x, pos.y);
        if(u){
            selectedUnit = u;
            cameraFollow = true;
        }else{
            selectedUnit = null;
            cameraFollow = false;
        }
    }
}

canvas.addEventListener('mousedown', (e) => handlePointerDown(e.clientX, e.clientY));
canvas.addEventListener('mousemove', (e) => handlePointerMove(e.clientX, e.clientY));
canvas.addEventListener('mouseup', (e) => handlePointerUp(e.clientX, e.clientY));
canvas.addEventListener('mouseleave', () => { camera.dragging = false; });

canvas.addEventListener('touchstart', (e) => {
    e.preventDefault();
    const t = e.touches[0];
    handlePointerDown(t.clientX, t.clientY);
}, {passive:false});
canvas.addEventListener('touchmove', (e) => {
    e.preventDefault();
    const t = e.touches[0];
    handlePointerMove(t.clientX, t.clientY);
}, {passive:false});
canvas.addEventListener('touchend', (e) => {
    e.preventDefault();
    const t = e.changedTouches[0];
    handlePointerUp(t.clientX, t.clientY);
}, {passive:false});

// ========== 子弹类 ==========
class Bullet {
    constructor(x, y, tx, ty, shooter, maxRange) {
        this.x = x;
        this.y = y;
        this.startX = x; // 记录出生点X(用于射程判定)
        this.startY = y; // 记录出生点Y
        this.maxRange = (maxRange !== undefined) ? maxRange : 99999; // 默认无限远,霰弹枪会传入150

        const dx = tx - x;
        const dy = ty - y;
        const dist = Math.hypot(dx, dy);
        if(dist < 0.001){
            this.vx = 0;
            this.vy = 0;
        }else{
            this.vx = (dx/dist)*BULLET_SPEED;
            this.vy = (dy/dist)*BULLET_SPEED;
        }
        this.alive = true;
        this.radius = 3;
        this.shooter = shooter;
    }
    update(){
        this.x += this.vx;
        this.y += this.vy;

        // 【新增】射程限制:超出最大飞行距离直接消失
        const traveled = Math.hypot(this.x - this.startX, this.y - this.startY);
        if (traveled > this.maxRange) {
            this.alive = false;
        }

        if(this.x < -100 || this.x > WORLD_WIDTH + 100 || this.y < -100 || this.y > WORLD_HEIGHT + 100){
            this.alive = false;
        }
    }
    draw(){
        ctx.beginPath();
        ctx.arc(this.x - camera.x, this.y - camera.y, this.radius, 0, Math.PI * 2);
        ctx.fillStyle = "#222222";
        ctx.fill();
    }
}

// ========== 单位类 ==========
class Unit {
    constructor(x, y, type) {
        this.x = x;
        this.y = y;
        this.hp = MAX_HP;
        this.type = type;
        this.target = null;

        this.wanderVx = (Math.random() - 0.5) * 0.8;
        this.wanderVy = (Math.random() - 0.5) * 0.8;
        this.wanderTimer = 0;
        this.wanderInterval = 60 + Math.random() * 120;

        this.runningTimer = 0;
        this.isTired = false;
        this.seePolice = false;

        this.lastShootTime = 0;
        this.aimAngle = 0;
        this.bulletsInMag = (type === "swat") ? SHOTGUN_MAG_SIZE : MAG_SIZE;
        this.isReloading = false;
        this.reloadStart = 0;
        this.isCryingForHelp = false;
        this.helpStartTime = -9999;

        this.killCount = 0;
        this.slowUntil = 0;
        this.lastAttackTime = 0;
    }

    distanceTo(other) {
        const dx = this.x - other.x;
        const dy = this.y - other.y;
        return Math.sqrt(dx * dx + dy * dy);
    }

    getMuzzlePos() {
        return {
            x: this.x + Math.cos(this.aimAngle) * (RADIUS + (this.type === "swat" ? 24 : GUN_LENGTH)),
            y: this.y + Math.sin(this.aimAngle) * (RADIUS + (this.type === "swat" ? 24 : GUN_LENGTH))
        };
    }

    isZombieType() {
        return this.type === "zombie" || this.type === "runner";
    }

    getZombieSpeed() {
        let base = this.type === "runner" ? RUNNER_SPEED : ZOMBIE_SPEED;
        if(performance.now() < this.slowUntil){
            base *= SLOW_FACTOR;
        }
        return base;
    }
    getZombieDetectRange() {
        return this.type === "runner" ? RUNNER_DETECT_RANGE : ZOMBIE_DETECT_RANGE;
    }

    getTypeName() {
        switch(this.type){
            case "zombie": return "僵尸";
            case "runner": return "跑尸";
            case "civilian": return "普通人";
            case "police": return "警察";
            case "swat": return "支援警察";
            default: return "未知";
        }
    }

    getEffectiveDamageChance(){
        let p = BASE_HIT_CHANCE + this.killCount * CHANCE_PER_KILL;
        return Math.min(p, MAX_HIT_CHANCE);
    }

    getCurrentSpeed() {
        if(this.type === "zombie" || this.type === "runner"){
            return this.target ? this.getZombieSpeed() : 0.4 + Math.random()*0.4;
        } else if(this.type === "civilian"){
            if(this.isTired) return HUMAN_TIRED_SPEED;
            if(this.seePolice) return HUMAN_POLICE_BOOST_SPEED;
            return HUMAN_NORMAL_SPEED;
        } else if(this.type === "police" || this.type === "swat"){
            return POLICE_MOVE_TO_HELP_SPEED;
        }
        return 0;
    }

    findNearestZombie(allUnits, range) {
        let nearest = null;
        let minDist = range;
        for (const u of allUnits) {
            if (u.isZombieType() && u.hp > 0) {
                const d = this.distanceTo(u);
                if (d < minDist) {
                    minDist = d;
                    nearest = u;
                }
            }
        }
        return nearest;
    }

    findHumanTarget(allUnits) {
        let nearest = null;
        let minDist = this.getZombieDetectRange();
        for (const u of allUnits) {
            if (!u.isZombieType() && u.hp > 0) {
                const d = this.distanceTo(u);
                if (d < minDist) {
                    minDist = d;
                    nearest = u;
                }
            }
        }
        this.target = nearest;
    }

    findNearestPolice(allUnits, range) {
        let nearest = null;
        let minDist = range;
        for (const u of allUnits) {
            if ((u.type === "police" || u.type === "swat") && u.hp > 0) {
                const d = this.distanceTo(u);
                if (d < minDist) {
                    minDist = d;
                    nearest = u;
                }
            }
        }
        return nearest;
    }

    findHelpingPolice(allUnits) {
        let nearest = null;
        let minDist = HELP_RESPONSE_RANGE;
        for (const u of allUnits) {
            if (u === this) continue;
            if ((u.type === "police" || u.type === "swat") && u.hp > 0 && u.isCryingForHelp) {
                const d = this.distanceTo(u);
                if (d < minDist) {
                    minDist = d;
                    nearest = u;
                }
            }
        }
        return nearest;
    }

    tryShoot(target, bulletList, currentTime) {
        if (this.isReloading) return false;
        if (this.bulletsInMag <= 0) return false;
        if (currentTime - this.lastShootTime < SHOOT_COOLDOWN) return false;

        const muzzle = this.getMuzzlePos();
        // 普通手枪:无限射程
        bulletList.push(new Bullet(muzzle.x, muzzle.y, target.x, target.y, this));
        this.bulletsInMag--;
        this.lastShootTime = currentTime;

        if (this.bulletsInMag <= 0) {
            this.isReloading = true;
            this.reloadStart = currentTime;
        }
        return true;
    }

    tryFireShotgun(target, bulletList, currentTime) {
        if (this.isReloading) return false;
        if (this.bulletsInMag <= 0) return false;
        if (currentTime - this.lastShootTime < SHOTGUN_SHOOT_COOLDOWN) return false;

        const muzzle = this.getMuzzlePos();
        // 生成 5 发霰弹丸,每发都带150像素射程限制
        for (let i = 0; i < SHOTGUN_PELLET_COUNT; i++) {
            let relativeAngle = -SHOTGUN_SPREAD_ANGLE / 2 + i * (SHOTGUN_SPREAD_ANGLE / (SHOTGUN_PELLET_COUNT - 1));
            let angle = this.aimAngle + relativeAngle;
            let vx = BULLET_SPEED * Math.cos(angle);
            let vy = BULLET_SPEED * Math.sin(angle);
            // 传入 SHOTGUN_MAX_RANGE 参数
            let pellet = new Bullet(muzzle.x, muzzle.y, muzzle.x + 100, muzzle.y + 100, this, SHOTGUN_MAX_RANGE);
            pellet.vx = vx;
            pellet.vy = vy;
            pellet.radius = 2;
            bulletList.push(pellet);
        }
        this.bulletsInMag--;
        this.lastShootTime = currentTime;

        if (this.bulletsInMag <= 0) {
            this.isReloading = true;
            this.reloadStart = currentTime;
        }
        return true;
    }

    update(allUnits, bulletList, currentTime) {
        if (this.hp <= 0) {
            if (!this.isZombieType()) {
                this.type = "zombie";
                this.hp = MAX_HP;
                this.runningTimer = 0;
                this.isTired = false;
                this.target = null;
                this.isCryingForHelp = false;
                this.seePolice = false;
                this.bulletsInMag = MAG_SIZE;
                this.isReloading = false;
                this.helpStartTime = -9999;
                this.slowUntil = 0;
                this.lastAttackTime = 0;
            }
            return;
        }

        if (this.isZombieType()) {
            this.findHumanTarget(allUnits);
            if (this.target) {
                const dx = this.target.x - this.x;
                const dy = this.target.y - this.y;
                const dist = Math.hypot(dx, dy);
                if (dist > ATTACK_RANGE) {
                    const spd = this.getZombieSpeed();
                    this.x += (dx / dist) * spd;
                    this.y += (dy / dist) * spd;
                } else {
                    const attackInterval = this.type === "runner" ? RUNNER_ATTACK_INTERVAL : ZOMBIE_ATTACK_INTERVAL;
                    if (currentTime - this.lastAttackTime >= attackInterval) {
                        this.target.hp -= DAMAGE_ZOMBIE;
                        damageTexts.push(new DamageText(this.target.x, this.target.y - RADIUS, `-${DAMAGE_ZOMBIE}`, false));
                        this.lastAttackTime = currentTime;
                    }
                }
            } else {
                this.wanderTimer++;
                if (this.wanderTimer > this.wanderInterval) {
                    this.wanderVx = (Math.random() - 0.5) * 0.8;
                    this.wanderVy = (Math.random() - 0.5) * 0.8;
                    this.wanderTimer = 0;
                    this.wanderInterval = 60 + Math.random() * 120;
                }
                this.x += this.wanderVx;
                this.y += this.wanderVy;
            }
        }
        else if (this.type === "civilian") {
            const dangerZombie = this.findNearestZombie(allUnits, HUMAN_VISION_RANGE);
            if (dangerZombie) {
                const nearbyPolice = this.findNearestPolice(allUnits, HUMAN_BOOST_VISION_RANGE);
                this.seePolice = !!nearbyPolice;

                this.runningTimer++;
                if (this.runningTimer >= FATIGUE_TRIGGER_TIME) {
                    this.isTired = true;
                }

                let speed = this.isTired ? HUMAN_TIRED_SPEED :
                            this.seePolice ? HUMAN_POLICE_BOOST_SPEED : HUMAN_NORMAL_SPEED;

                const dx = this.x - dangerZombie.x;
                const dy = this.y - dangerZombie.y;
                const dist = Math.hypot(dx, dy);
                this.x += (dx / dist) * speed;
                this.y += (dy / dist) * speed;
            } else {
                this.seePolice = false;
                this.runningTimer = Math.max(0, this.runningTimer - 1);
                if (this.runningTimer === 0) {
                    this.isTired = false;
                }
                this.wanderTimer++;
                if (this.wanderTimer > this.wanderInterval) {
                    this.wanderVx = (Math.random() - 0.5) * 0.7;
                    this.wanderVy = (Math.random() - 0.5) * 0.7;
                    this.wanderTimer = 0;
                    this.wanderInterval = 70 + Math.random() * 130;
                }
                this.x += this.wanderVx;
                this.y += this.wanderVy;
            }
        }
        else if (this.type === "police") {
            const targetZombie = this.findNearestZombie(allUnits, POLICE_VISION_RANGE);

            if (this.isReloading) {
                if (currentTime - this.reloadStart >= RELOAD_TIME) {
                    this.bulletsInMag = MAG_SIZE;
                    this.isReloading = false;
                    this.lastShootTime = currentTime;
                }
            }

            if (this.isReloading && targetZombie) {
                const distToZombie = this.distanceTo(targetZombie);
                if (distToZombie < HELP_TRIGGER_RANGE) {
                    this.isCryingForHelp = true;
                    this.helpStartTime = currentTime;
                }
            }
            if (this.isCryingForHelp) {
                if (currentTime - this.helpStartTime > HELP_TEXT_DURATION + HELP_FADE_DURATION) {
                    this.isCryingForHelp = false;
                }
            }

            const helpingPolice = this.findHelpingPolice(allUnits);
            if (helpingPolice && !this.isReloading) {
                const dx = helpingPolice.x - this.x;
                const dy = helpingPolice.y - this.y;
                const dist = Math.hypot(dx, dy);
                if (dist > 40) {
                    this.x += (dx / dist) * POLICE_MOVE_TO_HELP_SPEED;
                    this.y += (dy / dist) * POLICE_MOVE_TO_HELP_SPEED;
                }
                if (targetZombie) {
                    this.aimAngle = Math.atan2(targetZombie.y - this.y, targetZombie.x - this.x);
                    this.tryShoot(targetZombie, bulletList, currentTime);
                }
            }
            else if (targetZombie) {
                this.aimAngle = Math.atan2(targetZombie.y - this.y, targetZombie.x - this.x);

                const distToZombie = this.distanceTo(targetZombie);
                if (distToZombie < POLICE_RETREAT_RANGE) {
                    const dx = this.x - targetZombie.x;
                    const dy = this.y - targetZombie.y;
                    const dist = Math.hypot(dx, dy);
                    this.x += (dx/dist)*POLICE_RETREAT_SPEED;
                    this.y += (dy/dist)*POLICE_RETREAT_SPEED;
                }

                this.tryShoot(targetZombie, bulletList, currentTime);
            }
            else {
                this.wanderTimer++;
                if (this.wanderTimer > this.wanderInterval) {
                    this.wanderVx = (Math.random() - 0.5) * 0.6;
                    this.wanderVy = (Math.random() - 0.5) * 0.6;
                    this.wanderTimer = 0;
                    this.wanderInterval = 80 + Math.random() * 140;
                    this.aimAngle = Math.random()*Math.PI*2;
                }
                this.x += this.wanderVx;
                this.y += this.wanderVy;
            }
        }
        else if (this.type === "swat") {
            const targetZombie = this.findNearestZombie(allUnits, POLICE_VISION_RANGE);

            if (this.isReloading) {
                if (currentTime - this.reloadStart >= SHOTGUN_RELOAD_TIME) {
                    this.bulletsInMag = SHOTGUN_MAG_SIZE;
                    this.isReloading = false;
                    this.lastShootTime = currentTime;
                }
            }

            if (this.isReloading && targetZombie) {
                const distToZombie = this.distanceTo(targetZombie);
                if (distToZombie < HELP_TRIGGER_RANGE) {
                    this.isCryingForHelp = true;
                    this.helpStartTime = currentTime;
                }
            }
            if (this.isCryingForHelp) {
                if (currentTime - this.helpStartTime > HELP_TEXT_DURATION + HELP_FADE_DURATION) {
                    this.isCryingForHelp = false;
                }
            }

            const helpingPolice = this.findHelpingPolice(allUnits);
            if (helpingPolice && !this.isReloading) {
                const dx = helpingPolice.x - this.x;
                const dy = helpingPolice.y - this.y;
                const dist = Math.hypot(dx, dy);
                if (dist > 40) {
                    this.x += (dx / dist) * POLICE_MOVE_TO_HELP_SPEED;
                    this.y += (dy / dist) * POLICE_MOVE_TO_HELP_SPEED;
                }
                if (targetZombie) {
                    this.aimAngle = Math.atan2(targetZombie.y - this.y, targetZombie.x - this.x);
                    this.tryFireShotgun(targetZombie, bulletList, currentTime);
                }
            }
            else if (targetZombie) {
                this.aimAngle = Math.atan2(targetZombie.y - this.y, targetZombie.x - this.x);

                const distToZombie = this.distanceTo(targetZombie);
                if (distToZombie < 20) {
                    const dx = this.x - targetZombie.x;
                    const dy = this.y - targetZombie.y;
                    const dist = Math.hypot(dx, dy);
                    this.x += (dx/dist)*POLICE_RETREAT_SPEED;
                    this.y += (dy/dist)*POLICE_RETREAT_SPEED;
                }

                this.tryFireShotgun(targetZombie, bulletList, currentTime);
            }
            else {
                this.wanderTimer++;
                if (this.wanderTimer > this.wanderInterval) {
                    this.wanderVx = (Math.random() - 0.5) * 0.6;
                    this.wanderVy = (Math.random() - 0.5) * 0.6;
                    this.wanderTimer = 0;
                    this.wanderInterval = 80 + Math.random() * 140;
                    this.aimAngle = Math.random()*Math.PI*2;
                }
                this.x += this.wanderVx;
                this.y += this.wanderVy;
            }
        }

        const edgeBuffer = 15;
        this.x = Math.max(RADIUS + edgeBuffer, Math.min(WORLD_WIDTH - RADIUS - edgeBuffer, this.x));
        this.y = Math.max(RADIUS + edgeBuffer, Math.min(WORLD_HEIGHT - RADIUS - edgeBuffer, this.y));
    }

    draw() {
        if (this.hp <= 0) return;

        const sx = this.x - camera.x;
        const sy = this.y - camera.y;

        if (this.type === "zombie") {
            ctx.beginPath();
            ctx.arc(sx, sy, ZOMBIE_DETECT_RANGE, 0, Math.PI * 2);
            ctx.strokeStyle = "rgba(40, 220, 40, 0.3)";
            ctx.lineWidth = 1;
            ctx.stroke();
        } else if (this.type === "runner") {
            ctx.beginPath();
            ctx.arc(sx, sy, RUNNER_DETECT_RANGE, 0, Math.PI * 2);
            ctx.strokeStyle = "rgba(220, 40, 40, 0.3)";
            ctx.lineWidth = 1;
            ctx.stroke();
        } else if (this.type === "civilian") {
            if (this.seePolice && !this.isTired) {
                ctx.beginPath();
                ctx.arc(sx, sy, HUMAN_BOOST_VISION_RANGE, 0, Math.PI * 2);
                ctx.strokeStyle = "rgba(255, 140, 0, 0.45)";
                ctx.lineWidth = 2;
                ctx.setLineDash([6, 4]);
                ctx.stroke();
                ctx.setLineDash([]);
            }
            ctx.beginPath();
            ctx.arc(sx, sy, HUMAN_VISION_RANGE, 0, Math.PI * 2);
            ctx.strokeStyle = "rgba(80, 160, 255, 0.3)";
            ctx.lineWidth = 1;
            ctx.stroke();
        } else if (this.type === "police" || this.type === "swat") {
            ctx.beginPath();
            ctx.arc(sx, sy, POLICE_VISION_RANGE, 0, Math.PI * 2);
            ctx.strokeStyle = "rgba(60, 220, 255, 0.35)";
            ctx.lineWidth = 1;
            ctx.stroke();
        }

        if (selectedUnit === this) {
            ctx.beginPath();
            ctx.arc(sx, sy, RADIUS + 8, 0, Math.PI * 2);
            ctx.strokeStyle = "#ffaa00";
            ctx.lineWidth = 3;
            ctx.stroke();

            if (this.type === "police") {
                ctx.beginPath();
                ctx.moveTo(sx, sy);
                ctx.lineTo(sx + Math.cos(this.aimAngle) * 35, sy + Math.sin(this.aimAngle) * 35);
                ctx.strokeStyle = "rgba(255, 170, 0, 0.7)";
                ctx.lineWidth = 2;
                ctx.stroke();
            }
        }

        ctx.beginPath();
        ctx.arc(sx, sy, RADIUS, 0, Math.PI * 2);
        if (this.type === "zombie") {
            ctx.fillStyle = "#00c020";
        } else if (this.type === "runner") {
            ctx.fillStyle = RUNNER_COLOR;
        } else if (this.type === "civilian") {
            if (this.isTired) ctx.fillStyle = "#ccb880";
            else if (this.seePolice) ctx.fillStyle = "#ffcc44";
            else ctx.fillStyle = "#f7e4a5";
        } else if (this.type === "police") {
            ctx.fillStyle = "#0088ff";
        } else if (this.type === "swat") {
            ctx.fillStyle = "#3a86ff";
        }
        ctx.fill();
        ctx.strokeStyle = "#222222";
        ctx.lineWidth = 1;
        ctx.stroke();

        if (this.type === "runner") {
            ctx.strokeStyle = "#ff6060";
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.moveTo(sx - 6, sy - 3);
            ctx.lineTo(sx - 12, sy - 3);
            ctx.moveTo(sx - 6, sy + 3);
            ctx.lineTo(sx - 12, sy + 3);
            ctx.stroke();
        }

        if (this.type === "civilian" && this.seePolice && !this.isTired) {
            ctx.font = "bold 14px Arial";
            ctx.textAlign = "center";
            ctx.fillStyle = "#ff6600";
            ctx.fillText("⚡", sx, sy - RADIUS - 6);
        }

        if (this.type === "police") {
            ctx.save();
            ctx.translate(sx, sy);
            ctx.rotate(this.aimAngle);
            ctx.fillStyle = "#000000";
            ctx.fillRect(RADIUS, -3, GUN_LENGTH, 6);
            ctx.restore();

            if(this.isCryingForHelp){
                ctx.font = "bold 13px Arial";
                ctx.textAlign = "center";
                ctx.fillStyle="#ff2222";
                ctx.strokeStyle="#fff";
                ctx.lineWidth=2;
                ctx.strokeText("I need help!", sx, sy - RADIUS - 12);
                ctx.fillText("I need help!", sx, sy - RADIUS - 12);
            }
        } else if (this.type === "swat") {
            ctx.save();
            ctx.translate(sx, sy);
            ctx.rotate(this.aimAngle);

            // 木制握把(棕色,在黑色下方)
            ctx.fillStyle = "#D2691E";
            ctx.fillRect(RADIUS + 3, -5, 10, 20);
            ctx.strokeStyle = "#8B4513";
            ctx.lineWidth = 1;
            ctx.strokeRect(RADIUS + 3, -5, 10, 20);

            // 枪管(黑色,与棕色重叠)
            ctx.fillStyle = "#222222";
            ctx.fillRect(RADIUS, -6, 24, 12);
            ctx.strokeStyle = "#000";
            ctx.lineWidth = 1;
            ctx.strokeRect(RADIUS, -6, 24, 12);

            ctx.restore();

            if(this.isCryingForHelp){
                ctx.font = "bold 13px Arial";
                ctx.textAlign = "center";
                ctx.fillStyle="#ff2222";
                ctx.strokeStyle="#fff";
                ctx.lineWidth=2;
                ctx.strokeText("I need help!", sx, sy - RADIUS - 12);
                ctx.fillText("I need help!", sx, sy - RADIUS - 12);
            }
        }

        ctx.font = "bold 11px Arial";
        ctx.textAlign = "center";
        ctx.fillStyle = "#fff";
        ctx.strokeStyle = "#000";
        ctx.lineWidth = 2;
        ctx.strokeText(String(this.hp), sx, sy + 4);
        ctx.fillText(String(this.hp), sx, sy + 4);

        if (this.type === "police") {
            ctx.font = "11px Arial";
            ctx.fillStyle = "#fff";
            ctx.strokeStyle = "#000";
            ctx.lineWidth = 2;
            ctx.strokeText(`${this.bulletsInMag}/${MAG_SIZE}`, sx, sy - RADIUS - 6);
            ctx.fillText(`${this.bulletsInMag}/${MAG_SIZE}`, sx, sy - RADIUS - 6);

            if (this.isReloading && !this.isCryingForHelp) {
                ctx.fillStyle = "#ff4444";
                ctx.font = "bold 10px Arial";
                ctx.strokeText("换弹", sx, sy - RADIUS - 18);
                ctx.fillText("换弹", sx, sy - RADIUS - 18);
            }
        } else if (this.type === "swat") {
            ctx.font = "11px Arial";
            ctx.fillStyle = "#fff";
            ctx.strokeStyle = "#000";
            ctx.lineWidth = 2;
            ctx.strokeText(`${this.bulletsInMag}/${SHOTGUN_MAG_SIZE}`, sx, sy - RADIUS - 6);
            ctx.fillText(`${this.bulletsInMag}/${SHOTGUN_MAG_SIZE}`, sx, sy - RADIUS - 6);

            if (this.isReloading && !this.isCryingForHelp) {
                ctx.fillStyle = "#ff4444";
                ctx.font = "bold 10px Arial";
                ctx.strokeText("装填霰弹...", sx, sy - RADIUS - 18);
                ctx.fillText("装填霰弹...", sx, sy - RADIUS - 18);
            }
        }
    }
}

// ========== 碰撞处理 ==========
function resolveCollisions(units) {
    for (let i = 0; i < units.length; i++) {
        if (units[i].hp <= 0) continue;
        for (let j = i + 1; j < units.length; j++) {
            if (units[j].hp <= 0) continue;
            const dx = units[j].x - units[i].x;
            const dy = units[j].y - units[i].y;
            const dist = Math.hypot(dx, dy);
            const minDist = COLLISION_RADIUS * 2;
            if (dist < minDist && dist > 0) {
                const overlap = (minDist - dist) / 2;
                const nx = dx / dist;
                const ny = dy / dist;
                units[i].x -= nx * overlap;
                units[i].y -= ny * overlap;
                units[j].x += nx * overlap;
                units[j].y += ny * overlap;
            }
        }
    }
    for (const u of units) {
        const edgeBuffer = 15;
        u.x = Math.max(RADIUS + edgeBuffer, Math.min(WORLD_WIDTH - RADIUS - edgeBuffer, u.x));
        u.y = Math.max(RADIUS + edgeBuffer, Math.min(WORLD_HEIGHT - RADIUS - edgeBuffer, u.y));
    }
}

// ========== 创建实体 ==========
const units = [];
const bullets = [];

for (let i = 0; i < 18; i++) {
    const x = Math.random() * (WORLD_WIDTH - 200) + 100;
    const y = Math.random() * (WORLD_HEIGHT - 200) + 100;
    units.push(new Unit(x, y, "zombie"));
}
for (let i = 0; i < 6; i++) {
    const x = Math.random() * (WORLD_WIDTH - 200) + 100;
    const y = Math.random() * (WORLD_HEIGHT - 200) + 100;
    units.push(new Unit(x, y, "runner"));
}
for (let i = 0; i < 12; i++) {
    const x = Math.random() * (WORLD_WIDTH - 200) + 100;
    const y = Math.random() * (WORLD_HEIGHT - 200) + 100;
    units.push(new Unit(x, y, "civilian"));
}
for (let i = 0; i < 5; i++) {
    const x = Math.random() * (WORLD_WIDTH - 200) + 100;
    const y = Math.random() * (WORLD_HEIGHT - 200) + 100;
    units.push(new Unit(x, y, "police"));
}
for (let i = 0; i < 3; i++) {
    const x = Math.random() * (WORLD_WIDTH - 200) + 100;
    const y = Math.random() * (WORLD_HEIGHT - 200) + 100;
    units.push(new Unit(x, y, "swat"));
}

// ========== 游戏主循环 ==========
let gameTime = 0;
let lastFrameTime = performance.now();

function drawRoundRect(x, y, w, h, r) {
    ctx.beginPath();
    ctx.moveTo(x + r, y);
    ctx.lineTo(x + w - r, y);
    ctx.quadraticCurveTo(x + w, y, x + w, y + r);
    ctx.lineTo(x + w, y + h - r);
    ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
    ctx.lineTo(x + r, y + h);
    ctx.quadraticCurveTo(x, y + h, x, y + h - r);
    ctx.lineTo(x, y + r);
    ctx.quadraticCurveTo(x, y, x + r, y);
    ctx.closePath();
}

function gameLoop() {
    const now = performance.now();
    const realDelta = now - lastFrameTime;
    lastFrameTime = now;

    const gameDelta = realDelta * gameSpeed;
    gameTime += gameDelta;

    const steps = Math.max(1, Math.floor(gameSpeed));
    const stepDelta = gameDelta / steps;

    for (let s = 0; s < steps; s++) {
        const stepTime = gameTime - gameDelta + stepDelta * (s + 1);
        units.forEach(u => u.update(units, bullets, stepTime));
        resolveCollisions(units);

        bullets.forEach(b => b.update());

        for (const bullet of bullets) {
            if (!bullet.alive) continue;
            for (const u of units) {
                if (u.isZombieType() && u.hp > 0) {
                    const dist = Math.hypot(bullet.x - u.x, bullet.y - u.y);
                    if (dist < RADIUS + bullet.radius) {
                        bullet.alive = false;
                        u.slowUntil = now + SLOW_DURATION;

                        const shooter = bullet.shooter;
                        if (shooter && shooter.hp > 0) {
                            if (shooter.type === "police") {
                                const chance = shooter.getEffectiveDamageChance();
                                if (Math.random() < chance) {
                                    u.hp -= BULLET_DAMAGE;
                                    damageTexts.push(new DamageText(u.x, u.y - RADIUS, `-${BULLET_DAMAGE}`, false));
                                    if (u.hp <= 0) {
                                        shooter.killCount += 1;
                                    }
                                } else {
                                    damageTexts.push(new DamageText(u.x, u.y - RADIUS, "伤害无效", true));
                                }
                            } else if (shooter.type === "swat") {
                                u.hp -= BULLET_DAMAGE;
                                damageTexts.push(new DamageText(u.x, u.y - RADIUS, `-${BULLET_DAMAGE}`, false));
                                if (u.hp <= 0) {
                                    shooter.killCount += 1;
                                }
                            }
                        }
                        break;
                    }
                }
            }
        }
        bullets.splice(0, bullets.length, ...bullets.filter(b => b.alive));

        damageTexts.splice(0, damageTexts.length, ...damageTexts.filter(dt => dt.update()));
    }

    if (cameraFollow && selectedUnit && selectedUnit.hp > 0) {
        const targetX = selectedUnit.x - canvas.width / 2;
        const targetY = selectedUnit.y - canvas.height / 2;
        camera.x += (targetX - camera.x) * 0.1;
        camera.y += (targetY - camera.y) * 0.1;
    }
    if (selectedUnit && selectedUnit.hp <= 0) {
        selectedUnit = null;
        cameraFollow = false;
    }

    ctx.clearRect(0, 0, canvas.width, canvas.height);

    const gridSize = 50;
    ctx.strokeStyle = "#eee";
    ctx.lineWidth = 1;
    const startX = Math.floor(camera.x / gridSize) * gridSize - camera.x;
    const startY = Math.floor(camera.y / gridSize) * gridSize - camera.y;
    for (let x = startX; x < canvas.width; x += gridSize) {
        ctx.beginPath();
        ctx.moveTo(x, 0);
        ctx.lineTo(x, canvas.height);
        ctx.stroke();
    }
    for (let y = startY; y < canvas.height; y += gridSize) {
        ctx.beginPath();
        ctx.moveTo(0, y);
        ctx.lineTo(canvas.width, y);
        ctx.stroke();
    }

    ctx.strokeStyle = "#999";
    ctx.lineWidth = 3;
    ctx.strokeRect(-camera.x, -camera.y, WORLD_WIDTH, WORLD_HEIGHT);

    bullets.forEach(b => { if (b.alive) b.draw(); });
    units.forEach(u => u.draw());
    damageTexts.forEach(dt => dt.draw());

    let aliveCivilian = 0, alivePolice = 0, aliveZombie = 0, aliveRunner = 0, aliveSwat = 0;
    for (const u of units) {
        if (u.hp > 0) {
            if (u.type === "zombie") aliveZombie++;
            else if (u.type === "runner") aliveRunner++;
            else if (u.type === "civilian") aliveCivilian++;
            else if (u.type === "police") alivePolice++;
            else if (u.type === "swat") aliveSwat++;
        }
    }

    drawRoundRect(10, 10, 210, 110, 8);
    ctx.fillStyle = "rgba(255,255,255,0.92)";
    ctx.fill();
    ctx.strokeStyle = "#aaa";
    ctx.lineWidth = 1;
    ctx.stroke();

    ctx.font = "bold 15px Arial";
    ctx.textAlign = "left";
    ctx.fillStyle = "#333";
    ctx.fillText(`👤 普通人:${aliveCivilian}`, 20, 33);
    ctx.fillText(`👮 警察:${alivePolice}`, 20, 56);
    ctx.fillStyle = "#3a86ff";
    ctx.fillText(`🔫 支援警察:${aliveSwat}`, 20, 79);
    ctx.fillStyle = "#00a020";
    ctx.fillText(`🧟 僵尸:${aliveZombie}`, 100, 102);
    ctx.fillStyle = "#c02020";
    ctx.fillText(`🏃 跑尸:${aliveRunner}`, 170, 102);

    if (selectedUnit && selectedUnit.hp > 0) {
        const panelW = 240;
        const panelH = 240;
        const px = canvas.width - panelW - 12;
        const py = 12;

        drawRoundRect(px, py, panelW, panelH, 10);
        ctx.fillStyle = "rgba(255,255,255,0.95)";
        ctx.fill();
        ctx.strokeStyle = "#ffaa00";
        ctx.lineWidth = 2;
        ctx.stroke();

        ctx.fillStyle = "#ffaa00";
        ctx.font = "bold 16px Arial";
        ctx.textAlign = "left";
        ctx.fillText(`📌 ${selectedUnit.getTypeName()}`, px + 14, py + 26);

        ctx.font = "11px Arial";
        ctx.fillStyle = cameraFollow ? "#00a020" : "#888";
        ctx.textAlign = "right";
        ctx.fillText(cameraFollow ? "● 跟随中" : "○ 未跟随", px + panelW - 14, py + 24);

        ctx.beginPath();
        ctx.moveTo(px + 12, py + 36);
        ctx.lineTo(px + panelW - 12, py + 36);
        ctx.strokeStyle = "#ddd";
        ctx.lineWidth = 1;
        ctx.stroke();

        ctx.font = "13px Arial";
        ctx.textAlign = "left";
        ctx.fillStyle = "#333";
        let ly = py + 54;

        ctx.fillStyle = "#eee";
        ctx.fillRect(px + 14, ly - 10, panelW - 28, 8);
        const hpRatio = selectedUnit.hp / MAX_HP;
        ctx.fillStyle = hpRatio > 0.5 ? "#00c040" : hpRatio > 0.25 ? "#ffaa00" : "#ff4444";
        ctx.fillRect(px + 14, ly - 10, (panelW - 28) * hpRatio, 8);
        ctx.strokeStyle = "#999";
        ctx.lineWidth = 1;
        ctx.strokeRect(px + 14, ly - 10, panelW - 28, 8);

        ctx.fillStyle = "#333";
        ctx.fillText(`❤️ 生命值:${selectedUnit.hp} / ${MAX_HP}`, px + 14, ly + 10);
        ly += 26;

        ctx.fillText(`📍 位置:(${Math.round(selectedUnit.x)}, ${Math.round(selectedUnit.y)})`, px + 14, ly);
        ly += 22;

        ctx.fillText(`🏃 速度:${selectedUnit.getCurrentSpeed().toFixed(1)}`, px + 14, ly);
        ly += 22;

        if (selectedUnit.isZombieType()) {
            const targetName = selectedUnit.target ? selectedUnit.target.getTypeName() : "无";
            ctx.fillText(`🎯 目标:${targetName}`, px + 14, ly);
            ly += 22;
            ctx.fillText(`👁 探测范围:${selectedUnit.getZombieDetectRange()}`, px + 14, ly);
            ly += 22;
            if(now < selectedUnit.slowUntil){
                ctx.fillStyle="#4488ff";
                ctx.fillText(`❄️ 当前处于减速状态`, px + 14, ly);
                ly +=22;
            }
        } else if (selectedUnit.type === "civilian") {
            const state = selectedUnit.isTired ? "😩 疲劳" :
                         selectedUnit.seePolice ? "⚡ 加速逃跑" : "🚶 正常";
            ctx.fillText(`📊 状态:${state}`, px + 14, ly);
            ly += 22;
            const danger = selectedUnit.findNearestZombie(units, 9999);
            const distStr = danger ? Math.round(selectedUnit.distanceTo(danger)) + "px" : "无";
            ctx.fillText(`⚠️ 最近僵尸:${distStr}`, px + 14, ly);
            ly += 22;
        } else if (selectedUnit.type === "police") {
            ctx.fillText(`🔫 弹药:${selectedUnit.bulletsInMag} / ${MAG_SIZE}`, px + 14, ly);
            ly += 22;
            const state = selectedUnit.isReloading ? "🔄 换弹中" :
                         selectedUnit.isCryingForHelp ? "🆘 求救中" : "✅ 就绪";
            ctx.fillText(`📊 状态:${state}`, px + 14, ly);
            ly += 22;
            ctx.fillText(`💀 击杀僵尸:${selectedUnit.killCount}`, px + 14, ly);
            ly +=22;
            const hitPct = (selectedUnit.getEffectiveDamageChance() *100).toFixed(0);
            ctx.fillText(`🎯 有效伤害概率:${hitPct}%`, px +14, ly);
            ly +=22;
            const target = selectedUnit.findNearestZombie(units, POLICE_VISION_RANGE);
            const tName = target ? target.getTypeName() : "无";
            ctx.fillText(`🎯 目标:${tName}`, px + 14, ly);
            ly += 22;
        } else if (selectedUnit.type === "swat") {
            ctx.fillText(`🔫 弹药:${selectedUnit.bulletsInMag} / ${SHOTGUN_MAG_SIZE} (霰弹)`, px + 14, ly);
            ly += 22;
            const state = selectedUnit.isReloading ? "🔄 装填中(1.5秒)" :
                         selectedUnit.isCryingForHelp ? "🆘 求救中" : "✅ 就绪";
            ctx.fillText(`📊 状态:${state}`, px + 14, ly);
            ly += 22;
            ctx.fillText(`💀 击杀僵尸:${selectedUnit.killCount}`, px + 14, ly);
            ly +=22;
            ctx.fillText(`🎯 有效射程:${SHOTGUN_MAX_RANGE}px`, px +14, ly);
            ly +=22;
            const target = selectedUnit.findNearestZombie(units, POLICE_VISION_RANGE);
            const tName = target ? target.getTypeName() : "无";
            ctx.fillText(`🎯 目标:${tName}`, px + 14, ly);
            ly += 22;
        }

        ctx.font = "10px Arial";
        ctx.fillStyle = "#aaa";
        ctx.textAlign = "center";
        ctx.fillText("点击空白处取消选中 / 拖动视角", px + panelW / 2, py + panelH - 10);
    }

    for (const btn of speedButtons) {
        const isActive = gameSpeed === btn.speed;

        drawRoundRect(btn.x + 2, btn.y + 3, btn.w, btn.h, 10);
        ctx.fillStyle = "rgba(0,0,0,0.15)";
        ctx.fill();

        drawRoundRect(btn.x, btn.y, btn.w, btn.h, 10);
        if (isActive) {
            const grad = ctx.createLinearGradient(btn.x, btn.y, btn.x, btn.y + btn.h);
            grad.addColorStop(0, "#ff7b3a");
            grad.addColorStop(1, "#e85a1a");
            ctx.fillStyle = grad;
        } else {
            const grad = ctx.createLinearGradient(btn.x, btn.y, btn.x, btn.y + btn.h);
            grad.addColorStop(0, "#f5f5f5");
            grad.addColorStop(1, "#e0e0e0");
            ctx.fillStyle = grad;
        }
        ctx.fill();
        ctx.strokeStyle = isActive ? "#cc4400" : "#bbb";
        ctx.lineWidth = 2;
        ctx.stroke();

        ctx.fillStyle = isActive ? "#fff" : "#444";
        ctx.font = "bold 26px Arial";
        ctx.textAlign = "center";
        ctx.textBaseline = "middle";
        ctx.fillText(btn.label, btn.x + btn.w/2, btn.y + btn.h/2);
    }
    ctx.textBaseline = "alphabetic";

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

Game Source: 僵尸生存模拟 伤害浮动文字+警察熟练度机制【支援警察+射程限制】

Creator: EpicCoder88

Libraries: none

Complexity: complex (1295 lines, 45.3 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-epiccoder88-murxylff" to link back to the original. Then publish at arcadelab.ai/publish.