猫武士 · 演替兴衰(重置版)
by EpicCoder881824 lines63.9 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: #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>
// ============================================================
// 猫武士 · 演替兴衰 重置版 (v0.9.2)
// 修复:猫繁殖困难(降低繁殖要求,初始huntCount=2)
// 新增:生态归零自动重置并显示提示
// ============================================================
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 = 18;
const HERB_COLOR = "#1c4d19";
const HERB_SIZE = 10;
const HERB_MAX_COUNT = 80;
const HERB_MIN_SPACE = 35;
const HERB_RESPAWN_INTERVAL = 160;
const PREY_RADIUS = 8;
const PREY_COUNT = 28;
const PREY_MAX_COUNT = 70;
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; // 降低到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 = 3000;
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 = 40;
const BUSH_RADIUS = 30;
const THORN_COUNT = 25;
const THORN_RADIUS = 22;
// 窝相关
const NEST_CHECK_RADIUS = 80;
const NEST_PREFER_RADIUS = 120;
const MALE_SYMBOL = "♂";
const FEMALE_SYMBOL = "♀";
const WORLD_BOUND = {
left: -300,
top: -200,
right: 1500,
bottom: 1000
};
// ---------- 相机 ----------
let offsetX = 0, offsetY = 0, scale = 1.0;
const MIN_SCALE = 0.3, 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; // 3秒
// ---------- 工具函数 ----------
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 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]];
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 < 120) {
if (trySpawnHerb()) count++;
else break;
}
if (herbs.length < 10) {
for (let i=0; i<20; 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 < 5) {
for (let i=0; i<10; 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 < 5) {
for (let i=0; i<10; 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 < 3) {
for (let i=0; i<5; 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, // 初始捕猎次数设为2,加速繁殖
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
};
}
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();
return cat;
}
function generateCats() {
cats = [];
let tries = 0;
while (cats.length < 5 && tries < 5*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 = [[100,100], [200,200]];
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;
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;
} else if (!cat.nest) {
cat.isSleeping = true;
cat.sleepPenalty = (cat.sleepPenalty || 0) + 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;
if (cat.sleepPenalty > 0) {
cat.reputation = Math.max(0, cat.reputation - 1);
cat.sleepPenalty = 0;
}
}
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 < 2) 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");
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;
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;
}
// 等待救助
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;
}
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;
} 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");
}
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;
}
} 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;
}
}
} else {
cat.helpAttemptTimer += 1;
if (cat.helpAttemptTimer > 300) {
if (cat.helpingTarget) {
cat.helpingTarget.waitingForHelp = false;
cat.helpingTarget = null;
}
cat.isHelping = false;
cat.helpAttemptTimer = 0;
} 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 && !cat.isHelping && stage===2) {
let target = null, minDist = 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 < minDist) {
minDist = d;
target = other;
}
}
if (target) {
cat.isHelping = true;
cat.helpingTarget = target;
cat.helpTargetPos = { x: target.x, y: target.y };
cat.helpAttemptTimer = 0;
target.waitingForHelp = true;
addFloatingText(cat.x, cat.y-30, "在这等着。", "#ffd700");
continue;
}
}
// 筑巢
if (full && !cat.isHelping && !cat.targetMate && !cat.targetPrey && !cat.nest && cat.buildNestCooldown===0) {
if (tryBuildNest(cat)) continue;
}
// 求偶(仅吃饱的猫)
cat.targetMate = null;
if (canMate && full && !cat.isHelping) {
let nearestMate = null, minDist = 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 < minDist) {
minDist = d;
nearestMate = other;
}
}
cat.targetMate = nearestMate;
}
if (cat.targetMate) {
const d = distance(cat, cat.targetMate);
cat.angle = Math.atan2(cat.targetMate.y - cat.y, cat.targetMate.x - cat.x);
if (d > CAT_BREED_DISTANCE) {
const moveX = (cat.targetMate.x - cat.x)/d * CAT_SPEED;
const moveY = (cat.targetMate.y - cat.y)/d * CAT_SPEED;
safeMove(cat, moveX, moveY, currentRadius);
} else {
const father = cat.gender==="male" ? cat : cat.targetMate;
const mother = cat.gender==="female" ? cat : cat.targetMate;
const success = performMating(father, mother);
cat.targetMate = null;
if (cat.mate) cat.mate.targetMate = null;
if (!success) cat.breedCooldown = 60;
}
continue;
}
// 捕猎(未吃饱的猫捕猎)
cat.targetPrey = null;
if (stage !== 0 && !full) {
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;
}
}
cat.targetPrey = nearestPrey;
}
if (cat.targetPrey) {
const d = distance(cat, cat.targetPrey);
cat.angle = Math.atan2(cat.targetPrey.y - cat.y, cat.targetPrey.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 = (cat.targetPrey.x - cat.x)/d * CAT_SPEED;
moveY = (cat.targetPrey.y - cat.y)/d * CAT_SPEED;
}
safeMove(cat, moveX, moveY, currentRadius);
} else {
const idx = preys.indexOf(cat.targetPrey);
if (idx > -1) {
const prey = preys.splice(idx,1)[0];
cat.huntCount += 1;
cat.hungerTimer = CAT_HUNGER_MAX;
if (!hungry && cat.mate && cat.kittens.length>0 && stage===2 && !cat.isHelping) {
cat.carryingPrey = prey;
cat.targetMateLocation = { x: cat.mate.x, y: cat.mate.y };
}
cat.tryFlank = true;
}
cat.targetPrey = null;
}
} 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);
}
}
}
// ---------- 演替重置检测 ----------
function checkReset() {
if (preys.length === 0 || cats.length === 0) {
// 显示通知
resetNotice.style.opacity = '1';
resetNoticeTimer = RESET_NOTICE_DURATION;
// 重置世界
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 ? "饥饿" : "正常";
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 detail = `姓名:${c.name} | 性别:${genderStr} | 年龄:${stageStr} | 饥饿:${hungerStr} | 捕猎:${c.huntCount} | 幼崽:${c.kittens.length} | 声望:${c.reputation} | 状态:${statusStr}`;
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.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();
}
// 猎物Z标记
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 = 1.0;
generateTrees();
generateHerbs();
generatePreys();
generateBushes();
generateThorns();
generateCats();
// 显示重置通知
resetNotice.style.opacity = '1';
resetNoticeTimer = RESET_NOTICE_DURATION;
}
window.resetWorld = resetWorld;
// ---------- 主循环 ----------
function gameLoop() {
// 检查是否需要重置(在更新之前)
if (preys.length === 0 || cats.length === 0) {
// 避免重复触发
if (resetNoticeTimer === 0) {
resetWorld();
}
}
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 (1824 lines, 63.9 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-mslo0qfg" to link back to the original. Then publish at arcadelab.ai/publish.