猫武士 · 调试场地
by EpicCoder881723 lines65.2 KB
<!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;
}
canvas {
display: block;
width: 800px;
height: 600px;
touch-action: none;
}
.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: 6px;
margin-top: 6px;
}
.panel .tools button {
background: #333;
color: #ddd;
border: 1px solid #555;
border-radius: 4px;
padding: 4px 12px;
font-size: 13px;
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;
justify-content: space-between;
font-size: 12px;
color: #aaa;
border-top: 1px solid #333;
padding-top: 4px;
margin-top: 6px;
}
.badge {
background: #333;
padding: 0 6px;
border-radius: 3px;
color: #8f8;
}
@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>
<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.2;
const DASH_PATTERN = [4,4];
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 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 frame = 0;
let speedMultiplier = 1; // 固定1x,调试用
// 实体
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'; // cat, prey, bush, thorn, tree
let isDragging = false;
let dragStartX = 0, dragStartY = 0;
let dragEndX = 0, dragEndY = 0;
let mouseDownPos = null;
let isMouseDown = false;
// 相机(无缩放,固定)
let offsetX = 0, offsetY = 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 });
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':
bushes.push({ x, y });
break;
case 'thorn':
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;
}
}
}
}
// 更新猎物(如果开启饥饿和捕猎AI,但猎物自身移动与饥饿相关)
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; }
}
// 更新猎物AI(与捕猎相关)
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(或捕猎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) {
// 已经在上面求偶中调用performMating
}
// 更新浮动文字
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.setLineDash(DASH_PATTERN);
ctx.strokeStyle = "rgba(255,255,255,0.15)";
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.restore();
// 世界边界
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 (isMouseDown && 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);
}
}
// ---------- 游戏循环 ----------
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;
mouseDownPos = { x, y };
isMouseDown = true;
// 不立即放置,等待mouseup
});
canvas.addEventListener('mousemove', (e) => {
if (isMouseDown && mouseDownPos) {
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;
mouseDownPos = { x, y };
}
});
canvas.addEventListener('mouseup', (e) => {
if (isMouseDown && mouseDownPos) {
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;
// 放置生物
const type = activeTool;
if (placeEntity(type, x, y)) {
placeInfo.textContent = `放置了 ${type}`;
} else {
placeInfo.textContent = `放置失败 (可能太挤或超出边界)`;
}
isMouseDown = false;
mouseDownPos = null;
}
});
canvas.addEventListener('mouseleave', () => {
isMouseDown = false;
mouseDownPos = null;
});
// 触摸事件
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;
mouseDownPos = { x, y };
isMouseDown = true;
});
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (isMouseDown && mouseDownPos) {
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;
mouseDownPos = { x, y };
}
});
canvas.addEventListener('touchend', (e) => {
e.preventDefault();
if (isMouseDown && mouseDownPos) {
// 获取最后位置
const touch = e.changedTouches[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;
const type = activeTool;
if (placeEntity(type, x, y)) {
placeInfo.textContent = `放置了 ${type}`;
} else {
placeInfo.textContent = `放置失败`;
}
isMouseDown = false;
mouseDownPos = null;
}
});
// 工具切换
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 = '已清空所有';
});
// 初始化
initScene();
gameLoop();
</script>
</body>
</html>Game Source: 猫武士 · 调试场地
Creator: EpicCoder88
Libraries: none
Complexity: complex (1723 lines, 65.2 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-msmub5gv" to link back to the original. Then publish at arcadelab.ai/publish.