🎮ArcadeLab

猫 vs 猎物 · 迷你实验版

by EpicCoder88
531 lines16.4 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>猫 vs 猎物 · 迷你实验版</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; font-family: system-ui; }
        body {
            background: #111;
            display: flex;
            flex-direction: column;
            align-items: center;
            padding: 10px;
            min-height: 100vh;
            justify-content: center;
        }
        canvas {
            border: 2px solid #444;
            border-radius: 8px;
            background: #3d6b37;
            max-width: 100%;
            height: auto;
            touch-action: none;
        }
        .panel {
            margin-top: 12px;
            width: 100%;
            max-width: 800px;
            background: #1a1a1a;
            border: 1px solid #444;
            border-radius: 6px;
            padding: 10px 14px;
            color: #ddd;
            font-size: 14px;
            line-height: 1.6;
        }
        .panel strong { color: #ffd700; }
        .stats {
            display: flex;
            justify-content: space-between;
            flex-wrap: wrap;
        }
        .stats span {
            margin-right: 15px;
        }
        .controls {
            margin-top: 8px;
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
        }
        .controls button {
            background: #333;
            color: #fff;
            border: 1px solid #555;
            border-radius: 4px;
            padding: 4px 14px;
            cursor: pointer;
            font-size: 14px;
        }
        .controls button.active {
            background: #5a7a5a;
            border-color: #8aa88a;
        }
        .log {
            margin-top: 8px;
            max-height: 100px;
            overflow-y: auto;
            background: #111;
            border-radius: 4px;
            padding: 4px 8px;
            font-size: 13px;
            color: #8f8;
            border: 1px solid #333;
        }
        .log .entry { border-bottom: 1px solid #2a2a2a; padding: 2px 0; }
        .log .time { color: #888; }
    </style>
</head>
<body>
    <canvas id="simCanvas" width="800" height="500"></canvas>

    <div class="panel">
        <div><strong>🐱 猫 vs 猎物 · 迷你实验</strong></div>
        <div class="stats">
            <span>🐱 猫: <span id="catStatus">空闲</span></span>
            <span>🎯 猎物: <span id="preyStatus">闲逛</span></span>
            <span>📏 距离: <span id="distance">0</span></span>
            <span>⚡ 速度: <span id="speedDisplay">1×</span></span>
        </div>
        <div class="controls">
            <button id="resetBtn" onclick="resetSim()">🔄 重置</button>
            <button id="pauseBtn" onclick="togglePause()">⏸️ 暂停</button>
            <button class="active" onclick="setSpeed(1)">1×</button>
            <button onclick="setSpeed(2)">2×</button>
            <button onclick="setSpeed(5)">5×</button>
        </div>
        <div class="log" id="logPanel">
            <div class="entry">实验启动,观察猫的追击行为...</div>
        </div>
    </div>

<script>
// ============================================================
// 迷你实验版:1猫 vs 1猎物,关闭饥饿,仅保留核心逻辑
// ============================================================

const canvas = document.getElementById('simCanvas');
const ctx = canvas.getContext('2d');
const catStatusEl = document.getElementById('catStatus');
const preyStatusEl = document.getElementById('preyStatus');
const distanceEl = document.getElementById('distance');
const speedDisplay = document.getElementById('speedDisplay');
const logPanel = document.getElementById('logPanel');

// ---------- 配置 ----------
const CAT_RADIUS = 14;
const PREY_RADIUS = 8;
const CAT_SPEED = 1.8;          // 略快于猎物
const PREY_SPEED = 1.2;
const CAT_VIEW_RANGE = 200;
const CAT_VIEW_ANGLE = Math.PI / 2.5;  // 约72度
const PREY_VIEW_RANGE = 150;
const PREY_VIEW_ANGLE = Math.PI / 2.5;
const CAT_HUNT_RANGE = 28;       // 捕猎距离

const WORLD_BOUND = { left: 0, top: 0, right: 800, bottom: 500 };

// ---------- 状态 ----------
let paused = false;
let speed = 1;
let frameCount = 0;

// 实体
let cat = {
    x: 100, y: 250,
    angle: 0,
    speed: CAT_SPEED,
    target: null,          // 目标猎物引用
    state: 'idle',         // idle, chase, attack
    stuckTimer: 0
};

let prey = {
    x: 600, y: 250,
    angle: 0,
    speed: PREY_SPEED,
    target: null,          // 目标植物(无)
    state: 'wander',       // wander, flee, eating
    stuckTimer: 0
};

// 记录日志
let logMessages = [];

// ---------- 工具函数 ----------
function distance(a, b) {
    return Math.hypot(a.x - b.x, a.y - b.y);
}

function angleTo(a, b) {
    return Math.atan2(b.y - a.y, b.x - a.x);
}

function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }

function safeMove(entity, moveX, moveY, radius) {
    let newX = entity.x + moveX;
    let newY = entity.y + moveY;
    const left = WORLD_BOUND.left + radius;
    const right = WORLD_BOUND.right - radius;
    const top = WORLD_BOUND.top + radius;
    const bottom = WORLD_BOUND.bottom - radius;

    let canX = (newX >= left && newX <= right);
    let canY = (newY >= top && newY <= bottom);

    if (canX && canY) {
        entity.x = newX;
        entity.y = newY;
        entity.stuckTimer = 0;
    } else if (canX && !canY) {
        entity.x = newX;
        entity.y = clamp(entity.y, top, bottom);
        entity.stuckTimer = 0;
    } else if (!canX && canY) {
        entity.x = clamp(entity.x, left, right);
        entity.y = newY;
        entity.stuckTimer = 0;
    } else {
        // 两个方向都卡住,转向
        entity.stuckTimer = (entity.stuckTimer || 0) + 1;
        if (entity.stuckTimer > 20) {
            entity.x = clamp(entity.x + Math.cos(entity.angle)*10, left, right);
            entity.y = clamp(entity.y + Math.sin(entity.angle)*10, top, bottom);
            entity.angle += (Math.random() > 0.5 ? 1 : -1) * 1.2;
            entity.stuckTimer = 0;
        } else {
            entity.angle += (Math.random() > 0.5 ? 0.3 : -0.3);
        }
        // 钳制位置
        entity.x = clamp(entity.x, left, right);
        entity.y = clamp(entity.y, top, bottom);
    }
}

// 视线是否被遮挡(这里无遮挡,因为无树/灌木)
function isLineBlocked(x1, y1, x2, y2) {
    return false;  // 实验版无遮挡
}

// 猎物是否在猫的视野内
function isPreyVisible(catPos, preyPos) {
    const d = distance(catPos, preyPos);
    if (d > CAT_VIEW_RANGE) return false;
    const ang = angleTo(catPos, preyPos);
    let delta = ang - catPos.angle;
    while (delta > Math.PI) delta -= 2*Math.PI;
    while (delta < -Math.PI) delta += 2*Math.PI;
    return Math.abs(delta) <= CAT_VIEW_ANGLE/2;
}

// 猫是否在猎物视野内(用于猎物逃跑)
function isCatVisible(preyPos, catPos) {
    const d = distance(preyPos, catPos);
    if (d > PREY_VIEW_RANGE) return false;
    const ang = angleTo(preyPos, catPos);
    let delta = ang - preyPos.angle;
    while (delta > Math.PI) delta -= 2*Math.PI;
    while (delta < -Math.PI) delta += 2*Math.PI;
    return Math.abs(delta) <= PREY_VIEW_ANGLE/2;
}

// ---------- 日志 ----------
function addLog(msg) {
    const time = new Date().toLocaleTimeString();
    logMessages.push({ time, msg });
    if (logMessages.length > 30) logMessages.shift();
    renderLog();
}

function renderLog() {
    logPanel.innerHTML = logMessages.map(e =>
        `<div class="entry"><span class="time">[${e.time}]</span> ${e.msg}</div>`
    ).join('');
    logPanel.scrollTop = logPanel.scrollHeight;
}

// ---------- 更新UI ----------
function updateUI() {
    const d = distance(cat, prey);
    distanceEl.textContent = d.toFixed(1);
    catStatusEl.textContent = cat.state;
    preyStatusEl.textContent = prey.state;
    speedDisplay.textContent = speed + '×';
}

// ---------- 重置 ----------
function resetSim() {
    cat.x = 100; cat.y = 250; cat.angle = 0; cat.state = 'idle'; cat.target = null;
    prey.x = 600; prey.y = 250; prey.angle = 0; prey.state = 'wander';
    logMessages = [];
    addLog('场景已重置,猫和猎物分开放置。');
    updateUI();
}

// ---------- 暂停/速度 ----------
function togglePause() {
    paused = !paused;
    document.getElementById('pauseBtn').textContent = paused ? '▶️ 继续' : '⏸️ 暂停';
    addLog(paused ? '已暂停' : '已继续');
}

function setSpeed(s) {
    speed = s;
    document.querySelectorAll('.controls button').forEach(b => b.classList.remove('active'));
    // 高亮当前速度按钮(通过文本匹配)
    document.querySelectorAll('.controls button').forEach(b => {
        if (b.textContent.includes(s+'×')) b.classList.add('active');
    });
    addLog(`速度切换至 ${s}x`);
}

// ---------- AI更新 ----------
function updateAI() {
    if (paused) return;

    // ---- 猫的AI ----
    const catToPrey = distance(cat, prey);
    const catAngleToPrey = angleTo(cat, prey);

    // 判断猎物是否在视野内
    const visible = isPreyVisible(cat, prey);

    if (visible && catToPrey < CAT_VIEW_RANGE) {
        // 看到猎物,转向并追击
        cat.angle = catAngleToPrey;
        if (catToPrey <= CAT_HUNT_RANGE) {
            // 捕猎成功
            cat.state = 'attack';
            // 重置猎物位置(模拟捕获)
            addLog('🐱 猫捕获了猎物!重置位置。');
            prey.x = 700 + Math.random()*80 - 40;
            prey.y = 200 + Math.random()*100 - 50;
            prey.angle = Math.random()*2*Math.PI;
            prey.state = 'wander';
            // 猫重置状态
            cat.state = 'idle';
            cat.target = null;
            // 更新UI
            updateUI();
            return;
        } else {
            // 追击
            cat.state = 'chase';
            const moveX = Math.cos(catAngleToPrey) * cat.speed;
            const moveY = Math.sin(catAngleToPrey) * cat.speed;
            safeMove(cat, moveX, moveY, CAT_RADIUS);
        }
    } else {
        // 看不到猎物,闲逛
        if (cat.state !== 'idle') {
            cat.state = 'idle';
            addLog('猫丢失猎物,转为闲逛');
        }
        // 随机转向
        if (Math.random() < 0.01) {
            cat.angle += (Math.random() - 0.5) * 1.5;
        }
        const moveX = Math.cos(cat.angle) * cat.speed * 0.4;
        const moveY = Math.sin(cat.angle) * cat.speed * 0.4;
        safeMove(cat, moveX, moveY, CAT_RADIUS);
    }

    // ---- 猎物的AI ----
    const preyToCat = distance(prey, cat);
    const preyAngleToCat = angleTo(prey, cat);

    // 判断猫是否在猎物视野内
    const catVisible = isCatVisible(prey, cat);

    if (catVisible && preyToCat < PREY_VIEW_RANGE) {
        // 发现猫,逃跑
        prey.state = 'flee';
        const fleeAngle = preyAngleToCat + Math.PI; // 背对猫
        // 检查逃跑方向是否有效(边界)
        const testX = prey.x + Math.cos(fleeAngle)*30;
        const testY = prey.y + Math.sin(fleeAngle)*30;
        if (testX < WORLD_BOUND.left+20 || testX > WORLD_BOUND.right-20 ||
            testY < WORLD_BOUND.top+20 || testY > WORLD_BOUND.bottom-20) {
            // 如果逃跑方向出界,随机偏转
            fleeAngle += (Math.random() > 0.5 ? 1 : -1) * 1.2;
        }
        prey.angle = fleeAngle;
        const moveX = Math.cos(fleeAngle) * prey.speed * 1.5;
        const moveY = Math.sin(fleeAngle) * prey.speed * 1.5;
        safeMove(prey, moveX, moveY, PREY_RADIUS);
    } else {
        // 无威胁,闲逛
        if (prey.state !== 'wander') {
            prey.state = 'wander';
            addLog('猎物安全,转为闲逛');
        }
        if (Math.random() < 0.01) {
            prey.angle += (Math.random() - 0.5) * 1.5;
        }
        const moveX = Math.cos(prey.angle) * prey.speed * 0.5;
        const moveY = Math.sin(prey.angle) * prey.speed * 0.5;
        safeMove(prey, moveX, moveY, PREY_RADIUS);
    }

    // 边界修正(安全钳)
    cat.x = clamp(cat.x, WORLD_BOUND.left+CAT_RADIUS, WORLD_BOUND.right-CAT_RADIUS);
    cat.y = clamp(cat.y, WORLD_BOUND.top+CAT_RADIUS, WORLD_BOUND.bottom-CAT_RADIUS);
    prey.x = clamp(prey.x, WORLD_BOUND.left+PREY_RADIUS, WORLD_BOUND.right-PREY_RADIUS);
    prey.y = clamp(prey.y, WORLD_BOUND.top+PREY_RADIUS, WORLD_BOUND.bottom-PREY_RADIUS);

    updateUI();
}

// ---------- 绘制 ----------
function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // 网格
    ctx.strokeStyle = 'rgba(255,255,255,0.08)';
    ctx.lineWidth = 1;
    for (let x=0; x<canvas.width; x+=50) {
        ctx.beginPath();
        ctx.moveTo(x, 0);
        ctx.lineTo(x, canvas.height);
        ctx.stroke();
    }
    for (let y=0; y<canvas.height; y+=50) {
        ctx.beginPath();
        ctx.moveTo(0, y);
        ctx.lineTo(canvas.width, y);
        ctx.stroke();
    }

    // 视野锥(猫)
    ctx.save();
    ctx.beginPath();
    ctx.moveTo(cat.x, cat.y);
    const startAngle = cat.angle - CAT_VIEW_ANGLE/2;
    const endAngle = cat.angle + CAT_VIEW_ANGLE/2;
    ctx.arc(cat.x, cat.y, CAT_VIEW_RANGE, startAngle, endAngle);
    ctx.closePath();
    ctx.fillStyle = 'rgba(242, 166, 90, 0.08)';
    ctx.fill();
    ctx.strokeStyle = 'rgba(242, 166, 90, 0.3)';
    ctx.lineWidth = 1;
    ctx.stroke();

    // 猎物视野锥
    ctx.beginPath();
    ctx.moveTo(prey.x, prey.y);
    const preyStart = prey.angle - PREY_VIEW_ANGLE/2;
    const preyEnd = prey.angle + PREY_VIEW_ANGLE/2;
    ctx.arc(prey.x, prey.y, PREY_VIEW_RANGE, preyStart, preyEnd);
    ctx.closePath();
    ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
    ctx.fill();
    ctx.strokeStyle = 'rgba(0, 0, 0, 0.15)';
    ctx.lineWidth = 1;
    ctx.stroke();
    ctx.restore();

    // 猎物
    ctx.shadowColor = 'rgba(0,0,0,0.3)';
    ctx.shadowBlur = 6;
    ctx.beginPath();
    ctx.arc(prey.x, prey.y, PREY_RADIUS, 0, 2*Math.PI);
    ctx.fillStyle = '#333';
    ctx.fill();
    ctx.strokeStyle = '#222';
    ctx.lineWidth = 2;
    ctx.stroke();
    // 眼睛
    ctx.fillStyle = '#fff';
    ctx.beginPath();
    ctx.arc(prey.x-3, prey.y-2, 2, 0, 2*Math.PI);
    ctx.fill();
    ctx.beginPath();
    ctx.arc(prey.x+3, prey.y-2, 2, 0, 2*Math.PI);
    ctx.fill();
    ctx.fillStyle = '#000';
    ctx.beginPath();
    ctx.arc(prey.x-3, prey.y-2, 0.8, 0, 2*Math.PI);
    ctx.fill();
    ctx.beginPath();
    ctx.arc(prey.x+3, prey.y-2, 0.8, 0, 2*Math.PI);
    ctx.fill();
    // 状态文字
    ctx.shadowBlur = 0;
    ctx.fillStyle = '#ccc';
    ctx.font = '12px system-ui';
    ctx.textAlign = 'center';
    ctx.fillText(prey.state, prey.x, prey.y - PREY_RADIUS - 6);

    // 猫
    ctx.shadowBlur = 6;
    ctx.beginPath();
    ctx.arc(cat.x, cat.y, CAT_RADIUS, 0, 2*Math.PI);
    ctx.fillStyle = '#f2a65a';
    ctx.fill();
    ctx.strokeStyle = '#222';
    ctx.lineWidth = 2;
    ctx.stroke();
    // 眼睛
    ctx.fillStyle = '#000';
    const eyeOff = 4;
    ctx.beginPath();
    ctx.arc(cat.x + Math.cos(cat.angle-0.5)*5 - 2, cat.y + Math.sin(cat.angle-0.5)*5 - 2, 2.5, 0, 2*Math.PI);
    ctx.fill();
    ctx.beginPath();
    ctx.arc(cat.x + Math.cos(cat.angle+0.5)*5 + 2, cat.y + Math.sin(cat.angle+0.5)*5 + 2, 2.5, 0, 2*Math.PI);
    ctx.fill();
    // 鼻子
    ctx.fillStyle = '#d44';
    ctx.beginPath();
    ctx.arc(cat.x + Math.cos(cat.angle)*4, cat.y + Math.sin(cat.angle)*4, 1.5, 0, 2*Math.PI);
    ctx.fill();
    // 状态文字
    ctx.shadowBlur = 0;
    ctx.fillStyle = '#fff';
    ctx.font = 'bold 14px system-ui';
    ctx.textAlign = 'center';
    ctx.fillText(cat.state, cat.x, cat.y - CAT_RADIUS - 8);

    // 距离线
    ctx.beginPath();
    ctx.moveTo(cat.x, cat.y);
    ctx.lineTo(prey.x, prey.y);
    ctx.strokeStyle = 'rgba(255,255,0,0.2)';
    ctx.lineWidth = 1;
    ctx.setLineDash([4,4]);
    ctx.stroke();
    ctx.setLineDash([]);

    // 边界
    ctx.strokeStyle = '#000';
    ctx.lineWidth = 3;
    ctx.strokeRect(WORLD_BOUND.left, WORLD_BOUND.top,
                   WORLD_BOUND.right - WORLD_BOUND.left,
                   WORLD_BOUND.bottom - WORLD_BOUND.top);

    ctx.shadowBlur = 0;
}

// ---------- 主循环 ----------
function gameLoop() {
    for (let i=0; i<speed; i++) {
        updateAI();
    }
    draw();
    requestAnimationFrame(gameLoop);
}

// ---------- 初始化 ----------
resetSim();
gameLoop();

// 暴露控制函数
window.resetSim = resetSim;
window.togglePause = togglePause;
window.setSpeed = setSpeed;
</script>
</body>
</html>

Game Source: 猫 vs 猎物 · 迷你实验版

Creator: EpicCoder88

Libraries: none

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