猫武士 · 疲劳系统完整版
by EpicCoder88764 lines71.0 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; }
#debugInfo { position: fixed; top: 10px; left: 10px; color: red; font-size: 16px; font-weight: bold; z-index: 1000; background: rgba(0,0,0,0.5); padding: 4px 8px; border-radius: 4px; }
</style>
</head>
<body>
<div id="resetNotice">演替已经重新开启</div>
<div id="debugInfo">初始化中...</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>
// ============================================================
// 猫武士 · 演替兴衰 疲劳系统完整版 (v6.0)
// 新增:白天持续运动积累疲劳,疲劳降低速度,过劳强制睡眠
// ============================================================
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 debugInfo = document.getElementById("debugInfo");
// ---------- 名字库 ----------
const PREFIXES = ["虎","鹰","鸦","松鸦","香薇","冬青","蕨","玫瑰","火","云","雾","风","灰","蓝","斑","长","短","亮","暗","雨","雪","霜","露","溪","河","湖","星","月","日","影","光","叶","花","石","岩","荆棘","芦苇","柳","橡","枫","桦","松","杉","白","黑","红","金","银","铜"];
const SUFFIXES = ["尾","羽","花","毛","爪","足","心","风","叶","霜","云","星","光","月","溪","河","湖","石","岩","棘","苇","柳","橡","枫","桦","松","杉","飞","跃","奔","步","啸","嚎","眼","耳","鼻","须","掌","腿","腹","背","额","面","斑","纹","环"];
// ---------- 配置 ----------
const GRID_SIZE=60; const GRID_ALPHA=0.6; const DASH_PATTERN=[8,6]; const GRID_COLOR="#cccccc"; 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=30;
const HERB_COLOR="#1c4d19"; const HERB_SIZE=10; const HERB_MAX_COUNT=250; const HERB_MIN_SPACE=35; const HERB_RESPAWN_INTERVAL=160; const HERB_SPAWN_PER_TICK=4;
const PREY_RADIUS=8; const PREY_COUNT=100; const PREY_MAX_COUNT=200; const PREY_SPEED=1.1; const PREY_FLEE_SPEED=2.7; 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=21000;
const CAT_COLOR="#f2a65a"; const CAT_RADIUS=14; const CAT_BASE_SPEED=1.6; const CAT_VIEW_RANGE=220; const CAT_VIEW_ANGLE=Math.PI/3; const CAT_HUNT_RANGE=35; const REWARD_CATCH=10; const PENALTY_ESCAPE=-6;
const CAT_BREED_REQUIRE=3; const CAT_BREED_DISTANCE=40; const CAT_BREED_COOLDOWN=800; const CAT_LITTER_SIZE=2; const CAT_GROW_TIME=1800;
const CAT_HUNGER_MAX=21000; const CAT_HUNGER_DECAY=1; const CAT_HUNGER_STATE=1500; const CAT_FULL_THRESHOLD=5000; const MAX_CATS=120;
const DAY_LENGTH=2400; const NIGHT_LENGTH=1800;
const BUSH_COUNT=80; const BUSH_RADIUS=30; const THORN_COUNT=30; const THORN_RADIUS=22;
const NEST_CHECK_RADIUS=80; const NEST_PREFER_RADIUS=120;
const MALE_SYMBOL="♂"; const FEMALE_SYMBOL="♀";
const WORLD_BOUND={left:-600, top:-400, right:3000, bottom:2000};
// 疲劳参数
const FATIGUE_MAX = 100;
const FATIGUE_INCREASE_PER_FRAME = 0.15; // 白天移动时增加
const FATIGUE_RECOVER_SLEEP = 0.8; // 睡眠时恢复速度
const FATIGUE_RECOVER_IDLE = 0.15; // 静止时恢复速度
const FATIGUE_SLEEP_THRESHOLD = 90; // 达到此值强制睡眠
const FATIGUE_WAKE_THRESHOLD = 20; // 睡眠后恢复到此值醒来
// 社交参数
const FAMILIARITY_THRESHOLD = 30;
const FAMILIARITY_INCREASE_PER_FRAME = 0.05;
const FAMILIARITY_COOPERATION_BONUS = 15;
const FAMILIARITY_RESCUE_BONUS = 8;
const FAMILIARITY_MATE_BONUS = 5;
// ---------- 相机 ----------
let offsetX=0, offsetY=0, scale=1.0;
const MIN_SCALE=0.2, MAX_SCALE=2.5;
let isDragging=false, dragStartX=0, dragStartY=0, startOffsetX=0, startOffsetY=0, lastTouchDist=0, followedCat=null;
let pointerDownX=0, pointerDownY=0, pointerDownTime=0, isClick=false;
let speedMultiplier=1;
// ---------- 实体容器 ----------
let trees=[], herbs=[], preys=[], cats=[], bushes=[], thorns=[], nests=[], herbRespawnTimer=0, floatingTexts=[];
let isDay=true, dayTimer=0, nightTimer=0, resetNoticeTimer=0, RESET_NOTICE_DURATION=180;
let catIdCounter = 0;
// ---------- 工具函数 ----------
function isPointInWorld(x,y){ return x>=WORLD_BOUND.left && x<=WORLD_BOUND.right && y>=WORLD_BOUND.top && y<=WORLD_BOUND.bottom; }
function screenToWorld(sx,sy){ return {x:(sx-offsetX)/scale, y:(sy-offsetY)/scale}; }
function getCanvasCoords(cx,cy){ const rect=canvas.getBoundingClientRect(); return {x:(cx-rect.left)*(canvas.width/rect.width), y:(cy-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, fx=x1-cx, fy=y1-cy; const a=dx*dx+dy*dy, b=2*(fx*dx+fy*dy), 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), t2=(-b+disc)/(2*a); return (t1>=0&&t1<=1)||(t2>=0&&t2<=1); }
// ---------- 碰撞与视线 ----------
function checkCollision(x,y,radius){ for(const t of trees) if(distance({x,y},t)<TREE_COLLIDE_RADIUS+radius) return true; for(const th of thorns) if(distance({x,y},th)<THORN_RADIUS+radius) return true; return false; }
function isLineBlocked(x1,y1,x2,y2){ for(const t of trees) if(lineIntersectsCircle(x1,y1,x2,y2,t.x,t.y,TREE_BLOCK_RAY_RADIUS)) return true; for(const b of bushes){ if(pointInCircle(x1,y1,b.x,b.y,BUSH_RADIUS)) continue; if(pointInCircle(x2,y2,b.x,b.y,BUSH_RADIUS)) return true; if(lineIntersectsCircle(x1,y1,x2,y2,b.x,b.y,BUSH_RADIUS)) return true; } return false; }
function safeMove(entity, moveX, moveY, radius){ let newX=entity.x+moveX, newY=entity.y+moveY; const left=WORLD_BOUND.left+radius, right=WORLD_BOUND.right-radius, top=WORLD_BOUND.top+radius, bottom=WORLD_BOUND.bottom-radius; let canX=(newX>=left&&newX<=right), 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 findNearestPrey(cat){ let nearest=null, minDist=Infinity; for(const prey of preys){ if(prey.isSleeping) continue; const d=distance(cat, prey); if(d>CAT_VIEW_RANGE) continue; if(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; nearest=prey; } } return nearest; }
function findNearestBush(catPos){ let nearest=null, minDist=Infinity; for(const b of bushes){ const d=distance(catPos,b); if(d<minDist){ minDist=d; nearest=b; } } return nearest; }
function findBestCoverBush(catPos, preyPos){ let best=null, bestScore=-Infinity; for(const b of bushes){ const dCat=distance(catPos,b); const dPrey=distance(b,preyPos); if(dPrey>90||dPrey<20) continue; if(dCat>250) continue; if(isLineBlocked(b.x,b.y,preyPos.x,preyPos.y)) continue; const score=60-dPrey+(80-dCat)*0.3; if(score>bestScore){ bestScore=score; best=b; } } return best; }
function canSeePreyFromBush(catPos,bushPos,preyPos){ if(pointInCircle(preyPos.x,preyPos.y,bushPos.x,bushPos.y,BUSH_RADIUS)) return false; if(isLineBlocked(bushPos.x,bushPos.y,preyPos.x,preyPos.y)) return false; const d=distance(bushPos,preyPos); if(d>CAT_VIEW_RANGE) return false; const angleToPrey=Math.atan2(preyPos.y-catPos.y,preyPos.x-catPos.x); let delta=angleToPrey-catPos.angle; while(delta>Math.PI) delta-=2*Math.PI; while(delta<-Math.PI) delta+=2*Math.PI; return Math.abs(delta)<=CAT_VIEW_ANGLE/2; }
function 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 initFamiliarity(cat){ cat.familiarity = {}; }
function getFamiliarity(catA, catB){ if(catA.id === catB.id) return 0; return catA.familiarity[catB.id] || 0; }
function changeFamiliarity(catA, catB, delta){ if(catA.id === catB.id) return; if(!catA.familiarity) catA.familiarity = {}; if(!catB.familiarity) catB.familiarity = {}; catA.familiarity[catB.id] = Math.min(100, Math.max(0, (catA.familiarity[catB.id]||0) + delta)); catB.familiarity[catA.id] = Math.min(100, Math.max(0, (catB.familiarity[catA.id]||0) + delta)); }
function updateFamiliarities(){ for(let i=0;i<cats.length;i++){ for(let j=i+1;j<cats.length;j++){ const a=cats[i], b=cats[j]; if(!a.familiarity || !b.familiarity) continue; const d=distance(a,b); if(d<100){ changeFamiliarity(a, b, FAMILIARITY_INCREASE_PER_FRAME); } } } }
// ---------- 生成函数 ----------
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<5){ const fallback=[[-100,-50],[100,80],[-50,120],[200,-30],[-80,-80]]; for(const [x,y] of fallback){ 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}); } } }
function trySpawnHerb(){ if(herbs.length>=HERB_MAX_COUNT) return false; let spawned=0; 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+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}); spawned++; } } return spawned>0; }
function generateHerbs(){ herbs=[]; let count=0; while(count<HERB_MAX_COUNT && count<200){ if(trySpawnHerb()) count+=HERB_SPAWN_PER_TICK; 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<10){ for(let i=0;i<15;i++){ preys.push(createPrey(WORLD_BOUND.left+40+Math.random()*(WORLD_BOUND.right-WORLD_BOUND.left-80), WORLD_BOUND.top+40+Math.random()*(WORLD_BOUND.bottom-WORLD_BOUND.top-80))); } } }
function generateBushes(){ bushes=[]; let tries=0; while(bushes.length<BUSH_COUNT && tries<BUSH_COUNT*30){ tries++; const x=WORLD_BOUND.left+20+Math.random()*(WORLD_BOUND.right-WORLD_BOUND.left-40); const y=WORLD_BOUND.top+20+Math.random()*(WORLD_BOUND.bottom-WORLD_BOUND.top-40); let ok=true; for(const t of trees) if(distance({x,y},t)<TREE_COLLIDE_RADIUS+BUSH_RADIUS){ ok=false; break; } if(!ok) continue; for(const b of bushes) if(distance({x,y},b)<BUSH_RADIUS*1.8){ ok=false; break; } if(ok) bushes.push({x,y}); } if(bushes.length<10){ for(let i=0;i<15;i++){ bushes.push({x:WORLD_BOUND.left+30+Math.random()*(WORLD_BOUND.right-WORLD_BOUND.left-60), y:WORLD_BOUND.top+30+Math.random()*(WORLD_BOUND.bottom-WORLD_BOUND.top-60)}); } } }
function generateThorns(){ thorns=[]; let tries=0; while(thorns.length<THORN_COUNT && tries<THORN_COUNT*30){ tries++; const x=WORLD_BOUND.left+20+Math.random()*(WORLD_BOUND.right-WORLD_BOUND.left-40); const y=WORLD_BOUND.top+20+Math.random()*(WORLD_BOUND.bottom-WORLD_BOUND.top-40); let ok=true; for(const t of trees) if(distance({x,y},t)<TREE_COLLIDE_RADIUS+THORN_RADIUS){ ok=false; break; } if(!ok) continue; for(const th of thorns) if(distance({x,y},th)<THORN_RADIUS*2){ ok=false; break; } if(ok) thorns.push({x,y}); } if(thorns.length<5){ for(let i=0;i<8;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){ const traits=generateTraits(); const id = catIdCounter++; const cat = {id, 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, coverStartPos:null, targetLastKnownPos:null, targetLostTimer:0, familiarity:{}, cooperativePartner:null, isCooperative:false,
// 疲劳相关
fatigue:0,
isSleepingByFatigue:false,
get speed(){
let base = CAT_BASE_SPEED + this.traits.speedOffset;
// 疲劳修正
if(this.fatigue > 80) base *= 0.3;
else if(this.fatigue > 60) base *= 0.6;
else if(this.fatigue > 40) base *= 0.8;
else if(this.fatigue > 20) base *= 0.95;
return Math.max(0.1, base);
} }; initFamiliarity(cat); return cat; }
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(); initFamiliarity(cat); return cat; }
function generateCats(){ cats=[]; let tries=0; const initialCount=15; while(cats.length<initialCount && tries<initialCount*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<5){ const fallback=[[-50,0],[50,30],[-30,-40],[70,-20],[0,60]]; for(const [x,y] of fallback){ let ok=true; for(const c of cats) if(distance({x,y},c)<CAT_RADIUS*3){ ok=false; break; } if(ok) cats.push(createCat(x,y)); } } }
// ---------- 交配 ----------
function performMating(father,mother){ if(cats.length>=MAX_CATS) return false; if(father.gender===mother.gender) return false; if(father.growTimer<CAT_GROW_TIME||mother.growTimer<CAT_GROW_TIME) return false; if(father.breedCooldown>0||mother.breedCooldown>0) return false; if(father.huntCount<CAT_BREED_REQUIRE||mother.huntCount<CAT_BREED_REQUIRE) return false; father.mate=mother; mother.mate=father; const kittens=[]; for(let k=0;k<CAT_LITTER_SIZE;k++){ let sx=(father.x+mother.x)/2+(Math.random()-0.5)*45; let sy=(father.y+mother.y)/2+(Math.random()-0.5)*45; if(checkCollision(sx,sy,CAT_RADIUS*0.4)){ sx+=35; sy+=35; } kittens.push(createKitten(sx,sy,father,mother)); } cats.push(...kittens); mother.isLactating=true; mother.milkSupply=20; father.kittens.push(...kittens); mother.kittens.push(...kittens); father.huntCount=0; mother.huntCount=0; father.breedCooldown=CAT_BREED_COOLDOWN; mother.breedCooldown=CAT_BREED_COOLDOWN; father.targetMate=null; mother.targetMate=null; updateLearning(father,'mate',true); updateLearning(mother,'mate',true); changeFamiliarity(father, mother, FAMILIARITY_MATE_BONUS); return true; }
// ---------- 浮动文字 ----------
function addFloatingText(x,y,text,color="#ffd700"){ floatingTexts.push({x,y,text,color,life:60,maxLife:60,alpha:1.0}); }
function updateFloatingTexts(){ for(let i=floatingTexts.length-1;i>=0;i--){ const ft=floatingTexts[i]; ft.life-=1; ft.alpha=ft.life/ft.maxLife; if(ft.life<=0) floatingTexts.splice(i,1); } }
// ---------- 昼夜管理 ----------
function updateDayNight(){ if(isDay){ dayTimer++; if(dayTimer>=DAY_LENGTH){ isDay=false; dayTimer=0; nightTimer=0; handleNightStart(); timeDisplay.textContent="🌙 夜晚"; } }else{ nightTimer++; if(nightTimer>=NIGHT_LENGTH){ isDay=true; nightTimer=0; dayTimer=0; handleDayStart(); timeDisplay.textContent="☀️ 白天"; } } }
function handleNightStart(){ for(const cat of cats){ if(cat.isSleeping) continue; const progress=cat.growTimer/CAT_GROW_TIME; if(progress<0.33) continue; const hungry=cat.hungerTimer<CAT_HUNGER_STATE; if(hungry && Math.random()<0.3){ cat.isSleeping=false; continue; } if(cat.nest && !hungry){ cat.isSleeping=true; updateLearning(cat,'nest',true); }else if(!cat.nest){ cat.isSleeping=true; cat.sleepPenalty=(cat.sleepPenalty||0)+1; 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; }
function handleDayStart(){ for(const cat of cats) cat.isSleeping=false; for(const prey of preys) prey.isSleeping=false; }
// ---------- 筑巢 ----------
function tryBuildNest(cat){ if(cat.hungerTimer<CAT_FULL_THRESHOLD) return false; if(cat.isHelping||cat.targetMate||cat.isSleeping) return false; if(cat.buildNestCooldown>0) return false; if(cat.nest) return false; let treeCount=0, thornCount=0; for(const tree of trees) if(distance(cat,tree)<NEST_CHECK_RADIUS) treeCount++; for(const thorn of thorns) if(distance(cat,thorn)<NEST_CHECK_RADIUS) thornCount++; if(treeCount<1 || thornCount<1){ updateLearning(cat,'nest',false); return false; } let bestX=cat.x, bestY=cat.y, bestScore=0; for(let attempt=0; attempt<20; attempt++){ const angle=Math.random()*2*Math.PI; const dist=20+Math.random()*40; const tx=cat.x+Math.cos(angle)*dist; const ty=cat.y+Math.sin(angle)*dist; if(!isPointInWorld(tx,ty)) continue; if(checkCollision(tx,ty,CAT_RADIUS)) continue; let score=0; for(const nest of nests){ const d=distance({x:tx,y:ty},nest); if(d<NEST_PREFER_RADIUS) score+=(NEST_PREFER_RADIUS-d)/NEST_PREFER_RADIUS; } score+=Math.random()*0.5; if(score>bestScore){ bestScore=score; bestX=tx; bestY=ty; } } const nest={x:bestX,y:bestY,owner:cat}; nests.push(nest); cat.nest=nest; cat.buildNestCooldown=600; addFloatingText(bestX,bestY-20,cat.name+" 建了窝","#88dd88"); updateLearning(cat,'nest',true); return true; }
// ---------- 更新猎物 ----------
function updateBreeding(){ if(preys.length>=PREY_MAX_COUNT) return; for(let i=0;i<preys.length;i++){ const a=preys[i]; if(a.breedCooldown>0||a.eatenHerbs<FEED_REQUIRE) continue; for(let j=i+1;j<preys.length;j++){ const b=preys[j]; if(b.breedCooldown>0||b.eatenHerbs<FEED_REQUIRE) continue; if(distance(a,b)<=BREED_DISTANCE){ const cx=(a.x+b.x)/2, cy=(a.y+b.y)/2; for(let k=0;k<BABY_COUNT;k++){ let sx=cx+(Math.random()-0.5)*40; let sy=cy+(Math.random()-0.5)*40; if(checkCollision(sx,sy,PREY_RADIUS)){ sx+=30; sy+=30; } if(preys.length<PREY_MAX_COUNT) preys.push(createPrey(sx,sy)); } a.eatenHerbs=0; b.eatenHerbs=0; a.breedCooldown=600; b.breedCooldown=600; break; } } } }
function updatePreys(){ for(let i=preys.length-1;i>=0;i--){ const prey=preys[i]; if(prey.isSleeping) continue; prey.hungerTimer-=1; if(prey.hungerTimer<=0){ preys.splice(i,1); continue; } prey.wanderTimer-=1; if(prey.breedCooldown>0) prey.breedCooldown--; prey.targetHerb=null; prey.targetMate=null; prey.fleeTarget=null; let nearestCat=null, minCatDist=Infinity; for(const cat of cats){ if(cat.isSleeping) continue; const d=distance(prey,cat); if(d>PREY_VIEW_RANGE) continue; const angleToCat=Math.atan2(cat.y-prey.y,cat.x-prey.x); let delta=angleToCat-prey.angle; while(delta>Math.PI) delta-=2*Math.PI; while(delta<-Math.PI) delta+=2*Math.PI; if(Math.abs(delta)<=PREY_VIEW_ANGLE/2 && !isLineBlocked(prey.x,prey.y,cat.x,cat.y)){ if(d<minCatDist){ minCatDist=d; nearestCat=cat; } } } let moveX=0, moveY=0; if(nearestCat){ const dxAway=prey.x-nearestCat.x, dyAway=prey.y-nearestCat.y; const distToCat=Math.hypot(dxAway,dyAway); let fleeAngle=Math.atan2(dyAway,dxAway); prey.angle=fleeAngle; const testX=prey.x+Math.cos(fleeAngle)*30, 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 initiateCooperation(cat, partner, prey){ if(cat.growTimer < CAT_GROW_TIME || partner.growTimer < CAT_GROW_TIME) return false; if(cat.hungerTimer > CAT_FULL_THRESHOLD || partner.hungerTimer > CAT_FULL_THRESHOLD) return false; if(cat.isHelping || partner.isHelping) return false; if(cat.carryingPrey || partner.carryingPrey) return false; if(cat.isCooperative || partner.isCooperative) return false; const fam = getFamiliarity(cat, partner); if(fam < FAMILIARITY_THRESHOLD) return false; if(distance(cat, partner) > 150) return false; if(cat.targetPrey !== prey || partner.targetPrey !== prey) { if(!cat.targetPrey) cat.targetPrey = prey; if(!partner.targetPrey) partner.targetPrey = prey; } cat.isCooperative = true; partner.isCooperative = true; cat.cooperativePartner = partner; partner.cooperativePartner = cat; if(cat.traits.boldness >= partner.traits.boldness){ cat.huntMode = 'cooperative_front'; partner.huntMode = 'cooperative_flank'; }else{ cat.huntMode = 'cooperative_flank'; partner.huntMode = 'cooperative_front'; } addFloatingText(cat.x, cat.y-30, '🤝 合作!', '#88ddff'); addFloatingText(partner.x, partner.y-30, '🤝 合作!', '#88ddff'); return true; }
function updateCooperativeMovement(cat){ if(!cat.isCooperative || !cat.cooperativePartner) return false; const partner = cat.cooperativePartner; const prey = cat.targetPrey; if(!prey || !partner || !preys.includes(prey) || partner.isSleeping || prey.isSleeping){ endCooperation(cat, partner, false); return false; } if(!partner.isCooperative){ endCooperation(cat, partner, false); return false; } const d = distance(cat, prey); const angleToPrey = Math.atan2(prey.y - cat.y, prey.x - cat.x); cat.angle = angleToPrey; if(cat.huntMode === 'cooperative_front'){ if(d > CAT_HUNT_RANGE){ const moveX = Math.cos(angleToPrey) * cat.speed * 1.1; const moveY = Math.sin(angleToPrey) * cat.speed * 1.1; safeMove(cat, moveX, moveY, CAT_RADIUS); }else{ capturePrey(cat, prey); endCooperation(cat, partner, true); return true; } }else if(cat.huntMode === 'cooperative_flank'){ const flankAngle = angleToPrey + (Math.random()>0.5 ? 1.2 : -1.2); const moveX = Math.cos(flankAngle) * cat.speed * 1.0 + Math.cos(angleToPrey) * cat.speed * 0.4; const moveY = Math.sin(flankAngle) * cat.speed * 1.0 + Math.sin(angleToPrey) * cat.speed * 0.4; safeMove(cat, moveX, moveY, CAT_RADIUS); if(d < CAT_HUNT_RANGE + 20){ cat.huntMode = 'cooperative_front'; } } return true; }
function capturePrey(cat, prey){ const idx = preys.indexOf(prey); if(idx > -1){ preys.splice(idx, 1); cat.huntCount += 1; cat.hungerTimer = CAT_HUNGER_MAX; updateLearning(cat, 'hunt', true); addFloatingText(cat.x, cat.y-30, '✅ 捕猎成功!', '#8f8'); } }
function endCooperation(cat, partner, success){ cat.isCooperative = false; cat.cooperativePartner = null; partner.isCooperative = false; partner.cooperativePartner = null; cat.huntMode = null; partner.huntMode = null; if(success){ changeFamiliarity(cat, partner, FAMILIARITY_COOPERATION_BONUS); updateLearning(cat, 'hunt', true); updateLearning(partner, 'hunt', true); }else{ updateLearning(cat, 'hunt', false); updateLearning(partner, 'hunt', false); } }
// ---------- 更新猫(含疲劳逻辑) ----------
function updateCats(){
// 更新熟悉度
updateFamiliarities();
// 睡眠回窝(夜晚或疲劳强制睡眠)
for(const cat of cats){
// 如果因疲劳强制睡眠,且未到醒来阈值,则保持睡眠
if(cat.isSleeping && cat.isSleepingByFatigue){
// 恢复疲劳
cat.fatigue = Math.max(0, cat.fatigue - FATIGUE_RECOVER_SLEEP);
if(cat.fatigue <= FATIGUE_WAKE_THRESHOLD){
cat.isSleeping = false;
cat.isSleepingByFatigue = false;
addFloatingText(cat.x, cat.y-20, '💪 恢复精力', '#88ddff');
}
continue;
}
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.5; if(cat.hungerTimer>CAT_HUNGER_STATE+300) cat.hungerTimer=CAT_HUNGER_STATE+300; }
continue;
}
}
for(let i=cats.length-1;i>=0;i--){
const cat=cats[i];
if(cat.isSleeping) continue;
// 处理合作状态
if(cat.isCooperative && cat.cooperativePartner){
const updated = updateCooperativeMovement(cat);
if(!updated){
cat.isCooperative = false;
cat.cooperativePartner = null;
cat.huntMode = null;
if(!cat.targetPrey || !preys.includes(cat.targetPrey)){
cat.targetPrey = null;
cat.targetLastKnownPos = null;
cat.targetLostTimer = 0;
}
}
// 合作中疲劳增加(运动)
if(isDay && (cat.huntMode === 'cooperative_front' || cat.huntMode === 'cooperative_flank')){
cat.fatigue = Math.min(FATIGUE_MAX, cat.fatigue + FATIGUE_INCREASE_PER_FRAME);
}
continue;
}
// 等待救助
if(cat.waitingForHelp){
let helperAlive=false;
for(const other of cats){
if(other.helpingTarget===cat){ helperAlive=true; break; }
}
if(!helperAlive){ cat.waitingForHelp=false; }else{
// 等待时疲劳不增加
continue;
}
}
// 饥饿
cat.hungerTimer-=CAT_HUNGER_DECAY;
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;
// 哺乳时移动少,疲劳恢复
if(isDay) cat.fatigue = Math.max(0, cat.fatigue - FATIGUE_RECOVER_IDLE);
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);
if(isDay) cat.fatigue = Math.min(FATIGUE_MAX, cat.fatigue + FATIGUE_INCREASE_PER_FRAME * 1.5);
}else{
target.milkSupply+=30;
target.isLactating=true;
cat.carryingPrey=null;
cat.targetMateLocation=null;
}
cat.targetMate=null; cat.targetPrey=null;
continue;
}
const hungry=cat.hungerTimer<CAT_HUNGER_STATE;
const full=cat.hungerTimer>CAT_FULL_THRESHOLD;
const canMate=(stage===2)&&cat.huntCount>=CAT_BREED_REQUIRE&&cat.breedCooldown===0;
// 救助
if(cat.isHelping){
if(hungry){
if(cat.helpingTarget){
cat.helpingTarget.waitingForHelp=false;
cat.helpingTarget=null;
}
cat.isHelping=false;
cat.carryingPrey=null;
cat.helpAttemptTimer=0;
updateLearning(cat,'rescue',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);
if(isDay) cat.fatigue = Math.min(FATIGUE_MAX, cat.fatigue + FATIGUE_INCREASE_PER_FRAME);
}else{
if(cat.helpingTarget && cat.helpingTarget.waitingForHelp){
cat.helpingTarget.hungerTimer=CAT_HUNGER_MAX;
cat.helpingTarget.waitingForHelp=false;
cat.reputation+=2;
cat.carryingPrey=null;
addFloatingText(cat.x,cat.y-30,"喂食成功","#8f8");
updateLearning(cat,'rescue',true);
changeFamiliarity(cat, cat.helpingTarget, FAMILIARITY_RESCUE_BONUS);
}
cat.helpingTarget=null;
cat.isHelping=false;
cat.helpTargetPos=null;
cat.helpAttemptTimer=0;
}
}else{
cat.isHelping=false;
cat.carryingPrey=null;
cat.helpingTarget=null;
cat.helpAttemptTimer=0;
updateLearning(cat,'rescue',false);
}
}else{
// 救助过程中捕猎(运动)
cat.targetPrey=null;
let nearestPrey=null, minDist=Infinity;
for(const prey of preys){
if(prey.isSleeping) continue;
const d=distance(cat,prey);
if(d>CAT_VIEW_RANGE || isLineBlocked(cat.x,cat.y,prey.x,prey.y)) continue;
const angleToTarget=Math.atan2(prey.y-cat.y,prey.x-cat.x);
let delta=angleToTarget-cat.angle;
while(delta>Math.PI) delta-=2*Math.PI;
while(delta<-Math.PI) delta+=2*Math.PI;
if(Math.abs(delta)<=CAT_VIEW_ANGLE/2 && d<minDist){ minDist=d; nearestPrey=prey; }
}
if(nearestPrey){
cat.targetPrey=nearestPrey;
const d=distance(cat,nearestPrey);
cat.angle=Math.atan2(nearestPrey.y-cat.y,nearestPrey.x-cat.x);
if(d>CAT_HUNT_RANGE){
let moveX, moveY;
if(cat.tryFlank){
const flankOffset=cat.angle+(Math.random()>0.5?0.6:-0.6);
moveX=Math.cos(flankOffset)*cat.speed;
moveY=Math.sin(flankOffset)*cat.speed;
}else{
moveX=(nearestPrey.x-cat.x)/d*cat.speed;
moveY=(nearestPrey.y-cat.y)/d*cat.speed;
}
safeMove(cat,moveX,moveY,currentRadius);
if(isDay) cat.fatigue = Math.min(FATIGUE_MAX, cat.fatigue + FATIGUE_INCREASE_PER_FRAME);
}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;
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;
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);
if(isDay) cat.fatigue = Math.min(FATIGUE_MAX, cat.fatigue + FATIGUE_INCREASE_PER_FRAME * 0.8);
}
}
}
}
continue;
}
// ============================================================
// 未吃饱强制捕猎(含合作触发)
// ============================================================
const isNotFull=!full;
if(isNotFull && stage!==0 && !cat.isHelping && !cat.carryingPrey){
// 检查目标有效性
if(cat.targetPrey && !preys.includes(cat.targetPrey)){
cat.targetPrey=null;
cat.targetLastKnownPos=null;
cat.targetLostTimer=0;
cat.huntMode=null;
}
const nearestPrey=findNearestPrey(cat);
if(nearestPrey){
cat.targetLastKnownPos={x:nearestPrey.x,y:nearestPrey.y};
cat.targetLostTimer=0;
if(!cat.targetPrey && cat.huntMode===null){
let cooperated = false;
for(const other of cats){
if(other === cat || other.isSleeping || other.isCooperative) continue;
if(other.growTimer < CAT_GROW_TIME) continue;
if(other.hungerTimer > CAT_FULL_THRESHOLD) continue;
const fam = getFamiliarity(cat, other);
if(fam < FAMILIARITY_THRESHOLD) continue;
const d2 = distance(cat, other);
if(d2 > 200) continue;
const otherPrey = findNearestPrey(other);
if(otherPrey && otherPrey === nearestPrey){
if(initiateCooperation(cat, other, nearestPrey)){
cooperated = true;
break;
}
}
}
if(!cooperated){
updateLearning(cat,'hunt',false);
cat.targetPrey=nearestPrey;
cat.huntMode='direct';
addFloatingText(cat.x,cat.y-20,'⚡ 强制追击!','#ff8844');
}
}
}
if(cat.isCooperative) continue;
// 直接追击
if(cat.huntMode === 'direct' && cat.targetPrey){
const prey=cat.targetPrey;
if(!preys.includes(prey)){
cat.targetPrey=null;
cat.targetLastKnownPos=null;
cat.targetLostTimer=0;
cat.huntMode=null;
continue;
}
const d=distance(cat,prey);
if(d<CAT_VIEW_RANGE && !isLineBlocked(cat.x,cat.y,prey.x,prey.y)){
const angleToPrey=Math.atan2(prey.y-cat.y, prey.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);
if(isDay) cat.fatigue = Math.min(FATIGUE_MAX, cat.fatigue + FATIGUE_INCREASE_PER_FRAME);
}else{
const idx=preys.indexOf(prey);
if(idx>-1){
preys.splice(idx,1);
cat.huntCount+=1;
cat.hungerTimer=CAT_HUNGER_MAX;
updateLearning(cat,'hunt',true);
addFloatingText(cat.x,cat.y-30,'✅ 捕猎成功!','#8f8');
}
cat.targetPrey=null;
cat.targetLastKnownPos=null;
cat.targetLostTimer=0;
cat.huntMode=null;
}
continue;
}else{
if(cat.targetLastKnownPos){
cat.targetLostTimer++;
if(cat.targetLostTimer>120){
updateLearning(cat,'hunt',false);
cat.policyScore+=PENALTY_ESCAPE;
cat.tryFlank=cat.policyScore<-12;
cat.targetPrey=null;
cat.targetLastKnownPos=null;
cat.targetLostTimer=0;
cat.huntMode=null;
}else{
const target=cat.targetLastKnownPos;
const d2=distance(cat,target);
if(d2>10){
const angleToTarget=Math.atan2(target.y-cat.y,target.x-cat.x);
cat.angle=angleToTarget;
const moveX=Math.cos(angleToTarget)*cat.speed;
const moveY=Math.sin(angleToTarget)*cat.speed;
safeMove(cat,moveX,moveY,currentRadius);
if(isDay) cat.fatigue = Math.min(FATIGUE_MAX, cat.fatigue + FATIGUE_INCREASE_PER_FRAME * 0.6);
}else{
cat.wanderTimer-=1;
if(cat.wanderTimer<=0){
cat.angle=Math.random()*2*Math.PI;
cat.wanderTimer=30+Math.random()*60;
}
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(isDay) cat.fatigue = Math.min(FATIGUE_MAX, cat.fatigue + FATIGUE_INCREASE_PER_FRAME * 0.5);
}
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);
if(isDay) cat.fatigue = Math.min(FATIGUE_MAX, cat.fatigue + FATIGUE_INCREASE_PER_FRAME * 0.3);
continue;
}
}
}
// 如果没有目标,闲逛
if(!cat.targetPrey && !cat.huntMode){
cat.wanderTimer-=1;
if(cat.wanderTimer<=0){
cat.angle=Math.random()*2*Math.PI;
cat.wanderTimer=100+Math.random()*180;
}
const moveX=Math.cos(cat.angle)*cat.speed*0.7;
const moveY=Math.sin(cat.angle)*cat.speed*0.7;
safeMove(cat,moveX,moveY,currentRadius);
if(isDay) cat.fatigue = Math.min(FATIGUE_MAX, cat.fatigue + FATIGUE_INCREASE_PER_FRAME * 0.2);
}
// 检查疲劳是否达到睡眠阈值
if(isDay && cat.fatigue >= FATIGUE_SLEEP_THRESHOLD && !cat.isSleeping && !cat.isHelping && !cat.isCooperative){
cat.isSleeping = true;
cat.isSleepingByFatigue = true;
addFloatingText(cat.x, cat.y-20, '😴 累倒了', '#ffaa88');
}
continue;
}
// ===== 吃饱后决策 =====
if(full){
if(canMate){
let nearestMate=null, 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 fam = getFamiliarity(cat, other);
const distanceWeight = 1 - (distance(cat, other) / CAT_VIEW_RANGE);
const score = distanceWeight * 0.5 + (fam/100) * 0.5;
if(score < 0.3) 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);
if(isDay) cat.fatigue = Math.min(FATIGUE_MAX, cat.fatigue + FATIGUE_INCREASE_PER_FRAME * 0.5);
}else{
const father=cat.gender==="male"?cat:nearestMate;
const mother=cat.gender==="female"?cat:nearestMate;
const success=performMating(father,mother);
cat.targetMate=null;
if(cat.mate) cat.mate.targetMate=null;
if(!success){
cat.breedCooldown=60;
updateLearning(cat,'mate',false);
}
}
continue;
}
}
// 救助(优先熟悉的)
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 fam = getFamiliarity(cat, other);
if(fam < FAMILIARITY_THRESHOLD/2) 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");
// 救助行动(移动)
const d=distance(cat,rescueTarget);
if(d>30){
const angleToTarget=Math.atan2(rescueTarget.y-cat.y,rescueTarget.x-cat.x);
cat.angle=angleToTarget;
const moveX=Math.cos(angleToTarget)*cat.speed;
const moveY=Math.sin(angleToTarget)*cat.speed;
safeMove(cat,moveX,moveY,currentRadius);
if(isDay) cat.fatigue = Math.min(FATIGUE_MAX, cat.fatigue + FATIGUE_INCREASE_PER_FRAME);
}
continue;
}
// 筑巢
if(!cat.nest && stage===2){
if(Math.random()<0.5){
if(tryBuildNest(cat)) continue;
}
}
// 闲逛(移动)
cat.wanderTimer-=1;
if(cat.wanderTimer<=0){
cat.angle=Math.random()*2*Math.PI;
cat.wanderTimer=100+Math.random()*180;
}
const moveX=Math.cos(cat.angle)*cat.speed*0.7;
const moveY=Math.sin(cat.angle)*cat.speed*0.7;
safeMove(cat,moveX,moveY,currentRadius);
if(isDay) cat.fatigue = Math.min(FATIGUE_MAX, cat.fatigue + FATIGUE_INCREASE_PER_FRAME * 0.2);
}
// 非白天的疲劳恢复
if(!isDay && !cat.isSleeping){
cat.fatigue = Math.max(0, cat.fatigue - FATIGUE_RECOVER_IDLE * 2);
}
// 静止时恢复(如果猫没有移动,由上面逻辑处理)
// 检查疲劳并强制睡眠(白天)
if(isDay && cat.fatigue >= FATIGUE_SLEEP_THRESHOLD && !cat.isSleeping && !cat.isHelping && !cat.isCooperative){
cat.isSleeping = true;
cat.isSleepingByFatigue = true;
addFloatingText(cat.x, cat.y-20, '😴 累倒了', '#ffaa88');
}
}
}
// ---------- 重置检测 ----------
function checkReset(){ if(preys.length===0 || cats.length===0){ if(resetNoticeTimer===0){ resetWorld(); } } }
// ---------- 主更新 ----------
function updateWorld(){ herbRespawnTimer++; if(herbRespawnTimer>=HERB_RESPAWN_INTERVAL){ herbRespawnTimer=0; trySpawnHerb(); } updateFloatingTexts(); updateDayNight(); if(resetNoticeTimer>0){ resetNoticeTimer--; if(resetNoticeTimer===0){ resetNotice.style.opacity='0'; } } }
// ---------- 面板 ----------
function updatePopulationPanel(){ let adult=0, kid=0; for(const c of cats) if(c.growTimer>=CAT_GROW_TIME) adult++; else kid++; let followText="无"; if(followedCat){ const genderText=followedCat.gender==="male"?"公猫":"母猫"; followText=`${genderText} | 捕猎:${followedCat.huntCount}`; } populationPanel.textContent=`猎物:${preys.length} / 草:${herbs.length} / 成年猫:${adult} / 幼猫:${kid} / 跟随:${followText}`; }
function updateDetailPanel(){ if(followedCat && !cats.includes(followedCat)){ followedCat=null; } if(!followedCat){ detailPanel.textContent="点击一只猫查看详情"; return; } const c=followedCat; const progress=c.growTimer/CAT_GROW_TIME; let stageStr="成年"; if(progress<0.33) stageStr="幼崽"; else if(progress<0.66) stageStr="幼猫"; const hungerStr=c.hungerTimer<CAT_HUNGER_STATE?"饥饿":(c.hungerTimer>CAT_FULL_THRESHOLD?"吃饱":"未吃饱"); const genderStr=c.gender==="male"?"公":"母"; const statusParts=[]; if(c.isLactating) statusParts.push("哺乳中"); if(c.carryingPrey) statusParts.push("携带猎物"); if(c.mate) statusParts.push("有配偶"); if(c.isHelping) statusParts.push("救助中"); if(c.waitingForHelp) statusParts.push("等待救助"); if(c.isSleeping) statusParts.push("💤 睡觉"); if(c.nest) statusParts.push("有窝"); if(c.isCooperative) statusParts.push("合作中"); const statusStr=statusParts.length?statusParts.join("、"):"无特殊"; const learn=c.learning; const huntRate=learn.hunt.attempts>0?(learn.hunt.success/learn.hunt.attempts*100).toFixed(0):'N/A'; const rescueRate=learn.rescue.attempts>0?(learn.rescue.success/learn.rescue.attempts*100).toFixed(0):'N/A'; const mateRate=learn.mate.attempts>0?(learn.mate.success/learn.mate.attempts*100).toFixed(0):'N/A'; const nestRate=learn.nest.attempts>0?(learn.nest.success/learn.nest.attempts*100).toFixed(0):'N/A'; const traits=c.traits; const traitStr=`性格: 大胆${(traits.boldness*100).toFixed(0)}% 耐心${(traits.patience*100).toFixed(0)}% 速度${(traits.speedOffset*100).toFixed(0)}%`; const familiarCount = Object.values(c.familiarity).filter(v => v >= FAMILIARITY_THRESHOLD).length; const fatigueStr = `疲劳: ${Math.round(c.fatigue)}%`; const detail=`姓名:${c.name} | 性别:${genderStr} | 年龄:${stageStr} | 饥饿:${hungerStr} | 捕猎:${c.huntCount} | 幼崽:${c.kittens.length} | 声望:${c.reputation} | 状态:${statusStr} | 捕猎成功率: ${huntRate}% | 救助率: ${rescueRate}% | 求偶率: ${mateRate}% | 筑巢率: ${nestRate}% | ${traitStr} | 熟悉伙伴: ${familiarCount}只 | ${fatigueStr}`; detailPanel.textContent=detail; }
// ---------- 渲染 ----------
function renderMap(){ ctx.clearRect(0,0,canvas.width,canvas.height); ctx.save(); if(followedCat){ const targetX=canvas.width/2-followedCat.x*scale; const targetY=canvas.height/2-followedCat.y*scale; offsetX+=(targetX-offsetX)*0.08; offsetY+=(targetY-offsetY)*0.08; } ctx.translate(offsetX,offsetY); ctx.scale(scale,scale);
ctx.fillStyle=GRASS_COLOR; ctx.fillRect(-3000,-3000,6000,6000);
ctx.beginPath(); ctx.setLineDash(DASH_PATTERN); ctx.strokeStyle=GRID_COLOR; ctx.globalAlpha=GRID_ALPHA; ctx.lineWidth=1; const p1=screenToWorld(0,0); const p2=screenToWorld(canvas.width,canvas.height); const startX=Math.floor(p1.x/GRID_SIZE)*GRID_SIZE; const startY=Math.floor(p1.y/GRID_SIZE)*GRID_SIZE; for(let x=startX;x<=p2.x;x+=GRID_SIZE){ctx.moveTo(x,p1.y);ctx.lineTo(x,p2.y);} for(let y=startY;y<=p2.y;y+=GRID_SIZE){ctx.moveTo(p1.x,y);ctx.lineTo(p2.x,y);} ctx.stroke(); ctx.globalAlpha=1; ctx.setLineDash([]);
ctx.fillStyle=HERB_COLOR; for(const herb of herbs){ ctx.beginPath(); ctx.moveTo(herb.x,herb.y-HERB_SIZE); ctx.lineTo(herb.x-HERB_SIZE*0.7,herb.y+HERB_SIZE*0.6); ctx.lineTo(herb.x+HERB_SIZE*0.7,herb.y+HERB_SIZE*0.6); ctx.closePath(); ctx.fill(); }
ctx.fillStyle="rgba(0,80,0,0.3)"; for(const bush of bushes){ ctx.beginPath(); ctx.arc(bush.x,bush.y,BUSH_RADIUS,0,2*Math.PI); ctx.fill(); ctx.strokeStyle="rgba(0,200,0,0.4)"; ctx.lineWidth=1; ctx.setLineDash([4,4]); ctx.stroke(); ctx.setLineDash([]); }
ctx.fillStyle="#1a4a1a"; for(const thorn of thorns){ ctx.beginPath(); ctx.arc(thorn.x,thorn.y,THORN_RADIUS,0,2*Math.PI); ctx.fill(); ctx.strokeStyle="#000"; ctx.lineWidth=3; const off=THORN_RADIUS*0.6; ctx.beginPath(); ctx.moveTo(thorn.x-off,thorn.y-off); ctx.lineTo(thorn.x+off,thorn.y+off); ctx.moveTo(thorn.x+off,thorn.y-off); ctx.lineTo(thorn.x-off,thorn.y+off); ctx.stroke(); }
ctx.fillStyle=TREE_COLOR; for(const tree of trees){ ctx.beginPath(); ctx.arc(tree.x,tree.y,TREE_RADIUS,0,2*Math.PI); ctx.fill(); }
for(const prey of preys){ const color=prey.eatenHerbs>=FEED_REQUIRE?(prey.targetMate?"#555":"#222"):"#111"; ctx.fillStyle=color; ctx.beginPath(); ctx.arc(prey.x,prey.y,PREY_RADIUS,0,2*Math.PI); ctx.fill(); }
for(const cat of cats){ const progress=Math.min(1,cat.growTimer/CAT_GROW_TIME); let sf=1; if(progress<0.33) sf=0.4; else if(progress<0.66) sf=0.7; else sf=1.0; const r=CAT_RADIUS*sf; const isAdult=progress>=1; const full=cat.hungerTimer>CAT_FULL_THRESHOLD; const canBreed=isAdult && cat.huntCount>=CAT_BREED_REQUIRE && cat.breedCooldown===0 && full; if(cat===followedCat){ ctx.beginPath(); ctx.strokeStyle="#fff"; ctx.lineWidth=3; ctx.arc(cat.x,cat.y,r+10,0,2*Math.PI); ctx.stroke(); } if(canBreed){ ctx.beginPath(); ctx.strokeStyle="#ff88bb"; ctx.lineWidth=2; ctx.arc(cat.x,cat.y,r+6,0,2*Math.PI); ctx.stroke(); } if(cat.hungerTimer<CAT_HUNGER_STATE){ ctx.beginPath(); ctx.strokeStyle="rgba(255,0,0,0.6)"; ctx.lineWidth=2; ctx.arc(cat.x,cat.y,r+4,0,2*Math.PI); ctx.stroke(); } let color=CAT_COLOR; if(cat.carryingPrey) color="#3a86ff"; if(cat.waitingForHelp) color="#ffaa44"; if(cat.isSleeping) color="#555555"; if(cat.isCooperative) color="#44aaff"; // 疲劳视觉提示:疲劳高时颜色变暗
if(cat.fatigue > 70) color = "#a67c52";
else if(cat.fatigue > 40) color = "#c9975a";
ctx.fillStyle=color; ctx.beginPath(); ctx.arc(cat.x,cat.y,r,0,2*Math.PI); ctx.fill(); ctx.fillStyle="#995511"; ctx.beginPath(); ctx.arc(cat.x+Math.cos(cat.angle)*(r-3),cat.y+Math.sin(cat.angle)*(r-3),3,0,2*Math.PI); ctx.fill(); }
for(const nest of nests){ ctx.setLineDash([5,5]); ctx.strokeStyle="#ffffff"; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(nest.x,nest.y,CAT_RADIUS+4,0,2*Math.PI); ctx.stroke(); ctx.setLineDash([]); }
ctx.strokeStyle="#000"; ctx.lineWidth=4; ctx.strokeRect(WORLD_BOUND.left,WORLD_BOUND.top,WORLD_BOUND.right-WORLD_BOUND.left,WORLD_BOUND.bottom-WORLD_BOUND.top);
ctx.restore();
if(!isDay){ ctx.save(); ctx.fillStyle="rgba(0,0,0,0.6)"; ctx.fillRect(0,0,canvas.width,canvas.height); ctx.restore(); }
for(const cat of cats){ if(cat.isSleeping) continue; const progress=Math.min(1,cat.growTimer/CAT_GROW_TIME); let sf=1; if(progress<0.33) sf=0.4; else if(progress<0.66) sf=0.7; else sf=1.0; const viewRange=CAT_VIEW_RANGE*sf*scale; const sx=cat.x*scale+offsetX; const sy=cat.y*scale+offsetY; ctx.save(); ctx.beginPath(); ctx.moveTo(sx,sy); const startAngle=cat.angle-CAT_VIEW_ANGLE/2; const endAngle=cat.angle+CAT_VIEW_ANGLE/2; ctx.arc(sx,sy,viewRange,startAngle,endAngle); ctx.closePath(); ctx.fillStyle="rgba(242,166,90,0.12)"; ctx.fill(); ctx.strokeStyle="rgba(242,166,90,0.35)"; ctx.lineWidth=1; ctx.stroke(); ctx.restore(); }
for(const prey of preys){ if(prey.isSleeping) continue; const viewRange=PREY_VIEW_RANGE*scale; const sx=prey.x*scale+offsetX; const sy=prey.y*scale+offsetY; ctx.save(); ctx.beginPath(); ctx.moveTo(sx,sy); const startAngle=prey.angle-PREY_VIEW_ANGLE/2; const endAngle=prey.angle+PREY_VIEW_ANGLE/2; ctx.arc(sx,sy,viewRange,startAngle,endAngle); ctx.closePath(); ctx.fillStyle="rgba(0,0,0,0.07)"; ctx.fill(); ctx.strokeStyle="rgba(0,0,0,0.14)"; ctx.lineWidth=1; ctx.stroke(); ctx.restore(); }
for(const cat of cats){ const sx=cat.x*scale+offsetX; const sy=cat.y*scale+offsetY; const progress=Math.min(1,cat.growTimer/CAT_GROW_TIME); let sf=1; if(progress<0.33) sf=0.4; else if(progress<0.66) sf=0.7; else sf=1.0; const r=CAT_RADIUS*sf*scale; const symbol=cat.gender==="male"?MALE_SYMBOL:FEMALE_SYMBOL; const text=`(${symbol}) ${cat.name}`; ctx.save(); ctx.font="bold 14px system-ui"; ctx.textAlign="center"; ctx.textBaseline="bottom"; ctx.fillStyle="#fff"; ctx.shadowColor="rgba(0,0,0,0.9)"; ctx.shadowBlur=6; ctx.fillText(text,sx,sy-r-6); ctx.restore(); }
for(const nest of nests){ const sx=nest.x*scale+offsetX; const sy=nest.y*scale+offsetY; ctx.save(); ctx.font="12px system-ui"; ctx.textAlign="center"; ctx.textBaseline="bottom"; ctx.fillStyle="#ddd"; ctx.shadowColor="rgba(0,0,0,0.8)"; ctx.shadowBlur=4; ctx.fillText(nest.owner.name+" 的窝",sx,sy-(CAT_RADIUS+4)*scale-6); ctx.restore(); }
for(const ft of floatingTexts){ const sx=ft.x*scale+offsetX; const sy=ft.y*scale+offsetY; ctx.save(); ctx.globalAlpha=ft.alpha; ctx.font="bold 20px system-ui"; ctx.textAlign="center"; ctx.textBaseline="bottom"; ctx.shadowColor="rgba(0,0,0,0.9)"; ctx.shadowBlur=8; ctx.fillStyle=ft.color||"#ffd700"; ctx.fillText(ft.text,sx,sy); ctx.restore(); }
for(const prey of preys){ if(prey.isSleeping){ const sx=prey.x*scale+offsetX; const sy=prey.y*scale+offsetY; ctx.save(); ctx.font="12px system-ui"; ctx.textAlign="center"; ctx.textBaseline="bottom"; ctx.fillStyle="#ccc"; ctx.shadowColor="rgba(0,0,0,0.8)"; ctx.shadowBlur=4; ctx.fillText("Z",sx,sy-PREY_RADIUS*scale-2); ctx.restore(); } }
for(const cat of cats){ if(cat.isSleeping){ const sx=cat.x*scale+offsetX; const sy=cat.y*scale+offsetY; const progress=Math.min(1,cat.growTimer/CAT_GROW_TIME); let sf=1; if(progress<0.33) sf=0.4; else if(progress<0.66) sf=0.7; else sf=1.0; const r=CAT_RADIUS*sf*scale; ctx.save(); ctx.font="18px system-ui"; ctx.textAlign="center"; ctx.textBaseline="bottom"; ctx.fillStyle="#aaddff"; ctx.shadowColor="rgba(0,0,0,0.8)"; ctx.shadowBlur=4; ctx.fillText("💤",sx,sy-r-6); ctx.restore(); } } }
// ---------- 交互 ----------
function getMouseScreenPos(e){ if(e.touches) return {x:e.touches[0].clientX, y:e.touches[0].clientY}; return {x:e.clientX, y:e.clientY}; }
function handlePointerDown(e){ const pos=getMouseScreenPos(e); pointerDownX=pos.x; pointerDownY=pos.y; pointerDownTime=Date.now(); isClick=true; }
function handlePointerUp(e){ if(!isClick) return; const pos=getMouseScreenPos(e); const dx=pos.x-pointerDownX; const dy=pos.y-pointerDownY; const dist=Math.hypot(dx,dy); const elapsed=Date.now()-pointerDownTime; if(dist<10 && elapsed<300){ const canvasPos=getCanvasCoords(pos.x,pos.y); const world=screenToWorld(canvasPos.x,canvasPos.y); const pixelRatio=getPixelRatio(); let clicked=null; for(const cat of cats){ const progress=Math.min(1,cat.growTimer/CAT_GROW_TIME); let sf=1; if(progress<0.33) sf=0.4; else if(progress<0.66) sf=0.7; else sf=1.0; const r=CAT_RADIUS*sf; const hitRadius=Math.max(30,30/pixelRatio); if(Math.hypot(world.x-cat.x,world.y-cat.y)<=r+hitRadius){ clicked=cat; break; } } if(clicked){ followedCat=clicked; isDragging=false; return; }else{ followedCat=null; } } }
function handlePointerMove(e){ const pos=getMouseScreenPos(e); if(isClick){ const dx=pos.x-pointerDownX; const dy=pos.y-pointerDownY; if(Math.hypot(dx,dy)>10){ isClick=false; isDragging=true; dragStartX=pointerDownX; dragStartY=pointerDownY; startOffsetX=offsetX; startOffsetY=offsetY; } } if(isDragging){ const pixelRatio=getPixelRatio(); const dx=(pos.x-dragStartX)*pixelRatio; const dy=(pos.y-dragStartY)*pixelRatio; offsetX=startOffsetX+dx; offsetY=startOffsetY+dy; } }
function handlePointerCancel(){ isDragging=false; isClick=false; }
canvas.addEventListener("mousedown",handlePointerDown); window.addEventListener("mousemove",handlePointerMove); window.addEventListener("mouseup",handlePointerUp);
canvas.addEventListener("touchstart",(e)=>{ if(e.touches.length===1){ const touch=e.touches[0]; handlePointerDown({clientX:touch.clientX,clientY:touch.clientY}); }else if(e.touches.length===2){ lastTouchDist=Math.hypot(e.touches[0].clientX-e.touches[1].clientX, e.touches[0].clientY-e.touches[1].clientY); isDragging=false; isClick=false; } }, {passive:true});
canvas.addEventListener("touchmove",(e)=>{ e.preventDefault(); if(e.touches.length===1){ const touch=e.touches[0]; handlePointerMove({clientX:touch.clientX,clientY:touch.clientY}); }else if(e.touches.length===2){ const dist=Math.hypot(e.touches[0].clientX-e.touches[1].clientX, e.touches[0].clientY-e.touches[1].clientY); const cx=(e.touches[0].clientX+e.touches[1].clientX)/2; const cy=(e.touches[0].clientY+e.touches[1].clientY)/2; const canvasPos=getCanvasCoords(cx,cy); const wBefore=screenToWorld(canvasPos.x,canvasPos.y); const ratio=dist/lastTouchDist; scale=Math.max(MIN_SCALE,Math.min(MAX_SCALE,scale*ratio)); lastTouchDist=dist; const wAfter=screenToWorld(canvasPos.x,canvasPos.y); offsetX+=(wAfter.x-wBefore.x)*scale; offsetY+=(wAfter.y-wBefore.y)*scale; } }, {passive:false});
canvas.addEventListener("touchend",(e)=>{ if(e.touches.length===0){ const touch=e.changedTouches[0]; if(touch) handlePointerUp({clientX:touch.clientX,clientY:touch.clientY}); else handlePointerUp({clientX:pointerDownX,clientY:pointerDownY}); } });
canvas.addEventListener("touchcancel",handlePointerCancel);
canvas.addEventListener("wheel",(e)=>{ e.preventDefault(); const pos={x:e.clientX,y:e.clientY}; const canvasPos=getCanvasCoords(pos.x,pos.y); const wBefore=screenToWorld(canvasPos.x,canvasPos.y); const zoom=e.deltaY>0?-0.12:0.12; scale=Math.max(MIN_SCALE,Math.min(MAX_SCALE,scale+zoom)); const wAfter=screenToWorld(canvasPos.x,canvasPos.y); offsetX+=(wAfter.x-wBefore.x)*scale; offsetY+=(wAfter.y-wBefore.y)*scale; }, {passive:false});
// ---------- 控制 ----------
function setSpeed(s){ speedMultiplier=s; document.querySelectorAll('.controls button').forEach(btn=>btn.classList.remove('active')); const map={1:'speed1x',2:'speed2x',5:'speed5x'}; const el=document.getElementById(map[s]); if(el) el.classList.add('active'); }
window.setSpeed=setSpeed;
function resetWorld(){ trees.length=0; herbs.length=0; preys.length=0; cats.length=0; bushes.length=0; thorns.length=0; nests.length=0; floatingTexts=[]; followedCat=null; isDay=true; dayTimer=0; nightTimer=0; timeDisplay.textContent="☀️ 白天"; offsetX=0; offsetY=0; scale=1.0; generateTrees(); generateHerbs(); generatePreys(); generateBushes(); generateThorns(); generateCats(); resetNotice.style.opacity='1'; resetNoticeTimer=RESET_NOTICE_DURATION; debugInfo.textContent = `初始化完成:树${trees.length} 草${herbs.length} 猎物${preys.length} 猫${cats.length}`; }
window.resetWorld=resetWorld;
// ---------- 主循环 ----------
function gameLoop(){ checkReset(); for(let t=0;t<speedMultiplier;t++){ updateWorld(); updatePreys(); updateCats(); } updatePopulationPanel(); updateDetailPanel(); renderMap(); requestAnimationFrame(gameLoop); }
// ---------- 初始化 ----------
debugInfo.textContent = "正在初始化...";
generateTrees(); generateHerbs(); generatePreys(); generateBushes(); generateThorns(); generateCats();
debugInfo.textContent = `初始化完成:树${trees.length} 草${herbs.length} 猎物${preys.length} 猫${cats.length}`;
gameLoop();
</script>
</body>
</html>Game Source: 猫武士 · 疲劳系统完整版
Creator: EpicCoder88
Libraries: none
Complexity: complex (764 lines, 71.0 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-epiccoder88-msoiob7s" to link back to the original. Then publish at arcadelab.ai/publish.