🎮ArcadeLab

猫武士 · 演替兴衰(掩护学习版)

by EpicCoder88
2086 lines76.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>猫武士 · 演替兴衰(掩护学习版)</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;
            overflow: hidden;
            touch-action: none;
            user-select: none;
            height: 100vh;
        }
        canvas {
            border: 2px solid #444;
            border-radius: 6px;
            touch-action: none;
            max-width: 100%;
            max-height: 70vh;
            background: #477c42;
            display: block;
        }
        .panel {
            margin-top: 8px;
            width: 100%;
            max-width: 900px;
            background: #1a1a1a;
            border: 2px solid #444;
            border-radius: 6px;
            padding: 8px 12px;
            color: #ddd;
            font-size: 13px;
            line-height: 1.5;
        }
        .panel strong { color: #fff; }
        #detailPanel {
            margin-top: 4px;
            border-top: 1px solid #444;
            padding-top: 4px;
            font-size: 12px;
            color: #aaa;
            min-height: 1.2em;
        }
        .controls {
            margin-top: 6px;
            display: flex;
            flex-wrap: wrap;
            gap: 6px;
            align-items: center;
        }
        .controls button {
            background: #333;
            color: #ddd;
            border: 1px solid #555;
            border-radius: 4px;
            padding: 4px 10px;
            font-size: 13px;
            cursor: pointer;
        }
        .controls button.active {
            background: #5a7a5a;
            border-color: #8aa88a;
        }
        .controls .reset-btn {
            background: #5a3a3a;
            border-color: #8a5a5a;
        }
        #timeDisplay {
            color: #ffd700;
            font-weight: bold;
            margin-left: 6px;
            font-size: 14px;
        }
        .info {
            font-size: 11px;
            color: #888;
            margin-top: 3px;
        }
        #resetNotice {
            position: fixed;
            top: 20px;
            left: 50%;
            transform: translateX(-50%);
            background: rgba(0,0,0,0.8);
            color: #ffd700;
            padding: 8px 20px;
            border-radius: 8px;
            font-size: 18px;
            font-weight: bold;
            pointer-events: none;
            opacity: 0;
            transition: opacity 0.5s;
            z-index: 999;
        }
    </style>
</head>
<body>
    <div id="resetNotice">演替已经重新开启</div>
    <canvas id="worldCanvas" width="900" height="600"></canvas>

    <div class="panel">
        <div><strong>生态实时面板</strong></div>
        <div id="populationPanel">猎物:0 / 草:0 / 成年猫:0 / 幼猫:0 / 跟随:无</div>
        <div id="detailPanel">点击一只猫查看详情</div>
        <div class="controls">
            <span style="color:#888; font-size:13px;">速度:</span>
            <button id="speed1x" class="active" onclick="setSpeed(1)">1×</button>
            <button id="speed2x" onclick="setSpeed(2)">2×</button>
            <button id="speed5x" onclick="setSpeed(5)">5×</button>
            <button class="reset-btn" onclick="resetWorld()">重置</button>
            <span id="timeDisplay">☀️ 白天</span>
        </div>
        <div class="info">
            点击猫跟随|拖拽平移|双指缩放|红圈=饥饿|💤=睡觉|粉色圈=吃饱且可繁殖|未吃饱猫会利用灌木掩护捕猎
        </div>
    </div>

<script>
// ============================================================
// 猫武士 · 演替兴衰  掩护学习版
// 新增:猫会根据经验选择直接追击或利用灌木掩护捕猎
// ============================================================

const canvas = document.getElementById("worldCanvas");
const ctx = canvas.getContext("2d");
const populationPanel = document.getElementById("populationPanel");
const detailPanel = document.getElementById("detailPanel");
const timeDisplay = document.getElementById("timeDisplay");
const resetNotice = document.getElementById("resetNotice");

// ---------- 名字库 ----------
const PREFIXES = [
    "虎", "鹰", "鸦", "松鸦", "香薇", "冬青", "蕨", "玫瑰", "火", "云", "雾", "风",
    "灰", "蓝", "斑", "长", "短", "亮", "暗", "雨", "雪", "霜", "露", "溪", "河",
    "湖", "星", "月", "日", "影", "光", "叶", "花", "石", "岩", "荆棘", "芦苇",
    "柳", "橡", "枫", "桦", "松", "杉", "白", "黑", "红", "金", "银", "铜"
];
const SUFFIXES = [
    "尾", "羽", "花", "毛", "爪", "足", "心", "风", "叶", "霜", "云", "星",
    "光", "月", "溪", "河", "湖", "石", "岩", "棘", "苇", "柳", "橡", "枫",
    "桦", "松", "杉", "飞", "跃", "奔", "步", "啸", "嚎", "眼", "耳", "鼻",
    "须", "掌", "腿", "腹", "背", "额", "面", "斑", "纹", "环"
];

// ---------- 配置 ----------
const GRID_SIZE = 60;
const GRID_ALPHA = 0.35;
const DASH_PATTERN = [8,6];
const GRID_COLOR = "#ffffff";
const GRASS_COLOR = "#477c42";

const TREE_COLOR = "#6b4423";
const TREE_RADIUS = 22;
const TREE_COLLIDE_RADIUS = 28;
const TREE_BLOCK_RAY_RADIUS = 26;
const TREE_MIN_DISTANCE = 110;
const TREE_TOTAL_COUNT = 40;

const HERB_COLOR = "#1c4d19";
const HERB_SIZE = 10;
const HERB_MAX_COUNT = 160;
const HERB_MIN_SPACE = 35;
const HERB_RESPAWN_INTERVAL = 160;

const PREY_RADIUS = 8;
const PREY_COUNT = 60;
const PREY_MAX_COUNT = 150;
const PREY_SPEED = 1.1;
const PREY_FLEE_SPEED = 3.0;
const PREY_VIEW_RANGE = 170;
const PREY_VIEW_ANGLE = Math.PI / 3;
const PREY_FEED_RANGE = 16;
const BREED_DISTANCE = 35;
const FEED_REQUIRE = 3;
const BABY_COUNT = 4;
const MAX_HUNGER_TIME = 2500;

const CAT_COLOR = "#f2a65a";
const CAT_RADIUS = 14;
const CAT_SPEED = 1.4;
const CAT_VIEW_RANGE = 220;
const CAT_VIEW_ANGLE = Math.PI / 3;
const CAT_HUNT_RANGE = 25;
const REWARD_CATCH = 10;
const PENALTY_ESCAPE = -6;
const CAT_BREED_REQUIRE = 3;
const CAT_BREED_DISTANCE = 40;
const CAT_BREED_COOLDOWN = 800;
const CAT_LITTER_SIZE = 2;
const CAT_GROW_TIME = 1800;
const CAT_HUNGER_MAX = 5000;
const CAT_HUNGER_STATE = 600;
const CAT_FULL_THRESHOLD = 2000;
const MAX_CATS = 120;

// 昼夜时长(帧数)
const DAY_LENGTH = 2400;
const NIGHT_LENGTH = 1800;

// 灌木和荆棘
const BUSH_COUNT = 80;
const BUSH_RADIUS = 30;
const THORN_COUNT = 50;
const THORN_RADIUS = 22;

// 窝相关
const NEST_CHECK_RADIUS = 80;
const NEST_PREFER_RADIUS = 120;

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

// 大世界边界
const WORLD_BOUND = {
    left: -600,
    top: -400,
    right: 3000,
    bottom: 2000
};

// ---------- 相机 ----------
let offsetX = 0, offsetY = 0, scale = 0.6;
const MIN_SCALE = 0.2, MAX_SCALE = 2.5;
let isDragging = false;
let dragStartX = 0, dragStartY = 0;
let startOffsetX = 0, startOffsetY = 0;
let lastTouchDist = 0;
let followedCat = null;

let pointerDownX = 0, pointerDownY = 0;
let pointerDownTime = 0;
let isClick = false;

let speedMultiplier = 1;

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

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

let resetNoticeTimer = 0;
const RESET_NOTICE_DURATION = 180;

// ---------- 工具函数 ----------
function isPointInWorld(x, y) {
    return x >= WORLD_BOUND.left && x <= WORLD_BOUND.right &&
           y >= WORLD_BOUND.top && y <= WORLD_BOUND.bottom;
}

function screenToWorld(screenX, screenY) {
    return { x: (screenX - offsetX) / scale, y: (screenY - offsetY) / scale };
}

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

function getPixelRatio() {
    const rect = canvas.getBoundingClientRect();
    return canvas.width / rect.width;
}

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 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 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 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: {
            hungry: { attempts: 0, success: 0 },
            normal: { attempts: 0, success: 0 },
            full: { attempts: 0, success: 0 }
        },
        rescue: { attempts: 0, success: 0 },
        mate: { attempts: 0, success: 0 },
        nest: { attempts: 0, success: 0 },
        cover: { attempts: 0, success: 0 } // 新增:掩护捕猎经验
    };
}

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

function getUtility(success, attempts, baseWeight = 0.5) {
    if (attempts === 0) return baseWeight + 0.2 * Math.random();
    const rate = success / attempts;
    const exploration = 1 / (1 + attempts * 0.05);
    return rate * (1 - exploration * 0.3) + baseWeight * exploration * 0.3 + 0.1 * Math.random();
}

function getHuntUtility(cat) {
    const state = cat.hungerTimer < CAT_HUNGER_STATE ? 'hungry' :
                  cat.hungerTimer > CAT_FULL_THRESHOLD ? 'full' : 'normal';
    const exp = cat.learning.hunt[state];
    let baseWeight = 0.9;
    if (state === 'hungry') baseWeight = 1.0;
    else if (state === 'full') baseWeight = 0.2;
    return getUtility(exp.success, exp.attempts, baseWeight);
}

function getRescueUtility(cat) {
    const exp = cat.learning.rescue;
    return getUtility(exp.success, exp.attempts, 0.6);
}

function getMateUtility(cat) {
    if (cat.huntCount < CAT_BREED_REQUIRE) return 0;
    if (cat.hungerTimer < CAT_FULL_THRESHOLD) return 0;
    const exp = cat.learning.mate;
    return getUtility(exp.success, exp.attempts, 0.4);
}

function getNestUtility(cat) {
    if (cat.nest) return 0;
    if (cat.hungerTimer < CAT_FULL_THRESHOLD) return 0;
    const exp = cat.learning.nest;
    return getUtility(exp.success, exp.attempts, 0.9);
}

function getWanderUtility() {
    return 0.2 + 0.1 * Math.random();
}

// ---------- 掩护捕猎辅助函数 ----------
// 寻找最佳掩护灌木
function findBestCoverBush(catPos, preyPos) {
    let best = null;
    let bestScore = -Infinity;
    for (const bush of bushes) {
        const dCat = distance(catPos, bush);
        const dPrey = distance(bush, preyPos);
        // 灌木到猎物距离要近(便于突袭),但也不能太近(暴露)
        if (dPrey > 90 || dPrey < 20) continue;
        // 灌木到猫的距离不能太远(猫能到达)
        if (dCat > 250) continue;
        // 从灌木内部看猎物,猎物不能被其他灌木遮挡(确保视角良好)
        if (isLineBlocked(bush.x, bush.y, preyPos.x, preyPos.y)) continue;
        // 得分:距离猎物近,距离猫适中,且在两者连线上
        const score = 60 - dPrey + (80 - dCat)*0.3;
        if (score > bestScore) {
            bestScore = score;
            best = bush;
        }
    }
    return best;
}

// ---------- 生成函数 ----------
function generateTrees() {
    trees = [];
    let tries = 0;
    while (trees.length < TREE_TOTAL_COUNT && tries < TREE_TOTAL_COUNT*20) {
        tries++;
        const x = WORLD_BOUND.left + 50 + Math.random()*(WORLD_BOUND.right - WORLD_BOUND.left - 100);
        const y = WORLD_BOUND.top + 50 + Math.random()*(WORLD_BOUND.bottom - WORLD_BOUND.top - 100);
        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 = [
            [0,0], [300,200], [-100,100], [500,-50], [200,400],
            [-400,-200], [600,300], [-200,500], [800,0], [400,-300]
        ];
        for (const [x,y] of fallback) trees.push({x,y});
    }
}

function trySpawnHerb() {
    if (herbs.length >= HERB_MAX_COUNT) return false;
    for (let i=0; i<30; i++) {
        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 + 10) { 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}); return true; }
    }
    return false;
}

function generateHerbs() {
    herbs = [];
    let count = 0;
    while (count < HERB_MAX_COUNT && count < 200) {
        if (trySpawnHerb()) count++;
        else break;
    }
    if (herbs.length < 20) {
        for (let i=0; i<40; i++) {
            herbs.push({
                x: WORLD_BOUND.left + 30 + Math.random()*(WORLD_BOUND.right - WORLD_BOUND.left - 60),
                y: WORLD_BOUND.top + 30 + Math.random()*(WORLD_BOUND.bottom - WORLD_BOUND.top - 60)
            });
        }
    }
}

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 generatePreys() {
    preys = [];
    let tries = 0;
    while (preys.length < PREY_COUNT && tries < PREY_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));
    }
    if (preys.length < 10) {
        for (let i=0; i<20; i++) {
            preys.push(createPrey(
                WORLD_BOUND.left + 40 + Math.random()*(WORLD_BOUND.right - WORLD_BOUND.left - 80),
                WORLD_BOUND.top + 40 + Math.random()*(WORLD_BOUND.bottom - WORLD_BOUND.top - 80)
            ));
        }
    }
}

function generateBushes() {
    bushes = [];
    let tries = 0;
    while (bushes.length < BUSH_COUNT && tries < BUSH_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 + BUSH_RADIUS) { ok = false; break; }
        if (!ok) continue;
        for (const b of bushes) if (distance({x,y}, b) < BUSH_RADIUS*1.8) { ok = false; break; }
        if (ok) bushes.push({x,y});
    }
    if (bushes.length < 10) {
        for (let i=0; i<20; i++) {
            bushes.push({
                x: WORLD_BOUND.left + 30 + Math.random()*(WORLD_BOUND.right - WORLD_BOUND.left - 60),
                y: WORLD_BOUND.top + 30 + Math.random()*(WORLD_BOUND.bottom - WORLD_BOUND.top - 60)
            });
        }
    }
}

function generateThorns() {
    thorns = [];
    let tries = 0;
    while (thorns.length < THORN_COUNT && tries < THORN_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 + 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});
    }
    if (thorns.length < 5) {
        for (let i=0; i<10; i++) {
            thorns.push({
                x: WORLD_BOUND.left + 30 + Math.random()*(WORLD_BOUND.right - WORLD_BOUND.left - 60),
                y: WORLD_BOUND.top + 30 + Math.random()*(WORLD_BOUND.bottom - WORLD_BOUND.top - 60)
            });
        }
    }
}

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

function generateName() {
    return PREFIXES[Math.floor(Math.random()*PREFIXES.length)] +
           SUFFIXES[Math.floor(Math.random()*SUFFIXES.length)];
}

function createCat(x, y, isAdult = true) {
    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(),
        // 掩护捕猎状态
        huntMode: null,        // 'direct' 或 'cover'
        targetBush: null,      // 掩护目标灌木
        coverPhase: null,      // 'approach', 'stalk', 'attack'
        coverStartPos: null    // 记录进入灌木前位置
    };
}

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();
    cat.learning = createLearning();
    return cat;
}

function generateCats() {
    cats = [];
    let tries = 0;
    while (cats.length < 10 && tries < 10*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));
    }
    if (cats.length < 2) {
        const fallback = [[0,0], [300,300]];
        for (const [x,y] of fallback) cats.push(createCat(x,y));
    }
}

// ---------- 交配 ----------
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;
    updateLearning(father, 'mate', undefined, true);
    updateLearning(mother, 'mate', undefined, true);
    return true;
}

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

function updateFloatingTexts() {
    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);
    }
}

// ---------- 昼夜管理 ----------
function updateDayNight() {
    if (isDay) {
        dayTimer++;
        if (dayTimer >= DAY_LENGTH) {
            isDay = false;
            dayTimer = 0;
            nightTimer = 0;
            handleNightStart();
            timeDisplay.textContent = "🌙 夜晚";
        }
    } else {
        nightTimer++;
        if (nightTimer >= NIGHT_LENGTH) {
            isDay = true;
            nightTimer = 0;
            dayTimer = 0;
            handleDayStart();
            timeDisplay.textContent = "☀️ 白天";
        }
    }
}

function handleNightStart() {
    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;
            updateLearning(cat, 'nest', undefined, true);
        } else if (!cat.nest) {
            cat.isSleeping = true;
            cat.sleepPenalty = (cat.sleepPenalty || 0) + 1;
            updateLearning(cat, 'nest', undefined, false);
            cat.reputation = Math.max(0, cat.reputation - 1);
        } else {
            cat.isSleeping = true;
        }
    }
    for (const prey of preys) {
        prey.isSleeping = Math.random() < 0.8;
    }
}

function handleDayStart() {
    for (const cat of cats) {
        cat.isSleeping = false;
    }
    for (const prey of preys) {
        prey.isSleeping = false;
    }
}

// ---------- 筑巢 ----------
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) {
        updateLearning(cat, 'nest', undefined, 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");
    updateLearning(cat, 'nest', undefined, true);
    return true;
}

// ---------- 更新猎物 ----------
function updateBreeding() {
    if (preys.length >= PREY_MAX_COUNT) return;
    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;
            }
        }
    }
}

function updatePreys() {
    for (let i=preys.length-1; i>=0; i--) {
        const prey = preys[i];
        if (prey.isSleeping) continue;
        prey.hungerTimer -= 1;
        if (prey.hungerTimer <= 0) { preys.splice(i,1); continue; }
        prey.wanderTimer -= 1;
        if (prey.breedCooldown > 0) prey.breedCooldown--;
        prey.targetHerb = null;
        prey.targetMate = null;
        prey.fleeTarget = null;

        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 < 160) speedScale = 1 + (160 - distToCat)/160 * 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);
    }
    updateBreeding();
}

// ---------- 更新猫(含掩护捕猎) ----------
function updateCats() {
    // 睡眠回窝
    for (const cat of cats) {
        if (cat.isSleeping && cat.nest) {
            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);
            }
            if (cat.hungerTimer < CAT_HUNGER_MAX) {
                cat.hungerTimer += 0.2;
                if (cat.hungerTimer > CAT_HUNGER_STATE + 200) cat.hungerTimer = CAT_HUNGER_STATE + 200;
            }
            continue;
        }
    }

    for (let i=cats.length-1; i>=0; i--) {
        const cat = cats[i];
        if (cat.isSleeping) 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;
            }
        }

        cat.hungerTimer -= 1;
        if (cat.hungerTimer <= 0) {
            if (cat.waitingForHelp) {
                for (const other of cats) {
                    if (other.helpingTarget === cat) {
                        other.helpingTarget = null;
                        other.isHelping = false;
                        other.carryingPrey = null;
                        other.helpAttemptTimer = 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 (cat.isHelping) {
            if (hungry) {
                if (cat.helpingTarget) {
                    cat.helpingTarget.waitingForHelp = false;
                    cat.helpingTarget = null;
                }
                cat.isHelping = false;
                cat.carryingPrey = null;
                cat.helpAttemptTimer = 0;
                updateLearning(cat, 'rescue', undefined, 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;
                                addFloatingText(cat.x, cat.y-30, "喂食成功", "#8f8");
                                updateLearning(cat, 'rescue', undefined, true);
                            }
                            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;
                        updateLearning(cat, 'rescue', undefined, false);
                    }
                } else {
                    // 救助过程中捕猎
                    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;
                                const state = cat.hungerTimer < CAT_HUNGER_STATE ? 'hungry' :
                                              cat.hungerTimer > CAT_FULL_THRESHOLD ? 'full' : 'normal';
                                updateLearning(cat, 'hunt', state, 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;
                            updateLearning(cat, 'rescue', undefined, 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;
        }

        // ============================================================
        // 未吃饱强制捕猎(含掩护学习)
        // ============================================================
        const isNotFull = !full;

        if (isNotFull && stage !== 0 && !cat.isHelping && !cat.carryingPrey) {
            // 1. 搜索视野内最近的猎物
            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 (!cat.targetPrey && nearestPrey) {
                // 评估是否使用掩护
                const bestBush = findBestCoverBush(cat, nearestPrey);
                const directRate = cat.learning.cover.attempts > 0 ?
                    cat.learning.cover.success / cat.learning.cover.attempts : 0.5;
                // 探索概率
                const explore = Math.random() < 0.15;
                let useCover = false;
                if (bestBush && (directRate > 0.4 || explore)) {
                    useCover = true;
                }
                if (useCover) {
                    cat.huntMode = 'cover';
                    cat.targetBush = bestBush;
                    cat.coverPhase = 'approach';
                    cat.coverStartPos = { x: cat.x, y: cat.y };
                    cat.targetPrey = nearestPrey;
                    // 记录尝试
                    updateLearning(cat, 'cover', undefined, false); // 先记录尝试,成功后更新为成功
                    addFloatingText(cat.x, cat.y-20, '🌿 利用掩护', '#88dd88');
                } else {
                    cat.huntMode = 'direct';
                    cat.targetPrey = nearestPrey;
                    // 记录直接追击尝试
                    updateLearning(cat, 'hunt', 'normal', false); // 尝试,成功后更新为成功
                }
            }

            // 如果猫正在掩护模式
            if (cat.huntMode === 'cover' && cat.targetPrey) {
                const prey = cat.targetPrey;
                const bush = cat.targetBush;
                if (!bush || !prey.alive) {
                    cat.huntMode = null;
                    cat.targetPrey = null;
                    cat.targetBush = null;
                    cat.coverPhase = null;
                    continue;
                }

                const dToBush = distance(cat, bush);
                const dToPrey = distance(cat, prey);

                switch (cat.coverPhase) {
                    case 'approach':
                        // 移动到灌木内部
                        if (dToBush > 5) {
                            const angleToBush = Math.atan2(bush.y - cat.y, bush.x - cat.x);
                            cat.x += Math.cos(angleToBush) * CAT_SPEED * 0.8;
                            cat.y += Math.sin(angleToBush) * CAT_SPEED * 0.8;
                            cat.angle = angleToBush;
                        } else {
                            // 进入灌木,开始潜行
                            cat.coverPhase = 'stalk';
                            addFloatingText(cat.x, cat.y-20, '🤫 潜行中', '#88dd88');
                        }
                        break;
                    case 'stalk':
                        // 在灌木内缓慢接近猎物
                        const angleToPrey = Math.atan2(prey.y - cat.y, prey.x - cat.x);
                        cat.angle = angleToPrey;
                        // 只要猫在灌木内,猎物看不到猫(不会逃跑)
                        // 接近到攻击距离
                        if (dToPrey < CAT_HUNT_RANGE + 20) {
                            // 准备攻击
                            cat.coverPhase = 'attack';
                        } else {
                            // 缓慢前进
                            cat.x += Math.cos(angleToPrey) * CAT_SPEED * 0.5;
                            cat.y += Math.sin(angleToPrey) * CAT_SPEED * 0.5;
                        }
                        break;
                    case 'attack':
                        // 从灌木中扑出
                        const attackAngle = Math.atan2(prey.y - cat.y, prey.x - cat.x);
                        cat.x += Math.cos(attackAngle) * CAT_SPEED * 1.8;
                        cat.y += Math.sin(attackAngle) * CAT_SPEED * 1.8;
                        cat.angle = attackAngle;
                        if (dToPrey < CAT_HUNT_RANGE) {
                            // 捕猎成功
                            const idx = preys.indexOf(prey);
                            if (idx > -1) {
                                preys.splice(idx, 1);
                                cat.huntCount += 1;
                                cat.hungerTimer = CAT_HUNGER_MAX;
                                // 更新学习:掩护成功
                                updateLearning(cat, 'cover', undefined, true);
                                // 也更新普通捕猎成功(便于比较)
                                updateLearning(cat, 'hunt', 'normal', true);
                                addFloatingText(cat.x, cat.y-30, '✅ 掩护捕猎成功!', '#8f8');
                                // 重置捕猎状态
                                cat.huntMode = null;
                                cat.targetPrey = null;
                                cat.targetBush = null;
                                cat.coverPhase = null;
                            }
                        } else if (dToPrey > 150) {
                            // 猎物逃脱
                            updateLearning(cat, 'cover', undefined, false);
                            addFloatingText(cat.x, cat.y-30, '❌ 掩护失败', '#f88');
                            cat.huntMode = null;
                            cat.targetPrey = null;
                            cat.targetBush = null;
                            cat.coverPhase = null;
                        }
                        break;
                }
                // 边界限制
                cat.x = Math.max(WORLD_BOUND.left+20, Math.min(WORLD_BOUND.right-20, cat.x));
                cat.y = Math.max(WORLD_BOUND.top+20, Math.min(WORLD_BOUND.bottom-20, cat.y));
                continue; // 结束当前帧,不进入后续逻辑
            }

            // 直接追击模式(或普通捕猎)
            if (cat.huntMode === 'direct' && cat.targetPrey) {
                const prey = cat.targetPrey;
                if (!prey.alive) {
                    cat.huntMode = null;
                    cat.targetPrey = null;
                    continue;
                }
                const d = distance(cat, prey);
                const angleToPrey = Math.atan2(prey.y - cat.y, prey.x - cat.x);
                cat.angle = angleToPrey;
                // 如果猎物可见且未被灌木遮挡,猎物会逃跑(由猎物AI处理)
                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(prey);
                    if (idx > -1) {
                        preys.splice(idx, 1);
                        cat.huntCount += 1;
                        cat.hungerTimer = CAT_HUNGER_MAX;
                        // 更新学习:直接成功
                        updateLearning(cat, 'hunt', 'normal', true);
                        addFloatingText(cat.x, cat.y-30, '✅ 直接捕猎成功!', '#8f8');
                    }
                    cat.huntMode = null;
                    cat.targetPrey = null;
                }
                continue;
            }

            // 如果没有任何捕猎模式,则执行普通搜索(原逻辑)
            // 这里作为后备,但上面已经覆盖了所有情况
            if (!cat.targetPrey && !cat.huntMode) {
                // 闲逛
                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 (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;
                            updateLearning(cat, 'mate', undefined, false);
                        }
                    }
                    continue;
                }
            }

            // 救助
            let rescueTarget = null;
            let 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 (!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);
        }
    }
}

// ---------- 重置检测 ----------
function checkReset() {
    if (preys.length === 0 || cats.length === 0) {
        if (resetNoticeTimer === 0) {
            resetWorld();
        }
    }
}

// ---------- 主更新 ----------
function updateWorld() {
    herbRespawnTimer++;
    if (herbRespawnTimer >= HERB_RESPAWN_INTERVAL) {
        herbRespawnTimer = 0;
        trySpawnHerb();
    }
    updateFloatingTexts();
    updateDayNight();

    if (resetNoticeTimer > 0) {
        resetNoticeTimer--;
        if (resetNoticeTimer === 0) {
            resetNotice.style.opacity = '0';
        }
    }
}

// ---------- 面板 ----------
function updatePopulationPanel() {
    let adult = 0, kid = 0;
    for (const c of cats) {
        if (c.growTimer >= CAT_GROW_TIME) adult++;
        else kid++;
    }
    let followText = "无";
    if (followedCat) {
        const genderText = followedCat.gender === "male" ? "公猫" : "母猫";
        followText = `${genderText} | 捕猎:${followedCat.huntCount}`;
    }
    populationPanel.textContent = `猎物:${preys.length} / 草:${herbs.length} / 成年猫:${adult} / 幼猫:${kid} / 跟随:${followText}`;
}

function updateDetailPanel() {
    if (followedCat && !cats.includes(followedCat)) {
        followedCat = null;
    }
    if (!followedCat) {
        detailPanel.textContent = "点击一只猫查看详情";
        return;
    }
    const c = followedCat;
    const progress = c.growTimer / CAT_GROW_TIME;
    let stageStr = "成年";
    if (progress < 0.33) stageStr = "幼崽";
    else if (progress < 0.66) stageStr = "幼猫";

    const hungerStr = c.hungerTimer < CAT_HUNGER_STATE ? "饥饿" : (c.hungerTimer > CAT_FULL_THRESHOLD ? "吃饱" : "未吃饱");
    const genderStr = c.gender === "male" ? "公" : "母";
    const statusParts = [];
    if (c.isLactating) statusParts.push("哺乳中");
    if (c.carryingPrey) statusParts.push("携带猎物");
    if (c.mate) statusParts.push("有配偶");
    if (c.isHelping) statusParts.push("救助中");
    if (c.waitingForHelp) statusParts.push("等待救助");
    if (c.isSleeping) statusParts.push("💤 睡觉");
    if (c.nest) statusParts.push("有窝");
    const statusStr = statusParts.length ? statusParts.join("、") : "无特殊";

    const learn = c.learning;
    const huntRate = (h) => {
        const e = learn.hunt[h];
        return e.attempts > 0 ? (e.success/e.attempts*100).toFixed(0) : 'N/A';
    };
    const huntInfo = `捕猎率: ${huntRate('hungry')}%/${huntRate('normal')}%/${huntRate('full')}%`;
    const coverInfo = learn.cover.attempts > 0 ?
        `掩护率: ${(learn.cover.success/learn.cover.attempts*100).toFixed(0)}%` :
        '掩护: 无经验';
    const rescueInfo = learn.rescue.attempts > 0 ? `救助率: ${(learn.rescue.success/learn.rescue.attempts*100).toFixed(0)}%` : '救助: 无经验';
    const mateInfo = learn.mate.attempts > 0 ? `求偶率: ${(learn.mate.success/learn.mate.attempts*100).toFixed(0)}%` : '求偶: 无经验';
    const nestInfo = learn.nest.attempts > 0 ? `筑巢率: ${(learn.nest.success/learn.nest.attempts*100).toFixed(0)}%` : '筑巢: 无经验';

    const detail = `姓名:${c.name} | 性别:${genderStr} | 年龄:${stageStr} | 饥饿:${hungerStr} | 捕猎:${c.huntCount} | 幼崽:${c.kittens.length} | 声望:${c.reputation} | 状态:${statusStr} | ${huntInfo} | ${coverInfo} | ${rescueInfo} | ${mateInfo} | ${nestInfo}`;
    detailPanel.textContent = detail;
}

// ---------- 渲染 ----------
function renderMap() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.save();

    if (followedCat) {
        const targetX = canvas.width/2 - followedCat.x * scale;
        const targetY = canvas.height/2 - followedCat.y * scale;
        offsetX += (targetX - offsetX) * 0.08;
        offsetY += (targetY - offsetY) * 0.08;
    }

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

    // 地面
    ctx.fillStyle = GRASS_COLOR;
    ctx.fillRect(-3000, -3000, 6000, 6000);

    // 网格
    ctx.beginPath();
    ctx.setLineDash(DASH_PATTERN);
    ctx.strokeStyle = GRID_COLOR;
    ctx.globalAlpha = GRID_ALPHA;
    ctx.lineWidth = 1;
    const p1 = screenToWorld(0, 0);
    const p2 = screenToWorld(canvas.width, canvas.height);
    const startX = Math.floor(p1.x / GRID_SIZE) * GRID_SIZE;
    const startY = Math.floor(p1.y / GRID_SIZE) * GRID_SIZE;
    for (let x = startX; x <= p2.x; x += GRID_SIZE) {
        ctx.moveTo(x, p1.y);
        ctx.lineTo(x, p2.y);
    }
    for (let y = startY; y <= p2.y; y += GRID_SIZE) {
        ctx.moveTo(p1.x, y);
        ctx.lineTo(p2.x, y);
    }
    ctx.stroke();
    ctx.globalAlpha = 1;
    ctx.setLineDash([]);

    // 植物
    ctx.fillStyle = HERB_COLOR;
    for (const herb of herbs) {
        ctx.beginPath();
        ctx.moveTo(herb.x, herb.y - HERB_SIZE);
        ctx.lineTo(herb.x - HERB_SIZE*0.7, herb.y + HERB_SIZE*0.6);
        ctx.lineTo(herb.x + HERB_SIZE*0.7, herb.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.4)";
        ctx.lineWidth = 1;
        ctx.setLineDash([4,4]);
        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 = 3;
        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();
    }

    // 猫本体
    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 (cat === followedCat) {
            ctx.beginPath();
            ctx.strokeStyle = "#fff";
            ctx.lineWidth = 3;
            ctx.arc(cat.x, cat.y, r + 10, 0, 2*Math.PI);
            ctx.stroke();
        }
        if (canBreed) {
            ctx.beginPath();
            ctx.strokeStyle = "#ff88bb";
            ctx.lineWidth = 2;
            ctx.arc(cat.x, cat.y, r + 6, 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-3), cat.y + Math.sin(cat.angle)*(r-3), 3, 0, 2*Math.PI);
        ctx.fill();
    }

    // 窝
    for (const nest of nests) {
        ctx.setLineDash([5,5]);
        ctx.strokeStyle = "#ffffff";
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.arc(nest.x, nest.y, CAT_RADIUS + 4, 0, 2*Math.PI);
        ctx.stroke();
        ctx.setLineDash([]);
    }

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

    ctx.restore();

    // 夜晚覆盖层
    if (!isDay) {
        ctx.save();
        ctx.fillStyle = "rgba(0,0,0,0.6)";
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.restore();
    }

    // 视野
    for (const cat of cats) {
        if (cat.isSleeping) continue;
        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 viewRange = CAT_VIEW_RANGE * sf * scale;
        const sx = cat.x * scale + offsetX;
        const sy = cat.y * scale + offsetY;

        ctx.save();
        ctx.beginPath();
        ctx.moveTo(sx, sy);
        const startAngle = cat.angle - CAT_VIEW_ANGLE/2;
        const endAngle = cat.angle + CAT_VIEW_ANGLE/2;
        ctx.arc(sx, sy, viewRange, startAngle, endAngle);
        ctx.closePath();
        ctx.fillStyle = "rgba(242, 166, 90, 0.12)";
        ctx.fill();
        ctx.strokeStyle = "rgba(242, 166, 90, 0.35)";
        ctx.lineWidth = 1;
        ctx.stroke();
        ctx.restore();
    }

    for (const prey of preys) {
        if (prey.isSleeping) continue;
        const viewRange = PREY_VIEW_RANGE * scale;
        const sx = prey.x * scale + offsetX;
        const sy = prey.y * scale + offsetY;

        ctx.save();
        ctx.beginPath();
        ctx.moveTo(sx, sy);
        const startAngle = prey.angle - PREY_VIEW_ANGLE/2;
        const endAngle = prey.angle + PREY_VIEW_ANGLE/2;
        ctx.arc(sx, sy, viewRange, startAngle, endAngle);
        ctx.closePath();
        ctx.fillStyle = "rgba(0, 0, 0, 0.07)";
        ctx.fill();
        ctx.strokeStyle = "rgba(0, 0, 0, 0.14)";
        ctx.lineWidth = 1;
        ctx.stroke();
        ctx.restore();
    }

    // 文字
    for (const cat of cats) {
        const sx = cat.x * scale + offsetX;
        const sy = cat.y * scale + offsetY;
        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 * scale;

        const symbol = cat.gender === "male" ? MALE_SYMBOL : FEMALE_SYMBOL;
        const text = `(${symbol}) ${cat.name}`;
        ctx.save();
        ctx.font = "bold 14px system-ui";
        ctx.textAlign = "center";
        ctx.textBaseline = "bottom";
        ctx.fillStyle = "#fff";
        ctx.shadowColor = "rgba(0,0,0,0.9)";
        ctx.shadowBlur = 6;
        ctx.fillText(text, sx, sy - r - 6);
        ctx.restore();
    }

    for (const nest of nests) {
        const sx = nest.x * scale + offsetX;
        const sy = nest.y * scale + offsetY;
        ctx.save();
        ctx.font = "12px system-ui";
        ctx.textAlign = "center";
        ctx.textBaseline = "bottom";
        ctx.fillStyle = "#ddd";
        ctx.shadowColor = "rgba(0,0,0,0.8)";
        ctx.shadowBlur = 4;
        ctx.fillText(nest.owner.name + " 的窝", sx, sy - (CAT_RADIUS+4)*scale - 6);
        ctx.restore();
    }

    for (const ft of floatingTexts) {
        const sx = ft.x * scale + offsetX;
        const sy = ft.y * scale + offsetY;
        ctx.save();
        ctx.globalAlpha = ft.alpha;
        ctx.font = "bold 20px system-ui";
        ctx.textAlign = "center";
        ctx.textBaseline = "bottom";
        ctx.shadowColor = "rgba(0,0,0,0.9)";
        ctx.shadowBlur = 8;
        ctx.fillStyle = ft.color || "#ffd700";
        ctx.fillText(ft.text, sx, sy);
        ctx.restore();
    }

    for (const prey of preys) {
        if (prey.isSleeping) {
            const sx = prey.x * scale + offsetX;
            const sy = prey.y * scale + offsetY;
            ctx.save();
            ctx.font = "12px system-ui";
            ctx.textAlign = "center";
            ctx.textBaseline = "bottom";
            ctx.fillStyle = "#ccc";
            ctx.shadowColor = "rgba(0,0,0,0.8)";
            ctx.shadowBlur = 4;
            ctx.fillText("Z", sx, sy - PREY_RADIUS*scale - 2);
            ctx.restore();
        }
    }

    for (const cat of cats) {
        if (cat.isSleeping) {
            const sx = cat.x * scale + offsetX;
            const sy = cat.y * scale + offsetY;
            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 * scale;
            ctx.save();
            ctx.font = "18px system-ui";
            ctx.textAlign = "center";
            ctx.textBaseline = "bottom";
            ctx.fillStyle = "#aaddff";
            ctx.shadowColor = "rgba(0,0,0,0.8)";
            ctx.shadowBlur = 4;
            ctx.fillText("💤", sx, sy - r - 6);
            ctx.restore();
        }
    }
}

// ---------- 交互 ----------
function getMouseScreenPos(e) {
    if (e.touches) return { x: e.touches[0].clientX, y: e.touches[0].clientY };
    return { x: e.clientX, y: e.clientY };
}

function handlePointerDown(e) {
    const pos = getMouseScreenPos(e);
    pointerDownX = pos.x;
    pointerDownY = pos.y;
    pointerDownTime = Date.now();
    isClick = true;
}

function handlePointerUp(e) {
    if (!isClick) return;
    const pos = getMouseScreenPos(e);
    const dx = pos.x - pointerDownX;
    const dy = pos.y - pointerDownY;
    const dist = Math.hypot(dx, dy);
    const elapsed = Date.now() - pointerDownTime;
    if (dist < 10 && elapsed < 300) {
        const canvasPos = getCanvasCoords(pos.x, pos.y);
        const world = screenToWorld(canvasPos.x, canvasPos.y);
        const pixelRatio = getPixelRatio();
        let clicked = null;
        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 hitRadius = Math.max(30, 30 / pixelRatio);
            if (Math.hypot(world.x - cat.x, world.y - cat.y) <= r + hitRadius) {
                clicked = cat;
                break;
            }
        }
        if (clicked) {
            followedCat = clicked;
            isDragging = false;
            return;
        } else {
            followedCat = null;
        }
    }
}

function handlePointerMove(e) {
    const pos = getMouseScreenPos(e);
    if (isClick) {
        const dx = pos.x - pointerDownX;
        const dy = pos.y - pointerDownY;
        if (Math.hypot(dx, dy) > 10) {
            isClick = false;
            isDragging = true;
            dragStartX = pointerDownX;
            dragStartY = pointerDownY;
            startOffsetX = offsetX;
            startOffsetY = offsetY;
        }
    }
    if (isDragging) {
        const pixelRatio = getPixelRatio();
        const dx = (pos.x - dragStartX) * pixelRatio;
        const dy = (pos.y - dragStartY) * pixelRatio;
        offsetX = startOffsetX + dx;
        offsetY = startOffsetY + dy;
    }
}

function handlePointerCancel() {
    isDragging = false;
    isClick = false;
}

canvas.addEventListener("mousedown", handlePointerDown);
window.addEventListener("mousemove", handlePointerMove);
window.addEventListener("mouseup", handlePointerUp);

canvas.addEventListener("touchstart", (e) => {
    if (e.touches.length === 1) {
        const touch = e.touches[0];
        handlePointerDown({ clientX: touch.clientX, clientY: touch.clientY });
    } else if (e.touches.length === 2) {
        lastTouchDist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX,
                                   e.touches[0].clientY - e.touches[1].clientY);
        isDragging = false;
        isClick = false;
    }
}, { passive: true });

canvas.addEventListener("touchmove", (e) => {
    e.preventDefault();
    if (e.touches.length === 1) {
        const touch = e.touches[0];
        handlePointerMove({ clientX: touch.clientX, clientY: touch.clientY });
    } else if (e.touches.length === 2) {
        const dist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX,
                                e.touches[0].clientY - e.touches[1].clientY);
        const cx = (e.touches[0].clientX + e.touches[1].clientX)/2;
        const cy = (e.touches[0].clientY + e.touches[1].clientY)/2;
        const canvasPos = getCanvasCoords(cx, cy);
        const wBefore = screenToWorld(canvasPos.x, canvasPos.y);
        const ratio = dist / lastTouchDist;
        scale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale * ratio));
        lastTouchDist = dist;
        const wAfter = screenToWorld(canvasPos.x, canvasPos.y);
        offsetX += (wAfter.x - wBefore.x) * scale;
        offsetY += (wAfter.y - wBefore.y) * scale;
    }
}, { passive: false });

canvas.addEventListener("touchend", (e) => {
    if (e.touches.length === 0) {
        const touch = e.changedTouches[0];
        if (touch) handlePointerUp({ clientX: touch.clientX, clientY: touch.clientY });
        else handlePointerUp({ clientX: pointerDownX, clientY: pointerDownY });
    }
});
canvas.addEventListener("touchcancel", handlePointerCancel);

canvas.addEventListener("wheel", (e) => {
    e.preventDefault();
    const pos = { x: e.clientX, y: e.clientY };
    const canvasPos = getCanvasCoords(pos.x, pos.y);
    const wBefore = screenToWorld(canvasPos.x, canvasPos.y);
    const zoom = e.deltaY > 0 ? -0.12 : 0.12;
    scale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale + zoom));
    const wAfter = screenToWorld(canvasPos.x, canvasPos.y);
    offsetX += (wAfter.x - wBefore.x) * scale;
    offsetY += (wAfter.y - wBefore.y) * scale;
}, { passive: false });

// ---------- 控制 ----------
function setSpeed(s) {
    speedMultiplier = s;
    document.querySelectorAll('.controls button').forEach(btn => btn.classList.remove('active'));
    const map = {1:'speed1x', 2:'speed2x', 5:'speed5x'};
    const el = document.getElementById(map[s]);
    if (el) el.classList.add('active');
}
window.setSpeed = setSpeed;

function resetWorld() {
    trees.length = 0;
    herbs.length = 0;
    preys.length = 0;
    cats.length = 0;
    bushes.length = 0;
    thorns.length = 0;
    nests.length = 0;
    floatingTexts = [];
    followedCat = null;
    isDay = true;
    dayTimer = 0;
    nightTimer = 0;
    timeDisplay.textContent = "☀️ 白天";
    offsetX = 0;
    offsetY = 0;
    scale = 0.6;
    generateTrees();
    generateHerbs();
    generatePreys();
    generateBushes();
    generateThorns();
    generateCats();
    resetNotice.style.opacity = '1';
    resetNoticeTimer = RESET_NOTICE_DURATION;
}
window.resetWorld = resetWorld;

// ---------- 主循环 ----------
function gameLoop() {
    checkReset();
    for (let t=0; t<speedMultiplier; t++) {
        updateWorld();
        updatePreys();
        updateCats();
    }
    updatePopulationPanel();
    updateDetailPanel();
    renderMap();
    requestAnimationFrame(gameLoop);
}

// ---------- 初始化 ----------
generateTrees();
generateHerbs();
generatePreys();
generateBushes();
generateThorns();
generateCats();
gameLoop();

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

Game Source: 猫武士 · 演替兴衰(掩护学习版)

Creator: EpicCoder88

Libraries: none

Complexity: complex (2086 lines, 76.4 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-epiccoder88-mslturc4" to link back to the original. Then publish at arcadelab.ai/publish.