🎮ArcadeLab

猫武士 · 调试场地 (改进版)

by EpicCoder88
1833 lines69.0 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; font-family: system-ui; }
        body {
            background: #1a1a1a;
            display: flex;
            flex-direction: column;
            align-items: center;
            padding: 10px;
            min-height: 100vh;
            color: #ddd;
        }
        .container {
            display: flex;
            flex-wrap: wrap;
            gap: 12px;
            justify-content: center;
            max-width: 1200px;
            width: 100%;
        }
        .canvas-wrapper {
            border: 2px solid #444;
            border-radius: 6px;
            background: #3d6b37;
            flex: 0 0 auto;
            touch-action: none;
            position: relative;
            overflow: hidden;
        }
        canvas {
            display: block;
            width: 800px;
            height: 600px;
            touch-action: none;
            cursor: grab;
        }
        canvas:active {
            cursor: grabbing;
        }
        .panel {
            background: #222;
            border: 2px solid #444;
            border-radius: 6px;
            padding: 12px 16px;
            min-width: 220px;
            flex: 1 1 250px;
            max-width: 350px;
            height: fit-content;
            max-height: 600px;
            overflow-y: auto;
        }
        .panel h3 {
            color: #ffd700;
            margin-bottom: 8px;
            font-size: 16px;
            border-bottom: 1px solid #444;
            padding-bottom: 4px;
        }
        .panel .group {
            margin-bottom: 10px;
        }
        .panel label {
            display: flex;
            align-items: center;
            gap: 6px;
            font-size: 13px;
            cursor: pointer;
            padding: 3px 0;
        }
        .panel label input[type="checkbox"] {
            width: 16px;
            height: 16px;
            accent-color: #5a7a5a;
            cursor: pointer;
        }
        .panel .tools {
            display: flex;
            flex-wrap: wrap;
            gap: 4px;
            margin-top: 6px;
        }
        .panel .tools button {
            background: #333;
            color: #ddd;
            border: 1px solid #555;
            border-radius: 4px;
            padding: 4px 10px;
            font-size: 12px;
            cursor: pointer;
            flex: 1 0 auto;
        }
        .panel .tools button.active {
            background: #5a7a5a;
            border-color: #8aa88a;
        }
        .panel .tools button:hover {
            background: #444;
        }
        .info {
            font-size: 12px;
            color: #888;
            margin-top: 6px;
        }
        .status-line {
            display: flex;
            flex-wrap: wrap;
            justify-content: space-between;
            font-size: 12px;
            color: #aaa;
            border-top: 1px solid #333;
            padding-top: 4px;
            margin-top: 6px;
            gap: 4px;
        }
        .badge {
            background: #333;
            padding: 0 6px;
            border-radius: 3px;
            color: #8f8;
        }
        .view-controls {
            display: flex;
            gap: 6px;
            margin-top: 4px;
        }
        .view-controls button {
            background: #2a2a2a;
            border: 1px solid #444;
            color: #ccc;
            border-radius: 4px;
            padding: 2px 8px;
            font-size: 12px;
            cursor: pointer;
        }
        .view-controls button:hover {
            background: #3a3a3a;
        }
        @media (max-width: 850px) {
            canvas { width: 100%; height: auto; aspect-ratio: 4/3; }
            .panel { max-width: 100%; flex: 1 1 100%; }
        }
    </style>
</head>
<body>
<div class="container">
    <div class="canvas-wrapper">
        <canvas id="debugCanvas" width="800" height="600"></canvas>
    </div>
    <div class="panel">
        <h3>⚙️ 调试控制</h3>
        <div class="group">
            <label><input type="checkbox" id="chkDayNight" checked> 昼夜交替</label>
            <label><input type="checkbox" id="chkHunger" checked> 饥饿系统</label>
            <label><input type="checkbox" id="chkHuntAI" checked> 捕猎AI</label>
            <label><input type="checkbox" id="chkLearning" checked> 学习系统</label>
            <label><input type="checkbox" id="chkPlants" checked> 植物生长</label>
            <label><input type="checkbox" id="chkSleep" checked> 睡眠系统</label>
            <label><input type="checkbox" id="chkRescue" checked> 救助系统</label>
            <label><input type="checkbox" id="chkMate" checked> 繁殖系统</label>
            <label><input type="checkbox" id="chkNest" checked> 筑巢系统</label>
            <label><input type="checkbox" id="chkAmbush" checked> 伏击/掩护</label>
        </div>
        <div class="group">
            <div style="font-size:13px; margin-bottom:4px; color:#aaa;">🔧 放置生物(按住拖拽到地图)</div>
            <div class="tools">
                <button id="toolCat" class="active">🐱 猫</button>
                <button id="toolPrey">🐾 猎物</button>
                <button id="toolBush">🌿 灌木</button>
                <button id="toolThorn">🌵 荆棘</button>
                <button id="toolTree">🌳 树</button>
            </div>
            <div style="margin-top:6px;">
                <button id="btnReset" style="background:#5a3a3a; border-color:#8a5a5a;">🔄 重置场景</button>
                <button id="btnClear" style="background:#3a3a5a; border-color:#5a5a8a;">🗑️ 清空所有</button>
            </div>
            <div class="view-controls">
                <button id="btnResetView">🎯 复位视角</button>
                <span style="font-size:11px; color:#666; margin-left:6px;">拖拽平移 | 滚轮缩放</span>
            </div>
        </div>
        <div class="info">
            <span id="placeInfo">未放置</span>
        </div>
        <div class="status-line">
            <span>猎物: <span id="preyCount">0</span></span>
            <span>猫: <span id="catCount">0</span></span>
            <span>草: <span id="herbCount">0</span></span>
            <span>帧: <span id="frameCount">0</span></span>
        </div>
    </div>
</div>

<script>
// ============================================================
// 猫武士 · 调试场地 (改进版)
// 增加:视角拖拽平移、滚轮缩放、更精细的碰撞检测
// ============================================================

// ---------- 常量 ----------
const GRID_SIZE = 40;
const GRID_ALPHA = 0.15;
const GRID_COLOR = "#ffffff";
const GRASS_COLOR = "#3d6b37";

const TREE_COLOR = "#6b4423";
const TREE_RADIUS = 18;
const TREE_COLLIDE_RADIUS = 22;
const TREE_BLOCK_RAY_RADIUS = 20;
const TREE_MIN_DISTANCE = 80;

const HERB_COLOR = "#1c4d19";
const HERB_SIZE = 8;
const HERB_MAX_COUNT = 60;
const HERB_MIN_SPACE = 25;
const HERB_RESPAWN_INTERVAL = 120;
const HERB_SPAWN_PER_TICK = 3;

const PREY_RADIUS = 6;
const PREY_MAX_COUNT = 60;
const PREY_SPEED = 1.0;
const PREY_FLEE_SPEED = 2.5;
const PREY_VIEW_RANGE = 140;
const PREY_VIEW_ANGLE = Math.PI / 3;
const PREY_FEED_RANGE = 14;
const BREED_DISTANCE = 30;
const FEED_REQUIRE = 2;
const BABY_COUNT = 3;
const MAX_HUNGER_TIME = 18000;

const CAT_COLOR = "#f2a65a";
const CAT_RADIUS = 12;
const CAT_BASE_SPEED = 1.4;
const CAT_VIEW_RANGE = 180;
const CAT_VIEW_ANGLE = Math.PI / 3;
const CAT_HUNT_RANGE = 28;
const REWARD_CATCH = 10;
const PENALTY_ESCAPE = -6;
const CAT_BREED_REQUIRE = 2;
const CAT_BREED_DISTANCE = 35;
const CAT_BREED_COOLDOWN = 600;
const CAT_LITTER_SIZE = 2;
const CAT_GROW_TIME = 1200;
const CAT_HUNGER_MAX = 18000;
const CAT_HUNGER_DECAY = 1;
const CAT_HUNGER_STATE = 1200;
const CAT_FULL_THRESHOLD = 4000;
const MAX_CATS = 80;

const DAY_LENGTH = 1800;
const NIGHT_LENGTH = 1200;
const BUSH_RADIUS = 25;
const THORN_RADIUS = 18;
const BUSH_COUNT = 30;
const THORN_COUNT = 20;
const NEST_CHECK_RADIUS = 60;
const NEST_PREFER_RADIUS = 100;

const MALE_SYMBOL = "♂";
const FEMALE_SYMBOL = "♀";

// 世界边界(小场地)
const WORLD_BOUND = { left: -100, top: -80, right: 900, bottom: 680 };

// ---------- Canvas ----------
const canvas = document.getElementById('debugCanvas');
const ctx = canvas.getContext('2d');

// ---------- UI 元素 ----------
const chkDayNight = document.getElementById('chkDayNight');
const chkHunger = document.getElementById('chkHunger');
const chkHuntAI = document.getElementById('chkHuntAI');
const chkLearning = document.getElementById('chkLearning');
const chkPlants = document.getElementById('chkPlants');
const chkSleep = document.getElementById('chkSleep');
const chkRescue = document.getElementById('chkRescue');
const chkMate = document.getElementById('chkMate');
const chkNest = document.getElementById('chkNest');
const chkAmbush = document.getElementById('chkAmbush');

const toolCat = document.getElementById('toolCat');
const toolPrey = document.getElementById('toolPrey');
const toolBush = document.getElementById('toolBush');
const toolThorn = document.getElementById('toolThorn');
const toolTree = document.getElementById('toolTree');
const btnReset = document.getElementById('btnReset');
const btnClear = document.getElementById('btnClear');
const btnResetView = document.getElementById('btnResetView');
const placeInfo = document.getElementById('placeInfo');
const preyCountSpan = document.getElementById('preyCount');
const catCountSpan = document.getElementById('catCount');
const herbCountSpan = document.getElementById('herbCount');
const frameCountSpan = document.getElementById('frameCount');

// ---------- 相机 ----------
let offsetX = 0, offsetY = 0;
let scale = 1.0;
const MIN_SCALE = 0.4;
const MAX_SCALE = 2.0;

// ---------- 状态 ----------
let frame = 0;
let speedMultiplier = 1;

// 实体
let trees = [], herbs = [], preys = [], cats = [];
let bushes = [], thorns = [], nests = [];
let floatingTexts = [];
let herbRespawnTimer = 0;

let isDay = true;
let dayTimer = 0;
let nightTimer = 0;

// 放置工具
let activeTool = 'cat';
let isDragging = false;
let mouseDownPos = null;

// 视角控制
let isViewDragging = false;
let dragStartX = 0, dragStartY = 0;
let dragStartOffsetX = 0, dragStartOffsetY = 0;

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

function pointInCircle(px, py, cx, cy, r) {
    return Math.hypot(px - cx, py - cy) <= r;
}

function lineIntersectsCircle(x1, y1, x2, y2, cx, cy, r) {
    const dx = x2 - x1, dy = y2 - y1;
    const fx = x1 - cx, fy = y1 - cy;
    const a = dx*dx + dy*dy;
    const b = 2*(fx*dx + fy*dy);
    const c = fx*fx + fy*fy - r*r;
    let disc = b*b - 4*a*c;
    if (disc < 0) return false;
    disc = Math.sqrt(disc);
    const t1 = (-b - disc) / (2*a);
    const t2 = (-b + disc) / (2*a);
    return (t1 >= 0 && t1 <= 1) || (t2 >= 0 && t2 <= 1);
}

function isPointInWorld(x, y) {
    return x >= WORLD_BOUND.left && x <= WORLD_BOUND.right &&
           y >= WORLD_BOUND.top && y <= WORLD_BOUND.bottom;
}

function isLineBlocked(x1, y1, x2, y2) {
    for (const tree of trees) {
        if (lineIntersectsCircle(x1, y1, x2, y2, tree.x, tree.y, TREE_BLOCK_RAY_RADIUS)) return true;
    }
    for (const bush of bushes) {
        if (pointInCircle(x1, y1, bush.x, bush.y, BUSH_RADIUS)) continue;
        if (pointInCircle(x2, y2, bush.x, bush.y, BUSH_RADIUS)) return true;
        if (lineIntersectsCircle(x1, y1, x2, y2, bush.x, bush.y, BUSH_RADIUS)) return true;
    }
    return false;
}

function checkCollision(x, y, radius) {
    for (const tree of trees) {
        if (distance({x,y}, tree) < TREE_COLLIDE_RADIUS + radius) return true;
    }
    for (const thorn of thorns) {
        if (distance({x,y}, thorn) < THORN_RADIUS + radius) return true;
    }
    return false;
}

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);

    let finalX = entity.x, finalY = entity.y;

    if (canX && canY) {
        finalX = newX; finalY = newY;
    } else if (canX && !canY) {
        finalX = newX; finalY = entity.y;
    } else if (!canX && canY) {
        finalX = entity.x; finalY = newY;
    } else {
        entity.stuckTimer = (entity.stuckTimer || 0) + 1;
        if (entity.stuckTimer > 25) {
            entity.x -= Math.cos(entity.angle) * 18;
            entity.y -= Math.sin(entity.angle) * 18;
            entity.angle += (Math.random() > 0.5 ? 1 : -1) * (Math.PI/2 + Math.random()*Math.PI/3);
            entity.stuckTimer = 0;
        } else {
            entity.angle += (Math.random() > 0.5 ? 0.4 : -0.4);
        }
        entity.x = Math.max(left, Math.min(right, entity.x));
        entity.y = Math.max(top, Math.min(bottom, entity.y));
        return;
    }

    if (checkCollision(finalX, finalY, radius)) {
        if (!checkCollision(finalX, entity.y, radius)) {
            finalY = entity.y;
        } else if (!checkCollision(entity.x, finalY, radius)) {
            finalX = entity.x;
        } else {
            entity.stuckTimer = (entity.stuckTimer || 0) + 1;
            if (entity.stuckTimer > 25) {
                entity.x -= Math.cos(entity.angle) * 18;
                entity.y -= Math.sin(entity.angle) * 18;
                entity.angle += (Math.random() > 0.5 ? 1 : -1) * (Math.PI/2 + Math.random()*Math.PI/3);
                entity.stuckTimer = 0;
            } else {
                entity.angle += (Math.random() > 0.5 ? 0.4 : -0.4);
            }
            entity.x = Math.max(left, Math.min(right, entity.x));
            entity.y = Math.max(top, Math.min(bottom, entity.y));
            return;
        }
    }

    entity.x = Math.max(left, Math.min(right, finalX));
    entity.y = Math.max(top, Math.min(bottom, finalY));
    entity.stuckTimer = 0;
}

// ---------- 学习系统 ----------
function createLearning() {
    return {
        hunt: { attempts: 0, success: 0 },
        rescue: { attempts: 0, success: 0 },
        mate: { attempts: 0, success: 0 },
        nest: { attempts: 0, success: 0 }
    };
}

function updateLearning(cat, category, success = true) {
    if (!cat.learning) return;
    cat.learning[category].attempts++;
    if (success) cat.learning[category].success++;
}

function getUtility(cat, category) {
    const exp = cat.learning[category];
    if (exp.attempts === 0) return 0.3 + 0.3 * Math.random();
    const rate = exp.success / exp.attempts;
    const exploration = 1 / (1 + exp.attempts * 0.05);
    return rate * (1 - exploration * 0.3) + 0.3 * exploration * 0.3 + 0.1 * Math.random();
}

// ---------- 个体差异 ----------
function generateTraits() {
    return {
        boldness: Math.random() * 0.8 + 0.2,
        patience: Math.random() * 0.8 + 0.2,
        speedOffset: (Math.random() - 0.5) * 0.4,
        sociality: Math.random() * 0.8 + 0.2
    };
}

// ---------- 生成实体 ----------
function generateTrees() {
    trees = [];
    let tries = 0;
    const count = 12;
    while (trees.length < count && tries < count*20) {
        tries++;
        const x = WORLD_BOUND.left + 30 + Math.random()*(WORLD_BOUND.right - WORLD_BOUND.left - 60);
        const y = WORLD_BOUND.top + 30 + Math.random()*(WORLD_BOUND.bottom - WORLD_BOUND.top - 60);
        let ok = true;
        for (const t of trees) if (distance({x,y}, t) < TREE_MIN_DISTANCE) { ok = false; break; }
        if (ok) trees.push({x,y});
    }
    if (trees.length === 0) {
        const fallback = [[100,100], [300,200], [500,100], [200,400], [600,300]];
        for (const [x,y] of fallback) trees.push({x,y});
    }
}

function generateHerbs() {
    herbs = [];
    let count = 0;
    while (count < 30 && count < 50) {
        let x = WORLD_BOUND.left + 20 + Math.random()*(WORLD_BOUND.right - WORLD_BOUND.left - 40);
        let y = WORLD_BOUND.top + 20 + Math.random()*(WORLD_BOUND.bottom - WORLD_BOUND.top - 40);
        let ok = true;
        for (const t of trees) if (distance({x,y}, t) < TREE_COLLIDE_RADIUS + 8) { ok = false; break; }
        if (!ok) continue;
        for (const h of herbs) if (distance({x,y}, h) < HERB_MIN_SPACE) { ok = false; break; }
        if (ok) { herbs.push({x,y}); count++; }
    }
}

function generateBushes() {
    bushes = [];
    let tries = 0;
    while (bushes.length < 15 && tries < 200) {
        tries++;
        const x = WORLD_BOUND.left + 20 + Math.random()*(WORLD_BOUND.right - WORLD_BOUND.left - 40);
        const y = WORLD_BOUND.top + 20 + Math.random()*(WORLD_BOUND.bottom - WORLD_BOUND.top - 40);
        let ok = true;
        for (const t of trees) if (distance({x,y}, t) < TREE_COLLIDE_RADIUS + BUSH_RADIUS) { ok = false; break; }
        if (!ok) continue;
        for (const b of bushes) if (distance({x,y}, b) < BUSH_RADIUS*2) { ok = false; break; }
        if (ok) bushes.push({x,y});
    }
}

function generateThorns() {
    thorns = [];
    let tries = 0;
    while (thorns.length < 10 && tries < 200) {
        tries++;
        const x = WORLD_BOUND.left + 20 + Math.random()*(WORLD_BOUND.right - WORLD_BOUND.left - 40);
        const y = WORLD_BOUND.top + 20 + Math.random()*(WORLD_BOUND.bottom - WORLD_BOUND.top - 40);
        let ok = true;
        for (const t of trees) if (distance({x,y}, t) < TREE_COLLIDE_RADIUS + THORN_RADIUS) { ok = false; break; }
        if (!ok) continue;
        for (const th of thorns) if (distance({x,y}, th) < THORN_RADIUS*2) { ok = false; break; }
        if (ok) thorns.push({x,y});
    }
}

function randomGender() { return Math.random() < 0.5 ? "male" : "female"; }

function generateName() {
    const pre = ["虎","鹰","鸦","香薇","冬青","蕨","火","云","雾","灰","蓝","斑","长","短","亮","暗","雨","雪","霜","溪","河","星","月","日","影","光","叶","花","石","岩","荆棘","芦苇","柳","橡","枫","白","黑","红","金","银","铜"];
    const suf = ["尾","羽","花","毛","爪","足","心","风","叶","霜","云","星","光","月","溪","河","石","岩","棘","苇","柳","橡","枫","桦","松","杉","飞","跃","奔","步","啸","嚎","眼","耳","鼻","须","掌","腿","腹","背","额","面","斑","纹","环"];
    return pre[Math.floor(Math.random()*pre.length)] + suf[Math.floor(Math.random()*suf.length)];
}

function createCat(x, y, isAdult = true) {
    const traits = generateTraits();
    return {
        x, y,
        angle: Math.random()*2*Math.PI,
        wanderTimer: Math.random()*120,
        targetPrey: null,
        targetMate: null,
        lastTarget: null,
        policyScore: 0,
        tryFlank: false,
        stuckTimer: 0,
        huntCount: isAdult ? 2 : 0,
        breedCooldown: 0,
        growTimer: isAdult ? CAT_GROW_TIME : 0,
        gender: randomGender(),
        hungerTimer: CAT_HUNGER_MAX,
        mate: null,
        parents: { father: null, mother: null },
        kittens: [],
        isLactating: false,
        milkSupply: 0,
        carryingPrey: null,
        targetMateLocation: null,
        name: generateName(),
        helpingTarget: null,
        waitingForHelp: false,
        helpTargetPos: null,
        reputation: 0,
        isHelping: false,
        helpAttemptTimer: 0,
        nest: null,
        isSleeping: false,
        sleepPenalty: 0,
        buildNestCooldown: 0,
        learning: createLearning(),
        traits: traits,
        huntMode: null,
        targetBush: null,
        coverPhase: null,
        ambushPhase: null,
        ambushTimer: 0,
        targetLastKnownPos: null,
        targetLostTimer: 0,
        get speed() { return CAT_BASE_SPEED + this.traits.speedOffset; }
    };
}

function createKitten(x, y, father, mother) {
    const cat = createCat(x, y, false);
    cat.growTimer = 0;
    cat.huntCount = 0;
    cat.parents.father = father;
    cat.parents.mother = mother;
    cat.name = generateName();
    const mix = (a, b) => {
        const child = (a + b) / 2 + (Math.random() - 0.5) * 0.2;
        return Math.max(0.1, Math.min(1.0, child));
    };
    cat.traits = {
        boldness: mix(father.traits.boldness, mother.traits.boldness),
        patience: mix(father.traits.patience, mother.traits.patience),
        speedOffset: mix(father.traits.speedOffset, mother.traits.speedOffset),
        sociality: mix(father.traits.sociality, mother.traits.sociality)
    };
    cat.learning = createLearning();
    return cat;
}

function createPrey(x, y) {
    return {
        x, y,
        angle: Math.random()*2*Math.PI,
        wanderTimer: Math.random()*100,
        targetHerb: null,
        targetMate: null,
        eatenHerbs: 0,
        breedCooldown: 0,
        hungerTimer: MAX_HUNGER_TIME,
        fleeTarget: null,
        stuckTimer: 0,
        isSleeping: false
    };
}

function spawnCats(count) {
    let tries = 0;
    while (cats.length < count && tries < count*30) {
        tries++;
        const x = WORLD_BOUND.left + 20 + Math.random()*(WORLD_BOUND.right - WORLD_BOUND.left - 40);
        const y = WORLD_BOUND.top + 20 + Math.random()*(WORLD_BOUND.bottom - WORLD_BOUND.top - 40);
        let ok = true;
        for (const t of trees) if (distance({x,y}, t) < TREE_COLLIDE_RADIUS + CAT_RADIUS) { ok = false; break; }
        if (ok) cats.push(createCat(x,y));
    }
}

function spawnPreys(count) {
    let tries = 0;
    while (preys.length < count && tries < count*30) {
        tries++;
        const x = WORLD_BOUND.left + 20 + Math.random()*(WORLD_BOUND.right - WORLD_BOUND.left - 40);
        const y = WORLD_BOUND.top + 20 + Math.random()*(WORLD_BOUND.bottom - WORLD_BOUND.top - 40);
        let ok = true;
        for (const t of trees) if (distance({x,y}, t) < TREE_COLLIDE_RADIUS + PREY_RADIUS) { ok = false; break; }
        if (ok) preys.push(createPrey(x,y));
    }
}

// ---------- 初始化场景 ----------
function initScene() {
    trees = []; herbs = []; preys = []; cats = [];
    bushes = []; thorns = []; nests = [];
    floatingTexts = [];
    herbRespawnTimer = 0;
    isDay = true;
    dayTimer = 0; nightTimer = 0;
    generateTrees();
    generateHerbs();
    generateBushes();
    generateThorns();
    spawnCats(5);
    spawnPreys(15);
    while (bushes.length < 15) bushes.push({ x: WORLD_BOUND.left+40+Math.random()*600, y: WORLD_BOUND.top+40+Math.random()*400 });
    while (thorns.length < 10) thorns.push({ x: WORLD_BOUND.left+40+Math.random()*600, y: WORLD_BOUND.top+40+Math.random()*400 });
    // 复位视角
    offsetX = 0; offsetY = 0; scale = 1.0;
    updateUI();
}

// ---------- 放置生物 ----------
function placeEntity(type, x, y) {
    if (!isPointInWorld(x, y)) return false;
    let entity = null;
    switch (type) {
        case 'cat':
            if (cats.length >= MAX_CATS) return false;
            entity = createCat(x, y);
            cats.push(entity);
            break;
        case 'prey':
            if (preys.length >= PREY_MAX_COUNT) return false;
            entity = createPrey(x, y);
            preys.push(entity);
            break;
        case 'bush':
            // 不与树和荆棘重叠
            for (const t of trees) if (distance({x,y}, t) < TREE_COLLIDE_RADIUS + BUSH_RADIUS) return false;
            for (const th of thorns) if (distance({x,y}, th) < THORN_RADIUS + BUSH_RADIUS) return false;
            for (const b of bushes) if (distance({x,y}, b) < BUSH_RADIUS*2) return false;
            bushes.push({ x, y });
            break;
        case 'thorn':
            for (const t of trees) if (distance({x,y}, t) < TREE_COLLIDE_RADIUS + THORN_RADIUS) return false;
            for (const b of bushes) if (distance({x,y}, b) < BUSH_RADIUS + THORN_RADIUS) return false;
            for (const th of thorns) if (distance({x,y}, th) < THORN_RADIUS*2) return false;
            thorns.push({ x, y });
            break;
        case 'tree':
            let ok = true;
            for (const t of trees) if (distance({x,y}, t) < TREE_MIN_DISTANCE) { ok = false; break; }
            if (!ok) return false;
            trees.push({ x, y });
            break;
        default: return false;
    }
    return true;
}

// ---------- 更新逻辑 ----------
function updateWorld() {
    if (chkPlants.checked) {
        herbRespawnTimer++;
        if (herbRespawnTimer >= HERB_RESPAWN_INTERVAL) {
            herbRespawnTimer = 0;
            if (herbs.length < HERB_MAX_COUNT) {
                for (let i=0; i<HERB_SPAWN_PER_TICK; i++) {
                    let x = WORLD_BOUND.left + 20 + Math.random()*(WORLD_BOUND.right - WORLD_BOUND.left - 40);
                    let y = WORLD_BOUND.top + 20 + Math.random()*(WORLD_BOUND.bottom - WORLD_BOUND.top - 40);
                    let ok = true;
                    for (const t of trees) if (distance({x,y}, t) < TREE_COLLIDE_RADIUS + 8) { ok = false; break; }
                    if (!ok) continue;
                    for (const h of herbs) if (distance({x,y}, h) < HERB_MIN_SPACE) { ok = false; break; }
                    if (ok) herbs.push({x,y});
                }
            }
        }
    }

    if (chkDayNight.checked) {
        if (isDay) {
            dayTimer++;
            if (dayTimer >= DAY_LENGTH) {
                isDay = false;
                dayTimer = 0;
                nightTimer = 0;
                if (chkSleep.checked) {
                    for (const cat of cats) {
                        if (cat.isSleeping) continue;
                        const progress = cat.growTimer / CAT_GROW_TIME;
                        if (progress < 0.33) continue;
                        const hungry = cat.hungerTimer < CAT_HUNGER_STATE;
                        if (hungry && Math.random() < 0.3) {
                            cat.isSleeping = false;
                            continue;
                        }
                        if (cat.nest && !hungry) {
                            cat.isSleeping = true;
                            if (chkLearning.checked) updateLearning(cat, 'nest', true);
                        } else if (!cat.nest) {
                            cat.isSleeping = true;
                            cat.sleepPenalty = (cat.sleepPenalty || 0) + 1;
                            if (chkLearning.checked) updateLearning(cat, 'nest', false);
                            cat.reputation = Math.max(0, cat.reputation - 1);
                        } else {
                            cat.isSleeping = true;
                        }
                    }
                    for (const prey of preys) {
                        prey.isSleeping = Math.random() < 0.8;
                    }
                }
            }
        } else {
            nightTimer++;
            if (nightTimer >= NIGHT_LENGTH) {
                isDay = true;
                nightTimer = 0;
                dayTimer = 0;
                if (chkSleep.checked) {
                    for (const cat of cats) cat.isSleeping = false;
                    for (const prey of preys) prey.isSleeping = false;
                }
            }
        }
    }

    // 更新猎物
    if (chkHunger.checked || chkHuntAI.checked) {
        for (let i=preys.length-1; i>=0; i--) {
            const prey = preys[i];
            if (prey.isSleeping) continue;
            if (chkHunger.checked) {
                prey.hungerTimer -= 1;
                if (prey.hungerTimer <= 0) { preys.splice(i,1); continue; }
            }
            if (chkHuntAI.checked) {
                let nearestCat = null, minCatDist = Infinity;
                for (const cat of cats) {
                    if (cat.isSleeping) continue;
                    const d = distance(prey, cat);
                    if (d > PREY_VIEW_RANGE) continue;
                    const angleToCat = Math.atan2(cat.y - prey.y, cat.x - prey.x);
                    let delta = angleToCat - prey.angle;
                    while (delta > Math.PI) delta -= 2*Math.PI;
                    while (delta < -Math.PI) delta += 2*Math.PI;
                    if (Math.abs(delta) <= PREY_VIEW_ANGLE/2 && !isLineBlocked(prey.x, prey.y, cat.x, cat.y)) {
                        if (d < minCatDist) { minCatDist = d; nearestCat = cat; }
                    }
                }
                let moveX = 0, moveY = 0;
                if (nearestCat) {
                    const dxAway = prey.x - nearestCat.x, dyAway = prey.y - nearestCat.y;
                    const distToCat = Math.hypot(dxAway, dyAway);
                    let fleeAngle = Math.atan2(dyAway, dxAway);
                    prey.angle = fleeAngle;
                    const testX = prey.x + Math.cos(fleeAngle)*30;
                    const testY = prey.y + Math.sin(fleeAngle)*30;
                    if (!isPointInWorld(testX, testY)) {
                        fleeAngle += (Math.random()>0.5 ? 1 : -1) * Math.PI/4;
                        prey.angle = fleeAngle;
                    }
                    let speedScale = 1;
                    if (distToCat < 140) speedScale = 1 + (140 - distToCat)/140 * 0.45;
                    const speed = PREY_FLEE_SPEED * speedScale;
                    moveX = Math.cos(fleeAngle) * speed;
                    moveY = Math.sin(fleeAngle) * speed;
                } else {
                    if (prey.eatenHerbs >= FEED_REQUIRE && prey.breedCooldown <= 0) {
                        let nearestMate = null, minDist = Infinity;
                        for (const other of preys) {
                            if (other === prey || other.eatenHerbs < FEED_REQUIRE || other.breedCooldown > 0 || other.isSleeping) continue;
                            const d = distance(prey, other);
                            if (d > PREY_VIEW_RANGE || isLineBlocked(prey.x, prey.y, other.x, other.y)) continue;
                            const angleToTarget = Math.atan2(other.y - prey.y, other.x - prey.x);
                            let delta = angleToTarget - prey.angle;
                            while (delta > Math.PI) delta -= 2*Math.PI;
                            while (delta < -Math.PI) delta += 2*Math.PI;
                            if (Math.abs(delta) <= PREY_VIEW_ANGLE/2 && d < minDist) {
                                minDist = d;
                                nearestMate = other;
                            }
                        }
                        prey.targetMate = nearestMate;
                    }
                    if (!prey.targetMate) {
                        let nearestHerb = null, minDist = Infinity;
                        for (const herb of herbs) {
                            const d = distance(prey, herb);
                            if (d > PREY_VIEW_RANGE || isLineBlocked(prey.x, prey.y, herb.x, herb.y)) continue;
                            const angleToTarget = Math.atan2(herb.y - prey.y, herb.x - prey.x);
                            let delta = angleToTarget - prey.angle;
                            while (delta > Math.PI) delta -= 2*Math.PI;
                            while (delta < -Math.PI) delta += 2*Math.PI;
                            if (Math.abs(delta) <= PREY_VIEW_ANGLE/2 && d < minDist) {
                                minDist = d;
                                nearestHerb = herb;
                            }
                        }
                        prey.targetHerb = nearestHerb;
                    }

                    if (prey.targetMate) {
                        const d = distance(prey, prey.targetMate);
                        prey.angle = Math.atan2(prey.targetMate.y - prey.y, prey.targetMate.x - prey.x);
                        if (d > BREED_DISTANCE) {
                            moveX = (prey.targetMate.x - prey.x)/d * PREY_SPEED;
                            moveY = (prey.targetMate.y - prey.y)/d * PREY_SPEED;
                        }
                    } else if (prey.targetHerb) {
                        const d = distance(prey, prey.targetHerb);
                        prey.angle = Math.atan2(prey.targetHerb.y - prey.y, prey.targetHerb.x - prey.x);
                        if (d > PREY_FEED_RANGE) {
                            moveX = (prey.targetHerb.x - prey.x)/d * PREY_SPEED;
                            moveY = (prey.targetHerb.y - prey.y)/d * PREY_SPEED;
                        } else {
                            const idx = herbs.indexOf(prey.targetHerb);
                            if (idx > -1) herbs.splice(idx,1);
                            prey.eatenHerbs += 1;
                            prey.hungerTimer = MAX_HUNGER_TIME;
                            prey.targetHerb = null;
                        }
                    } else {
                        if (prey.wanderTimer <= 0) {
                            prey.angle = Math.random()*2*Math.PI;
                            prey.wanderTimer = 90 + Math.random()*140;
                        }
                        moveX = Math.cos(prey.angle) * PREY_SPEED * 0.65;
                        moveY = Math.sin(prey.angle) * PREY_SPEED * 0.65;
                    }
                }
                safeMove(prey, moveX, moveY, PREY_RADIUS);
            } else {
                // 无AI,闲逛
                prey.wanderTimer -= 1;
                if (prey.wanderTimer <= 0) {
                    prey.angle = Math.random()*2*Math.PI;
                    prey.wanderTimer = 90 + Math.random()*140;
                }
                const moveX = Math.cos(prey.angle) * PREY_SPEED * 0.65;
                const moveY = Math.sin(prey.angle) * PREY_SPEED * 0.65;
                safeMove(prey, moveX, moveY, PREY_RADIUS);
            }
        }
        // 猎物繁殖
        if (chkHuntAI.checked && chkMate.checked) {
            if (preys.length < PREY_MAX_COUNT) {
                for (let i=0; i<preys.length; i++) {
                    const a = preys[i];
                    if (a.breedCooldown > 0 || a.eatenHerbs < FEED_REQUIRE) continue;
                    for (let j=i+1; j<preys.length; j++) {
                        const b = preys[j];
                        if (b.breedCooldown > 0 || b.eatenHerbs < FEED_REQUIRE) continue;
                        if (distance(a,b) <= BREED_DISTANCE) {
                            const cx = (a.x+b.x)/2, cy = (a.y+b.y)/2;
                            for (let k=0; k<BABY_COUNT; k++) {
                                let sx = cx + (Math.random()-0.5)*40;
                                let sy = cy + (Math.random()-0.5)*40;
                                if (checkCollision(sx, sy, PREY_RADIUS)) { sx += 30; sy += 30; }
                                if (preys.length < PREY_MAX_COUNT) preys.push(createPrey(sx, sy));
                            }
                            a.eatenHerbs = 0; b.eatenHerbs = 0;
                            a.breedCooldown = 600; b.breedCooldown = 600;
                            break;
                        }
                    }
                }
            }
        }
    }

    // 更新猫
    for (let i=cats.length-1; i>=0; i--) {
        const cat = cats[i];
        if (cat.isSleeping) {
            if (chkSleep.checked && cat.nest) {
                if (cat.hungerTimer < CAT_HUNGER_MAX) {
                    cat.hungerTimer += 0.5;
                    if (cat.hungerTimer > CAT_HUNGER_STATE + 300) cat.hungerTimer = CAT_HUNGER_STATE + 300;
                }
                const d = distance(cat, cat.nest);
                if (d > 10) {
                    const moveX = (cat.nest.x - cat.x)/d * cat.speed * 0.5;
                    const moveY = (cat.nest.y - cat.y)/d * cat.speed * 0.5;
                    safeMove(cat, moveX, moveY, CAT_RADIUS);
                }
            }
            continue;
        }

        if (cat.waitingForHelp) {
            let helperAlive = false;
            for (const other of cats) {
                if (other.helpingTarget === cat) { helperAlive = true; break; }
            }
            if (!helperAlive) cat.waitingForHelp = false;
            else continue;
        }

        if (chkHunger.checked) {
            cat.hungerTimer -= CAT_HUNGER_DECAY;
            if (cat.hungerTimer <= 0) {
                cats.splice(i,1);
                continue;
            }
        }

        if (cat.growTimer < CAT_GROW_TIME) cat.growTimer += 1;
        if (cat.breedCooldown > 0) cat.breedCooldown--;
        if (cat.buildNestCooldown > 0) cat.buildNestCooldown--;

        const progress = cat.growTimer / CAT_GROW_TIME;
        let stage = 2;
        if (progress < 0.33) stage = 0;
        else if (progress < 0.66) stage = 1;
        const scaleFactor = stage===0 ? 0.4 : (stage===1 ? 0.7 : 1.0);
        const currentRadius = CAT_RADIUS * scaleFactor;

        // 哺乳
        if (cat.isLactating && cat.kittens.length > 0) {
            if (cat.milkSupply > 0) {
                for (const kitten of cat.kittens) {
                    if (distance(cat, kitten) < 40) {
                        cat.milkSupply -= 0.5;
                        kitten.growTimer += 2;
                    }
                }
                if (cat.milkSupply <= 0) cat.isLactating = false;
            } else {
                cat.isLactating = false;
            }
            cat.targetMate = null;
            cat.targetPrey = null;
            continue;
        }

        // 送货
        if (cat.carryingPrey && cat.mate) {
            const target = cat.mate;
            const d = distance(cat, target);
            cat.angle = Math.atan2(target.y - cat.y, target.x - cat.x);
            if (d > 30) {
                const moveX = (target.x - cat.x)/d * cat.speed * 1.2;
                const moveY = (target.y - cat.y)/d * cat.speed * 1.2;
                safeMove(cat, moveX, moveY, currentRadius);
            } else {
                target.milkSupply += 30;
                target.isLactating = true;
                cat.carryingPrey = null;
                cat.targetMateLocation = null;
            }
            cat.targetMate = null;
            cat.targetPrey = null;
            continue;
        }

        const hungry = cat.hungerTimer < CAT_HUNGER_STATE;
        const full = cat.hungerTimer > CAT_FULL_THRESHOLD;
        const canMate = (stage===2) && cat.huntCount >= CAT_BREED_REQUIRE && cat.breedCooldown === 0;

        // 救助
        if (chkRescue.checked && cat.isHelping) {
            if (hungry) {
                if (cat.helpingTarget) {
                    cat.helpingTarget.waitingForHelp = false;
                    cat.helpingTarget = null;
                }
                cat.isHelping = false;
                cat.carryingPrey = null;
                cat.helpAttemptTimer = 0;
                if (chkLearning.checked) updateLearning(cat, 'rescue', false);
            } else {
                if (cat.carryingPrey) {
                    const targetPos = cat.helpTargetPos;
                    if (targetPos) {
                        const d = distance(cat, targetPos);
                        cat.angle = Math.atan2(targetPos.y - cat.y, targetPos.x - cat.x);
                        if (d > 30) {
                            const moveX = (targetPos.x - cat.x)/d * cat.speed * 1.2;
                            const moveY = (targetPos.y - cat.y)/d * cat.speed * 1.2;
                            safeMove(cat, moveX, moveY, currentRadius);
                        } else {
                            if (cat.helpingTarget && cat.helpingTarget.waitingForHelp) {
                                cat.helpingTarget.hungerTimer = CAT_HUNGER_MAX;
                                cat.helpingTarget.waitingForHelp = false;
                                cat.reputation += 2;
                                cat.carryingPrey = null;
                                if (chkLearning.checked) updateLearning(cat, 'rescue', true);
                                addFloatingText(cat.x, cat.y-30, "喂食成功", "#8f8");
                            }
                            cat.helpingTarget = null;
                            cat.isHelping = false;
                            cat.helpTargetPos = null;
                            cat.helpAttemptTimer = 0;
                        }
                    } else {
                        cat.isHelping = false;
                        cat.carryingPrey = null;
                        cat.helpingTarget = null;
                        cat.helpAttemptTimer = 0;
                        if (chkLearning.checked) updateLearning(cat, 'rescue', false);
                    }
                } else {
                    if (chkHuntAI.checked) {
                        cat.targetPrey = null;
                        let nearestPrey = null, minDist = Infinity;
                        for (const prey of preys) {
                            if (prey.isSleeping) continue;
                            const d = distance(cat, prey);
                            if (d > CAT_VIEW_RANGE || isLineBlocked(cat.x, cat.y, prey.x, prey.y)) continue;
                            const angleToTarget = Math.atan2(prey.y - cat.y, prey.x - cat.x);
                            let delta = angleToTarget - cat.angle;
                            while (delta > Math.PI) delta -= 2*Math.PI;
                            while (delta < -Math.PI) delta += 2*Math.PI;
                            if (Math.abs(delta) <= CAT_VIEW_ANGLE/2 && d < minDist) {
                                minDist = d;
                                nearestPrey = prey;
                            }
                        }
                        if (nearestPrey) {
                            cat.targetPrey = nearestPrey;
                            const d = distance(cat, nearestPrey);
                            cat.angle = Math.atan2(nearestPrey.y - cat.y, nearestPrey.x - cat.x);
                            if (d > CAT_HUNT_RANGE) {
                                let moveX, moveY;
                                if (cat.tryFlank) {
                                    const flankOffset = cat.angle + (Math.random()>0.5 ? 0.6 : -0.6);
                                    moveX = Math.cos(flankOffset) * cat.speed;
                                    moveY = Math.sin(flankOffset) * cat.speed;
                                } else {
                                    moveX = (nearestPrey.x - cat.x)/d * cat.speed;
                                    moveY = (nearestPrey.y - cat.y)/d * cat.speed;
                                }
                                safeMove(cat, moveX, moveY, currentRadius);
                            } else {
                                const idx = preys.indexOf(nearestPrey);
                                if (idx > -1) {
                                    const prey = preys.splice(idx,1)[0];
                                    cat.huntCount += 1;
                                    cat.hungerTimer = CAT_HUNGER_MAX;
                                    cat.carryingPrey = prey;
                                    cat.tryFlank = true;
                                    cat.targetPrey = null;
                                    if (chkLearning.checked) updateLearning(cat, 'hunt', true);
                                }
                            }
                        } else {
                            cat.helpAttemptTimer += 1;
                            if (cat.helpAttemptTimer > 300) {
                                if (cat.helpingTarget) {
                                    cat.helpingTarget.waitingForHelp = false;
                                    cat.helpingTarget = null;
                                }
                                cat.isHelping = false;
                                cat.helpAttemptTimer = 0;
                                if (chkLearning.checked) updateLearning(cat, 'rescue', false);
                            } else {
                                cat.wanderTimer -= 1;
                                if (cat.wanderTimer <= 0) {
                                    cat.angle = Math.random()*2*Math.PI;
                                    cat.wanderTimer = 100 + Math.random()*180;
                                }
                                const moveX = Math.cos(cat.angle) * cat.speed * 0.7;
                                const moveY = Math.sin(cat.angle) * cat.speed * 0.7;
                                safeMove(cat, moveX, moveY, currentRadius);
                            }
                        }
                    }
                }
            }
            continue;
        }

        // 如果开启捕猎AI且猫未吃饱,强制捕猎
        if (chkHuntAI.checked && !full && stage !== 0 && !cat.isHelping && !cat.carryingPrey) {
            let nearestPrey = null;
            let minDist = Infinity;
            for (const prey of preys) {
                if (prey.isSleeping) continue;
                const d = distance(cat, prey);
                if (d > CAT_VIEW_RANGE || isLineBlocked(cat.x, cat.y, prey.x, prey.y)) continue;
                const angleToTarget = Math.atan2(prey.y - cat.y, prey.x - cat.x);
                let delta = angleToTarget - cat.angle;
                while (delta > Math.PI) delta -= 2*Math.PI;
                while (delta < -Math.PI) delta += 2*Math.PI;
                if (Math.abs(delta) <= CAT_VIEW_ANGLE/2 && d < minDist) {
                    minDist = d;
                    nearestPrey = prey;
                }
            }

            if (nearestPrey) {
                cat.targetPrey = nearestPrey;
                cat.targetLastKnownPos = { x: nearestPrey.x, y: nearestPrey.y };
                cat.targetLostTimer = 0;
                const d = distance(cat, nearestPrey);
                const angleToPrey = Math.atan2(nearestPrey.y - cat.y, nearestPrey.x - cat.x);
                cat.angle = angleToPrey;
                if (d > CAT_HUNT_RANGE) {
                    let moveX, moveY;
                    if (cat.tryFlank) {
                        const flankOffset = cat.angle + (Math.random()>0.5 ? 0.6 : -0.6);
                        moveX = Math.cos(flankOffset) * cat.speed;
                        moveY = Math.sin(flankOffset) * cat.speed;
                    } else {
                        moveX = Math.cos(angleToPrey) * cat.speed;
                        moveY = Math.sin(angleToPrey) * cat.speed;
                    }
                    safeMove(cat, moveX, moveY, currentRadius);
                } else {
                    const idx = preys.indexOf(nearestPrey);
                    if (idx > -1) {
                        preys.splice(idx, 1);
                        cat.huntCount += 1;
                        cat.hungerTimer = CAT_HUNGER_MAX;
                        if (chkLearning.checked) updateLearning(cat, 'hunt', true);
                    }
                    cat.targetPrey = null;
                    cat.targetLastKnownPos = null;
                }
                continue;
            } else {
                cat.wanderTimer -= 1;
                if (cat.wanderTimer <= 0) {
                    cat.angle = Math.random()*2*Math.PI;
                    cat.wanderTimer = 100 + Math.random()*180;
                }
                const moveX = Math.cos(cat.angle) * cat.speed * 0.7;
                const moveY = Math.sin(cat.angle) * cat.speed * 0.7;
                safeMove(cat, moveX, moveY, currentRadius);
                continue;
            }
        }

        // 吃饱后行为
        if (full) {
            // 求偶
            if (chkMate.checked && canMate) {
                let nearestMate = null;
                let minMateDist = Infinity;
                for (const other of cats) {
                    if (other === cat) continue;
                    if (other.gender === cat.gender) continue;
                    if (other.growTimer < CAT_GROW_TIME) continue;
                    if (other.breedCooldown > 0) continue;
                    if (other.huntCount < CAT_BREED_REQUIRE) continue;
                    if (other.isSleeping) continue;
                    const d = distance(cat, other);
                    if (d > CAT_VIEW_RANGE || isLineBlocked(cat.x, cat.y, other.x, other.y)) continue;
                    const angleToOther = Math.atan2(other.y - cat.y, other.x - cat.x);
                    let delta = angleToOther - cat.angle;
                    while (delta > Math.PI) delta -= 2*Math.PI;
                    while (delta < -Math.PI) delta += 2*Math.PI;
                    if (Math.abs(delta) <= CAT_VIEW_ANGLE/2 && d < minMateDist) {
                        minMateDist = d;
                        nearestMate = other;
                    }
                }
                if (nearestMate) {
                    cat.targetMate = nearestMate;
                    const d = distance(cat, nearestMate);
                    cat.angle = Math.atan2(nearestMate.y - cat.y, nearestMate.x - cat.x);
                    if (d > CAT_BREED_DISTANCE) {
                        const moveX = (nearestMate.x - cat.x)/d * cat.speed;
                        const moveY = (nearestMate.y - cat.y)/d * cat.speed;
                        safeMove(cat, moveX, moveY, currentRadius);
                    } else {
                        const father = cat.gender==="male" ? cat : nearestMate;
                        const mother = cat.gender==="female" ? cat : nearestMate;
                        const success = performMating(father, mother);
                        cat.targetMate = null;
                        if (cat.mate) cat.mate.targetMate = null;
                        if (!success) {
                            cat.breedCooldown = 60;
                            if (chkLearning.checked) updateLearning(cat, 'mate', false);
                        }
                    }
                    continue;
                }
            }

            // 救助
            if (chkRescue.checked) {
                let rescueTarget = null, rescueDist = Infinity;
                for (const other of cats) {
                    if (other === cat) continue;
                    if (other.hungerTimer >= CAT_HUNGER_STATE) continue;
                    if (other.waitingForHelp) continue;
                    const d = distance(cat, other);
                    if (d > CAT_VIEW_RANGE || isLineBlocked(cat.x, cat.y, other.x, other.y)) continue;
                    const angleToOther = Math.atan2(other.y - cat.y, other.x - cat.x);
                    let delta = angleToOther - cat.angle;
                    while (delta > Math.PI) delta -= 2*Math.PI;
                    while (delta < -Math.PI) delta += 2*Math.PI;
                    if (Math.abs(delta) <= CAT_VIEW_ANGLE/2 && d < rescueDist) {
                        rescueDist = d;
                        rescueTarget = other;
                    }
                }
                if (rescueTarget) {
                    cat.isHelping = true;
                    cat.helpingTarget = rescueTarget;
                    cat.helpTargetPos = { x: rescueTarget.x, y: rescueTarget.y };
                    cat.helpAttemptTimer = 0;
                    rescueTarget.waitingForHelp = true;
                    addFloatingText(cat.x, cat.y-30, "在这等着。", "#ffd700");
                    continue;
                }
            }

            // 筑巢
            if (chkNest.checked && !cat.nest && stage===2) {
                if (Math.random() < 0.5) {
                    if (tryBuildNest(cat)) continue;
                }
            }

            // 闲逛
            cat.wanderTimer -= 1;
            if (cat.wanderTimer <= 0) {
                cat.angle = Math.random()*2*Math.PI;
                cat.wanderTimer = 100 + Math.random()*180;
            }
            const moveX = Math.cos(cat.angle) * cat.speed * 0.7;
            const moveY = Math.sin(cat.angle) * cat.speed * 0.7;
            safeMove(cat, moveX, moveY, currentRadius);
        } else {
            // 未吃饱且无捕猎AI,闲逛
            cat.wanderTimer -= 1;
            if (cat.wanderTimer <= 0) {
                cat.angle = Math.random()*2*Math.PI;
                cat.wanderTimer = 100 + Math.random()*180;
            }
            const moveX = Math.cos(cat.angle) * cat.speed * 0.7;
            const moveY = Math.sin(cat.angle) * cat.speed * 0.7;
            safeMove(cat, moveX, moveY, currentRadius);
        }
    }

    // 猫繁殖
    if (chkMate.checked && chkHuntAI.checked) {
        // 已在上面的求偶中处理
    }

    // 更新浮动文字
    for (let i=floatingTexts.length-1; i>=0; i--) {
        const ft = floatingTexts[i];
        ft.life -= 1;
        ft.alpha = ft.life / ft.maxLife;
        if (ft.life <= 0) floatingTexts.splice(i,1);
    }

    updateUI();
}

function performMating(father, mother) {
    if (cats.length >= MAX_CATS) return false;
    if (father.gender === mother.gender) return false;
    if (father.growTimer < CAT_GROW_TIME || mother.growTimer < CAT_GROW_TIME) return false;
    if (father.breedCooldown > 0 || mother.breedCooldown > 0) return false;
    if (father.huntCount < CAT_BREED_REQUIRE || mother.huntCount < CAT_BREED_REQUIRE) return false;
    father.mate = mother;
    mother.mate = father;
    const kittens = [];
    for (let k=0; k<CAT_LITTER_SIZE; k++) {
        let sx = (father.x + mother.x)/2 + (Math.random()-0.5)*45;
        let sy = (father.y + mother.y)/2 + (Math.random()-0.5)*45;
        if (checkCollision(sx, sy, CAT_RADIUS*0.4)) { sx += 35; sy += 35; }
        kittens.push(createKitten(sx, sy, father, mother));
    }
    cats.push(...kittens);
    mother.isLactating = true;
    mother.milkSupply = 20;
    father.kittens.push(...kittens);
    mother.kittens.push(...kittens);
    father.huntCount = 0;
    mother.huntCount = 0;
    father.breedCooldown = CAT_BREED_COOLDOWN;
    mother.breedCooldown = CAT_BREED_COOLDOWN;
    father.targetMate = null;
    mother.targetMate = null;
    if (chkLearning.checked) {
        updateLearning(father, 'mate', true);
        updateLearning(mother, 'mate', true);
    }
    return true;
}

function tryBuildNest(cat) {
    if (cat.hungerTimer < CAT_FULL_THRESHOLD) return false;
    if (cat.isHelping || cat.targetMate || cat.isSleeping) return false;
    if (cat.buildNestCooldown > 0) return false;
    if (cat.nest) return false;
    let treeCount = 0, thornCount = 0;
    for (const tree of trees) {
        if (distance(cat, tree) < NEST_CHECK_RADIUS) treeCount++;
    }
    for (const thorn of thorns) {
        if (distance(cat, thorn) < NEST_CHECK_RADIUS) thornCount++;
    }
    if (treeCount < 1 || thornCount < 1) {
        if (chkLearning.checked) updateLearning(cat, 'nest', false);
        return false;
    }
    let bestX = cat.x, bestY = cat.y;
    let bestScore = 0;
    for (let attempt=0; attempt<20; attempt++) {
        const angle = Math.random()*2*Math.PI;
        const dist = 20 + Math.random()*40;
        const tx = cat.x + Math.cos(angle)*dist;
        const ty = cat.y + Math.sin(angle)*dist;
        if (!isPointInWorld(tx, ty)) continue;
        if (checkCollision(tx, ty, CAT_RADIUS)) continue;
        let score = 0;
        for (const nest of nests) {
            const d = distance({x:tx,y:ty}, nest);
            if (d < NEST_PREFER_RADIUS) score += (NEST_PREFER_RADIUS - d)/NEST_PREFER_RADIUS;
        }
        score += Math.random()*0.5;
        if (score > bestScore) {
            bestScore = score;
            bestX = tx; bestY = ty;
        }
    }
    const nest = { x: bestX, y: bestY, owner: cat };
    nests.push(nest);
    cat.nest = nest;
    cat.buildNestCooldown = 600;
    addFloatingText(bestX, bestY-20, cat.name+" 建了窝", "#88dd88");
    if (chkLearning.checked) updateLearning(cat, 'nest', true);
    return true;
}

function addFloatingText(x, y, text, color = "#ffd700") {
    floatingTexts.push({ x, y, text, color, life: 60, maxLife: 60, alpha: 1.0 });
}

function updateUI() {
    preyCountSpan.textContent = preys.length;
    catCountSpan.textContent = cats.length;
    herbCountSpan.textContent = herbs.length;
    frameCountSpan.textContent = frame;
}

// ---------- 渲染 ----------
function render() {
    ctx.clearRect(0, 0, 800, 600);

    ctx.save();
    ctx.translate(offsetX, offsetY);
    ctx.scale(scale, scale);

    // 网格
    ctx.setLineDash(DASH_PATTERN);
    ctx.strokeStyle = "rgba(255,255,255,0.12)";
    ctx.lineWidth = 1;
    for (let x=0; x<800; x+=GRID_SIZE) {
        ctx.beginPath();
        ctx.moveTo(x, 0);
        ctx.lineTo(x, 600);
        ctx.stroke();
    }
    for (let y=0; y<600; y+=GRID_SIZE) {
        ctx.beginPath();
        ctx.moveTo(0, y);
        ctx.lineTo(800, y);
        ctx.stroke();
    }
    ctx.setLineDash([]);

    // 世界边界
    ctx.strokeStyle = "#ff0000";
    ctx.lineWidth = 2;
    ctx.setLineDash([4,4]);
    ctx.strokeRect(WORLD_BOUND.left, WORLD_BOUND.top, WORLD_BOUND.right-WORLD_BOUND.left, WORLD_BOUND.bottom-WORLD_BOUND.top);
    ctx.setLineDash([]);

    // 植物
    ctx.fillStyle = HERB_COLOR;
    for (const herb of herbs) {
        const x = herb.x, y = herb.y;
        ctx.beginPath();
        ctx.moveTo(x, y - HERB_SIZE);
        ctx.lineTo(x - HERB_SIZE*0.7, y + HERB_SIZE*0.6);
        ctx.lineTo(x + HERB_SIZE*0.7, y + HERB_SIZE*0.6);
        ctx.closePath();
        ctx.fill();
    }

    // 灌木
    ctx.fillStyle = "rgba(0,80,0,0.3)";
    for (const bush of bushes) {
        ctx.beginPath();
        ctx.arc(bush.x, bush.y, BUSH_RADIUS, 0, 2*Math.PI);
        ctx.fill();
        ctx.strokeStyle = "rgba(0,200,0,0.3)";
        ctx.lineWidth = 1;
        ctx.setLineDash([3,3]);
        ctx.stroke();
        ctx.setLineDash([]);
    }

    // 荆棘
    ctx.fillStyle = "#1a4a1a";
    for (const thorn of thorns) {
        ctx.beginPath();
        ctx.arc(thorn.x, thorn.y, THORN_RADIUS, 0, 2*Math.PI);
        ctx.fill();
        ctx.strokeStyle = "#000";
        ctx.lineWidth = 2;
        const off = THORN_RADIUS * 0.6;
        ctx.beginPath();
        ctx.moveTo(thorn.x - off, thorn.y - off);
        ctx.lineTo(thorn.x + off, thorn.y + off);
        ctx.moveTo(thorn.x + off, thorn.y - off);
        ctx.lineTo(thorn.x - off, thorn.y + off);
        ctx.stroke();
    }

    // 树木
    ctx.fillStyle = TREE_COLOR;
    for (const tree of trees) {
        ctx.beginPath();
        ctx.arc(tree.x, tree.y, TREE_RADIUS, 0, 2*Math.PI);
        ctx.fill();
    }

    // 猎物
    for (const prey of preys) {
        const color = prey.eatenHerbs >= FEED_REQUIRE ? (prey.targetMate ? "#555" : "#222") : "#111";
        ctx.fillStyle = color;
        ctx.beginPath();
        ctx.arc(prey.x, prey.y, PREY_RADIUS, 0, 2*Math.PI);
        ctx.fill();
        if (prey.isSleeping) {
            ctx.fillStyle = "#ccc";
            ctx.font = "10px system-ui";
            ctx.textAlign = "center";
            ctx.textBaseline = "bottom";
            ctx.fillText("Z", prey.x, prey.y - PREY_RADIUS - 2);
        }
    }

    // 猫
    for (const cat of cats) {
        const progress = Math.min(1, cat.growTimer / CAT_GROW_TIME);
        let sf = 1;
        if (progress < 0.33) sf = 0.4;
        else if (progress < 0.66) sf = 0.7;
        else sf = 1.0;
        const r = CAT_RADIUS * sf;
        const isAdult = progress >= 1;
        const full = cat.hungerTimer > CAT_FULL_THRESHOLD;
        const canBreed = isAdult && cat.huntCount >= CAT_BREED_REQUIRE && cat.breedCooldown === 0 && full;

        if (canBreed) {
            ctx.beginPath();
            ctx.strokeStyle = "#ff88bb";
            ctx.lineWidth = 2;
            ctx.arc(cat.x, cat.y, r + 5, 0, 2*Math.PI);
            ctx.stroke();
        }
        if (cat.hungerTimer < CAT_HUNGER_STATE) {
            ctx.beginPath();
            ctx.strokeStyle = "rgba(255,0,0,0.6)";
            ctx.lineWidth = 2;
            ctx.arc(cat.x, cat.y, r + 4, 0, 2*Math.PI);
            ctx.stroke();
        }

        let color = CAT_COLOR;
        if (cat.carryingPrey) color = "#3a86ff";
        if (cat.waitingForHelp) color = "#ffaa44";
        if (cat.isSleeping) color = "#555555";
        ctx.fillStyle = color;
        ctx.beginPath();
        ctx.arc(cat.x, cat.y, r, 0, 2*Math.PI);
        ctx.fill();

        ctx.fillStyle = "#995511";
        ctx.beginPath();
        ctx.arc(cat.x + Math.cos(cat.angle)*(r-2), cat.y + Math.sin(cat.angle)*(r-2), 2.5, 0, 2*Math.PI);
        ctx.fill();

        const symbol = cat.gender === "male" ? MALE_SYMBOL : FEMALE_SYMBOL;
        const text = `(${symbol}) ${cat.name}`;
        ctx.fillStyle = "#fff";
        ctx.font = "10px system-ui";
        ctx.textAlign = "center";
        ctx.textBaseline = "bottom";
        ctx.shadowColor = "rgba(0,0,0,0.8)";
        ctx.shadowBlur = 4;
        ctx.fillText(text, cat.x, cat.y - r - 4);
        ctx.shadowBlur = 0;

        if (cat.isSleeping) {
            ctx.fillStyle = "#aaddff";
            ctx.font = "14px system-ui";
            ctx.textAlign = "center";
            ctx.textBaseline = "bottom";
            ctx.fillText("💤", cat.x, cat.y - r - 14);
        }
    }

    // 窝
    for (const nest of nests) {
        ctx.setLineDash([4,4]);
        ctx.strokeStyle = "#ffffff";
        ctx.lineWidth = 1.5;
        ctx.beginPath();
        ctx.arc(nest.x, nest.y, CAT_RADIUS+3, 0, 2*Math.PI);
        ctx.stroke();
        ctx.setLineDash([]);
        ctx.fillStyle = "#ddd";
        ctx.font = "9px system-ui";
        ctx.textAlign = "center";
        ctx.textBaseline = "bottom";
        ctx.fillText(nest.owner.name + " 的窝", nest.x, nest.y - CAT_RADIUS - 6);
    }

    // 浮动文字
    for (const ft of floatingTexts) {
        ctx.globalAlpha = ft.alpha;
        ctx.fillStyle = ft.color || "#ffd700";
        ctx.font = "bold 16px system-ui";
        ctx.textAlign = "center";
        ctx.textBaseline = "bottom";
        ctx.shadowColor = "rgba(0,0,0,0.8)";
        ctx.shadowBlur = 6;
        ctx.fillText(ft.text, ft.x, ft.y);
        ctx.shadowBlur = 0;
    }
    ctx.globalAlpha = 1;

    // 放置预览
    if (isDragging && mouseDownPos) {
        const mx = mouseDownPos.x, my = mouseDownPos.y;
        ctx.strokeStyle = "#fff";
        ctx.lineWidth = 1;
        ctx.setLineDash([2,2]);
        ctx.beginPath();
        ctx.arc(mx, my, 12, 0, 2*Math.PI);
        ctx.stroke();
        ctx.setLineDash([]);
        ctx.fillStyle = "rgba(255,255,255,0.5)";
        ctx.font = "12px system-ui";
        ctx.textAlign = "center";
        ctx.textBaseline = "bottom";
        ctx.fillText("放置", mx, my - 14);
    }

    ctx.restore();
}

// ---------- 游戏循环 ----------
function gameLoop() {
    frame++;
    updateWorld();
    render();
    requestAnimationFrame(gameLoop);
}

// ---------- 交互事件 ----------
// 视角拖拽
canvas.addEventListener('mousedown', (e) => {
    const rect = canvas.getBoundingClientRect();
    const scaleX = canvas.width / rect.width;
    const scaleY = canvas.height / rect.height;
    const x = (e.clientX - rect.left) * scaleX;
    const y = (e.clientY - rect.top) * scaleY;

    // 检查是否在工具放置模式(点击左键放置)
    if (e.button === 0) {
        // 如果按下鼠标时没有按住工具键,则视为视角拖拽
        // 但我们希望区分:左键用于放置,中键或右键用于拖拽?但用户可能期望左键拖拽。
        // 我们用左键拖拽,但保留放置功能:按住 Ctrl 或 Shift 放置?简化:左键拖拽视角,点击放置?
        // 更好的方案:点击放置,拖拽平移。所以判断是否发生了拖拽。
        mouseDownPos = { x, y };
        isDragging = false; // 重置
        isViewDragging = true;
        dragStartX = x;
        dragStartY = y;
        dragStartOffsetX = offsetX;
        dragStartOffsetY = offsetY;
        canvas.style.cursor = 'grabbing';
    }
});

canvas.addEventListener('mousemove', (e) => {
    const rect = canvas.getBoundingClientRect();
    const scaleX = canvas.width / rect.width;
    const scaleY = canvas.height / rect.height;
    const x = (e.clientX - rect.left) * scaleX;
    const y = (e.clientY - rect.top) * scaleY;

    if (isViewDragging) {
        const dx = x - dragStartX;
        const dy = y - dragStartY;
        offsetX = dragStartOffsetX + dx;
        offsetY = dragStartOffsetY + dy;
    }
    // 更新预览位置
    if (isDragging && mouseDownPos) {
        mouseDownPos = { x, y };
    }
});

canvas.addEventListener('mouseup', (e) => {
    const rect = canvas.getBoundingClientRect();
    const scaleX = canvas.width / rect.width;
    const scaleY = canvas.height / rect.height;
    const x = (e.clientX - rect.left) * scaleX;
    const y = (e.clientY - rect.top) * scaleY;

    if (isViewDragging) {
        isViewDragging = false;
        canvas.style.cursor = 'grab';
        // 如果几乎没有移动,视为点击,执行放置
        const dx = x - dragStartX;
        const dy = y - dragStartY;
        if (Math.hypot(dx, dy) < 5) {
            // 点击放置
            const type = activeTool;
            // 转换世界坐标到屏幕坐标(因为offset/scale)
            const wx = (x - offsetX) / scale;
            const wy = (y - offsetY) / scale;
            if (placeEntity(type, wx, wy)) {
                placeInfo.textContent = `放置了 ${type}`;
            } else {
                placeInfo.textContent = `放置失败`;
            }
        }
    }
    isDragging = false;
    mouseDownPos = null;
});

canvas.addEventListener('mouseleave', () => {
    isViewDragging = false;
    isDragging = false;
    mouseDownPos = null;
    canvas.style.cursor = 'grab';
});

// 滚轮缩放
canvas.addEventListener('wheel', (e) => {
    e.preventDefault();
    const rect = canvas.getBoundingClientRect();
    const scaleX = canvas.width / rect.width;
    const scaleY = canvas.height / rect.height;
    const mx = (e.clientX - rect.left) * scaleX;
    const my = (e.clientY - rect.top) * scaleY;

    // 缩放中心
    const before = { x: (mx - offsetX) / scale, y: (my - offsetY) / scale };
    const delta = e.deltaY > 0 ? -0.1 : 0.1;
    scale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale + delta));
    const after = { x: (mx - offsetX) / scale, y: (my - offsetY) / scale };
    offsetX += (after.x - before.x) * scale;
    offsetY += (after.y - before.y) * scale;
}, { passive: false });

// 触摸事件
canvas.addEventListener('touchstart', (e) => {
    e.preventDefault();
    const touch = e.touches[0];
    const rect = canvas.getBoundingClientRect();
    const scaleX = canvas.width / rect.width;
    const scaleY = canvas.height / rect.height;
    const x = (touch.clientX - rect.left) * scaleX;
    const y = (touch.clientY - rect.top) * scaleY;

    if (e.touches.length === 1) {
        isViewDragging = true;
        dragStartX = x;
        dragStartY = y;
        dragStartOffsetX = offsetX;
        dragStartOffsetY = offsetY;
        mouseDownPos = { x, y };
    }
}, { passive: false });

canvas.addEventListener('touchmove', (e) => {
    e.preventDefault();
    if (e.touches.length === 1) {
        const touch = e.touches[0];
        const rect = canvas.getBoundingClientRect();
        const scaleX = canvas.width / rect.width;
        const scaleY = canvas.height / rect.height;
        const x = (touch.clientX - rect.left) * scaleX;
        const y = (touch.clientY - rect.top) * scaleY;

        if (isViewDragging) {
            const dx = x - dragStartX;
            const dy = y - dragStartY;
            offsetX = dragStartOffsetX + dx;
            offsetY = dragStartOffsetY + dy;
        }
        mouseDownPos = { x, y };
    } else if (e.touches.length === 2) {
        // 双指缩放(简化)
        // 省略,保持简单
    }
}, { passive: false });

canvas.addEventListener('touchend', (e) => {
    e.preventDefault();
    if (isViewDragging && mouseDownPos) {
        // 检查是否放置
        const rect = canvas.getBoundingClientRect();
        const touch = e.changedTouches[0];
        const scaleX = canvas.width / rect.width;
        const scaleY = canvas.height / rect.height;
        const x = (touch.clientX - rect.left) * scaleX;
        const y = (touch.clientY - rect.top) * scaleY;
        const dx = x - dragStartX;
        const dy = y - dragStartY;
        if (Math.hypot(dx, dy) < 10) {
            // 点击放置
            const type = activeTool;
            const wx = (x - offsetX) / scale;
            const wy = (y - offsetY) / scale;
            if (placeEntity(type, wx, wy)) {
                placeInfo.textContent = `放置了 ${type}`;
            } else {
                placeInfo.textContent = `放置失败`;
            }
        }
    }
    isViewDragging = false;
    mouseDownPos = null;
}, { passive: false });

// 工具切换
function setTool(tool) {
    activeTool = tool;
    document.querySelectorAll('.tools button').forEach(b => b.classList.remove('active'));
    if (tool === 'cat') toolCat.classList.add('active');
    else if (tool === 'prey') toolPrey.classList.add('active');
    else if (tool === 'bush') toolBush.classList.add('active');
    else if (tool === 'thorn') toolThorn.classList.add('active');
    else if (tool === 'tree') toolTree.classList.add('active');
    placeInfo.textContent = `当前工具: ${tool}`;
}

toolCat.addEventListener('click', () => setTool('cat'));
toolPrey.addEventListener('click', () => setTool('prey'));
toolBush.addEventListener('click', () => setTool('bush'));
toolThorn.addEventListener('click', () => setTool('thorn'));
toolTree.addEventListener('click', () => setTool('tree'));

// 重置
btnReset.addEventListener('click', () => {
    initScene();
    placeInfo.textContent = '场景已重置';
});

btnClear.addEventListener('click', () => {
    trees = []; herbs = []; preys = []; cats = [];
    bushes = []; thorns = []; nests = [];
    floatingTexts = [];
    herbRespawnTimer = 0;
    isDay = true;
    dayTimer = 0; nightTimer = 0;
    updateUI();
    placeInfo.textContent = '已清空所有';
});

btnResetView.addEventListener('click', () => {
    offsetX = 0; offsetY = 0; scale = 1.0;
});

// 初始化
initScene();
gameLoop();

</script>
</body>
</html>

Game Source: 猫武士 · 调试场地 (改进版)

Creator: EpicCoder88

Libraries: none

Complexity: complex (1833 lines, 69.0 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-msmufsjd" to link back to the original. Then publish at arcadelab.ai/publish.