游戏简单演示
游戏介绍
一款单机 2D 吃球生存小游戏,玩法类似《球球大作战 / Agar.io》。你操控球体在巨型地图中吞噬食物和更小的敌人,不断变大冲击排行榜。W 分裂冲刺追杀/逃命,S 主动合并收回分身;E/右键发射粘液弹减速命中目标;小心病毒刺球会让大球爆裂。地图随机刷新多种道具(加速、护盾、磁铁、冲刺、双倍、减速光环等),配合操作打出节奏,目标只有一个:活下去,变成最大的那颗球。
这个小游戏是怎么生成出来的?
需要先说明一句:这个完整可玩的 2D 吃球小游戏,并不是手写代码完成的。
我只是通过在线生成环境,用自然语言把玩法和规则一步步描述清楚,包括移动手感、AI 行为、病毒机制、分裂与合并、粘液喷射等细节,再通过多轮生成不断调整,最终直接得到了一个能在浏览器运行的完整 index.html 游戏文件。
我用的生成环境就是下面这套,几个入口本质是同一套服务,只是线路不同,哪个快用哪个即可:
- https://share.zhangsan.cool
- https://share-hk.zhangsan.cool
- https://share.searchknowledge.cloud
- https://hello.aiforme.cloud
整个过程中不需要本地环境、不需要任何依赖,也不用拆分文件结构,对这种偏玩法、偏展示的项目来说非常省事。更重要的是,它并不是一次性生成,而是可以在原代码基础上持续往上加规则、加机制,把一个想法慢慢"逼"成现在这个完成度。
参考提示词
请用原生 HTML5 + CSS + JavaScript 实现一个类似《球球大作战 / Agar.io》的 2D 单机吃球小游戏,不依赖任何第三方库或打包工具,只输出一个可直接在浏览器打开的 index.html。
硬性功能要求:
使用
<canvas>全屏渲染地图,世界为大平面;摄像机跟随玩家(以玩家整体中心为镜头中心)。玩家球由鼠标控制移动:具备加速度、最大速度限制与平滑插值;体积越大最大速度越低;加入粘性阻尼(viscous damping)。
地图初始化大量食物小圆点;被吃后玩家/AI 增加质量(面积模型 πr²);只对已被吃掉的食物复活,60 秒后在随机位置复活,不额外无限补球。
生成若干 Bot AI 球:自动寻找附近食物发育;追击更小目标;遇到更大玩家/敌人会逃离;会规避病毒;AI 也有名字和分数。
实现圆形碰撞检测与吞噬:当小球被大球几乎完全覆盖(d + r_small <= r_big*0.98)且体积差超过阈值时判定被吃,质量转移。
W 分身:玩家按 W 分裂(每颗质量减半),新分身朝鼠标方向高速冲刺并有更精确的速度衰减曲线;分裂数量上限与冷却。
S 主动合并:玩家按 S 清除合并冷却并触发"慢慢加速靠拢合并"机制(分身向中心被拉回并重叠后合并)。
喷射玩法:E 或右键喷射"粘液弹 Slime Shot",消耗少量质量:
- 粘液弹可被更大球吃掉;
- 命中球体会施加明显减速/粘性阻尼持续约 2.5 秒;
- 可用于追杀/逃命/控 AI;
- 粘液弹也能喂病毒。
病毒/刺球系统:
- 大球撞上病毒会爆裂为多分身;
- 病毒可被粘液弹喂食,喂到阈值会分裂出新病毒并朝最后喂食方向喷射;喷射速度有平滑衰减曲线。
视野随体积变化:玩家总质量越大,镜头缩放越小(看得更远)。
边界软限制/回弹:世界有硬边界,靠近边缘会被柔性推回并带轻微回弹,不能穿出地图。
道具系统(随机刷新、道具更大、带名称显示 + 拾取 Toast + Buff 列表显示余时):至少实现 8 种有意义道具:
- ⚡加速(8s 提升最大速度)
- 🛡护盾(抵挡一次被吃)
- 🧲磁铁(10s 吸附近食物)
- 💨冲刺(瞬发向鼠标方向强冲)
- ✨双倍吞噬(12s 吃到的质量×2)
- ❄减速光环(10s 附近 AI 减速)
- 🌀分裂充能(清 W 冷却 + 清合并冷却)
- 🧪粘液强化(10s 粘液弹更大更远更久减速)
- UI:显示当前质量/得分、分身数、AI/病毒/食物/道具数量;排行榜 Top5(含 AI 名字与分数);开始/重新开始按钮;小地图 minimap。
代码与结构要求:
- 面向对象/模块化风格:如 Game、PlayerCell、Bot、Food、SlimeBall、Virus、PowerUp 等类。
- 对核心算法/关键函数添加简要注释。
- 输出必须是完整单文件 index.html,复制后浏览器即可运行。
**注意:**不要使用第三方库、不要拆分文件、不要使用构建工具。
最终完整代码
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>吃球小游戏</title>
<style>
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #0b0f14; }
canvas { display: block; width: 100vw; height: 100vh; }
#ui {
position: fixed; left: 12px; top: 12px; z-index: 10; color: #e6edf3;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "PingFang SC", "Microsoft Yahei", sans-serif;
user-select: none; pointer-events: none; text-shadow: 0 2px 8px rgba(0,0,0,.6);
}
#stats, #leaderboard {
background: rgba(0,0,0,.35); padding: 8px 10px; border-radius: 10px; font-size: 14px;
border: 1px solid rgba(255,255,255,.06); backdrop-filter: blur(4px);
display: block; pointer-events: none;
margin-bottom: 8px;
min-width: 280px;
}
#leaderboard h4{ margin:0 0 6px 0; font-size:13px; opacity:.9; font-weight:600; }
#leaderboard ol{ margin:0; padding-left:18px; line-height:1.55; }
#leaderboard li{ white-space:nowrap; }
#leaderboard .me{ color:#7dd3fc; font-weight:700; }
#controls { position: fixed; right: 12px; top: 12px; z-index: 10; display: flex; gap: 8px; pointer-events: auto; }
button {
background: rgba(255,255,255,.08); color: #e6edf3; border: 1px solid rgba(255,255,255,.12);
padding: 8px 12px; border-radius: 10px; cursor: pointer; font-size: 14px;
transition: .15s ease; backdrop-filter: blur(4px);
}
button:hover { transform: translateY(-1px); background: rgba(255,255,255,.12); }
#hint {
position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
z-index: 10; color: #e6edf3; text-align: center; font-size: 18px;
background: rgba(0,0,0,.55); padding: 16px 18px; border-radius: 14px;
border: 1px solid rgba(255,255,255,.08);
}
#hint small { display: block; opacity: .8; margin-top: 6px; font-size: 13px; }
#hint.hidden { display: none; }
#minimap {
position: fixed; right: 12px; bottom: 12px; z-index: 10;
width: 160px; height: 160px; background: rgba(0,0,0,.35);
border: 1px solid rgba(255,255,255,.08); border-radius: 12px;
pointer-events: none;
}
#toast {
position: fixed; left: 50%; top: 14%;
transform: translateX(-50%);
z-index: 20; color: #e6edf3; font-size: 18px; font-weight: 700;
background: rgba(0,0,0,.6); padding: 10px 14px; border-radius: 12px;
border: 1px solid rgba(255,255,255,.12);
opacity: 0; transition: .2s ease; pointer-events: none; white-space: nowrap;
}
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="ui">
<div id="stats">
质量:<span id="mass">0</span>
分身数:<span id="cells">1</span>
得分:<span id="score">0</span>
AI:<span id="bots">0</span>
病毒:<span id="viruses">0</span>
食物:<span id="foods">0</span>
道具:<span id="items">0</span>
<div style="opacity:.8;margin-top:4px;font-size:12px">
W 分身 / S 合并 / E或右键 粘液喷射(减速命中目标) / 食物60s复活
</div>
<div id="buffs" style="opacity:.95;margin-top:4px;font-size:12px"></div>
</div>
<div id="leaderboard">
<h4>排行榜 Top5</h4>
<ol id="lbList"></ol>
</div>
</div>
<div id="controls">
<button id="startBtn">开始</button>
<button id="restartBtn">重新开始</button>
</div>
<div id="hint">
点击"开始"进入游戏<br/>
<small>鼠标控制方向;W 分身;S 合并;E/右键粘液喷射</small>
</div>
<div id="toast"></div>
<canvas id="minimap"></canvas>
<script>
(() => {
// ===== utils =====
const rand = (a, b) => a + Math.random() * (b - a);
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
const dist2 = (ax, ay, bx, by) => { const dx=ax-bx, dy=ay-by; return dx*dx+dy*dy; };
const dist = (ax, ay, bx, by) => Math.hypot(ax-bx, ay-by);
const lerp=(a,b,t)=>a+(b-a)*t;
const hsl=(h,s,l)=>`hsl(${h},${s}%,${l}%)`;
const BOT_NAMES = [
"Aqua","Blaze","Coco","Duke","Echo","Frost","Gizmo","Hana","Iris","Jade",
"Kite","Luna","Mango","Nova","Odin","Pico","Quinn","Rex","Sora","Tango",
"Uma","Vivi","Wisp","Xeno","Yuki","Zane","Kitty","Bobo","Mimi","Toto"
];
// ===== Entities =====
class Entity {
constructor(x,y,r,color){
this.x=x; this.y=y; this.vx=0; this.vy=0;
this.mass=Math.PI*r*r; this.r=r; this.color=color;
this.alive=true; this.name=""; this.mergeCooldown=0;
this.boostAge=0; this.boostDuration=0;
this.slowTimer=0; // 被粘液减速的剩余时间
}
setMass(m){ this.mass=Math.max(1,m); this.r=Math.sqrt(this.mass/Math.PI); }
addMass(dm){ this.setMass(this.mass+dm); }
getMaxSpeed(game){
const base=300;
let spd=base/Math.sqrt(this.r/16);
if(this.isPlayer && game.speedBoostTimer>0) spd*=game.speedBoostMul;
if(this.slowTimer>0) spd*=game.slowMul;
if(this.isBot && game.freezeTimer>0){
const pc=game.playerCenter();
if(dist(pc.x,pc.y,this.x,this.y)<game.freezeRadius) spd*=game.freezeMul;
}
return spd;
}
applyViscousDamping(dt){
const k=0.0009*(this.r/20);
const factor=Math.exp(-k*dt*60);
this.vx*=factor; this.vy*=factor;
}
applyBoost(vx,vy,duration=0.9){
this.vx=vx; this.vy=vy; this.boostDuration=duration; this.boostAge=0;
}
applyBoostDecay(dt){
if(this.boostDuration<=0) return;
this.boostAge+=dt;
const t=clamp(this.boostAge/this.boostDuration,0,1);
const drag=lerp(0.015,0.30,t*t);
const factor=Math.exp(-drag*dt*60);
this.vx*=factor; this.vy*=factor;
if(t>=1) this.boostDuration=0;
}
moveToward(dx,dy,dt,game){
const len=Math.hypot(dx,dy)||1;
const ux=dx/len, uy=dy/len;
const maxSpd=this.getMaxSpeed(game);
const desiredVx=ux*maxSpd, desiredVy=uy*maxSpd;
const smooth=1-Math.pow(0.001,dt);
this.vx=lerp(this.vx,desiredVx,smooth);
this.vy=lerp(this.vy,desiredVy,smooth);
const spd=Math.hypot(this.vx,this.vy);
if(spd>maxSpd){ this.vx=this.vx/spd*maxSpd; this.vy=this.vy/spd*maxSpd; }
}
updateBase(dt,game){
this.applyBoostDecay(dt);
this.applyViscousDamping(dt);
if(this.slowTimer>0) this.slowTimer-=dt;
this.x+=this.vx*dt; this.y+=this.vy*dt;
game.applySoftBounds(this,dt);
if(this.mergeCooldown>0) this.mergeCooldown-=dt;
}
draw(ctx){
ctx.beginPath();
ctx.fillStyle=this.color;
ctx.arc(this.x,this.y,this.r,0,Math.PI*2);
ctx.fill();
ctx.lineWidth=Math.max(1.2,this.r*0.06);
ctx.strokeStyle="rgba(0,0,0,0.35)";
ctx.stroke();
if(this.name){
ctx.fillStyle="rgba(255,255,255,0.9)";
ctx.font=`${Math.max(12,this.r*0.6)}px sans-serif`;
ctx.textAlign="center"; ctx.textBaseline="middle";
ctx.fillText(this.name,this.x,this.y);
}
// 减速圈提示
if(this.slowTimer>0){
ctx.beginPath();
ctx.strokeStyle="rgba(140,200,255,0.35)";
ctx.lineWidth=3;
ctx.arc(this.x,this.y,this.r*1.12,0,Math.PI*2);
ctx.stroke();
}
}
}
class PlayerCell extends Entity{
constructor(x,y,r,color){ super(x,y,r,color); this.isPlayer=true; }
update(dt,game){
const dx=game.inputX-game.w/2, dy=game.inputY-game.h/2;
if(Math.hypot(dx,dy)>5) this.moveToward(dx,dy,dt,game);
this.updateBase(dt,game);
}
}
class Bot extends Entity{
constructor(x,y,r,color,name){
super(x,y,r,color); this.isBot=true; this.name=name;
this.wanderAngle=rand(0,Math.PI*2); this.thinkTimer=0; this.goal=null;
}
update(dt,game){
this.thinkTimer-=dt;
if(this.thinkTimer<=0){ this.thinkTimer=rand(0.15,0.35); this.pickGoal(game); }
if(this.goal) this.moveToward(this.goal.x-this.x,this.goal.y-this.y,dt,game);
else{
this.wanderAngle+=rand(-1,1)*dt;
this.moveToward(Math.cos(this.wanderAngle),Math.sin(this.wanderAngle),dt,game);
}
this.updateBase(dt,game);
}
pickGoal(game){
const sense=1200, sense2=sense*sense;
let bestScore=-Infinity, best=null;
for(const v of game.viruses){
const d2=dist2(this.x,this.y,v.x,v.y);
if(d2>sense2) continue;
if(this.r>game.config.virusExplodeRadius*0.9){
const d=Math.sqrt(d2);
const score=2500/(d+40);
if(score>bestScore){ bestScore=score; best={x:this.x-(v.x-this.x),y:this.y-(v.y-this.y)}; }
}
}
for(const cell of game.allCells()){
if(cell===this||!cell.alive) continue;
const d2=dist2(this.x,this.y,cell.x,cell.y);
if(d2>sense2) continue;
const d=Math.sqrt(d2);
if(cell.r<this.r*0.85){
const score=4200/(d+80)+cell.mass*0.02;
if(score>bestScore){ bestScore=score; best=cell; }
}else if(cell.r>this.r*1.08){
const score=3000/(d+50);
if(score>bestScore){ bestScore=score; best={x:this.x-(cell.x-this.x),y:this.y-(cell.y-this.y)}; }
}
}
for(const f of game.foods){
if(!f.alive) continue;
const d2=dist2(this.x,this.y,f.x,f.y);
if(d2>sense2) continue;
const d=Math.sqrt(d2);
const score=1200/(d+30)+f.mass*0.5;
if(score>bestScore){ bestScore=score; best=f; }
}
this.goal=best;
}
}
class Food{
constructor(x,y,r,color){
this.x=x; this.y=y; this.r=r;
this.mass=Math.PI*r*r; this.color=color; this.alive=true; this.isFood=true;
}
draw(ctx){
ctx.beginPath();
ctx.fillStyle=this.color;
ctx.arc(this.x,this.y,this.r,0,Math.PI*2);
ctx.fill();
}
}
// 粘液弹:命中减速
class SlimeBall extends Food{
constructor(x,y,r,color,vx,vy,slowDuration){
super(x,y,r,color);
this.vx=vx; this.vy=vy;
this.isEjecta=true;
this.age=0; this.decayDuration=1.0;
this.slowDuration=slowDuration;
this.isSlime=true;
}
update(dt,game){
this.age+=dt;
const t=clamp(this.age/this.decayDuration,0,1);
const drag=lerp(0.06,0.42,t*t);
const factor=Math.exp(-drag*dt*60);
this.vx*=factor; this.vy*=factor;
this.x+=this.vx*dt; this.y+=this.vy*dt;
game.applySoftBounds(this,dt,true);
}
draw(ctx){
ctx.beginPath();
ctx.fillStyle=this.color;
ctx.arc(this.x,this.y,this.r,0,Math.PI*2);
ctx.fill();
ctx.strokeStyle="rgba(255,255,255,0.35)";
ctx.lineWidth=2;
ctx.stroke();
}
}
class Virus{
constructor(x,y,r=38){
this.x=x; this.y=y; this.r=r; this.mass=Math.PI*r*r;
this.alive=true; this.isVirus=true; this.color="hsl(115,75%,45%)";
this.spikeCount=18; this.feedMass=0; this.lastFeedDir={x:1,y:0};
}
addMass(dm){ this.mass=Math.max(1,this.mass+dm); this.r=Math.sqrt(this.mass/Math.PI); }
draw(ctx){
ctx.save(); ctx.translate(this.x,this.y); ctx.fillStyle=this.color; ctx.beginPath();
for(let i=0;i<this.spikeCount*2;i++){
const ang=i*Math.PI/this.spikeCount;
const rr=(i%2===0)?this.r*1.15:this.r*0.85;
ctx.lineTo(Math.cos(ang)*rr,Math.sin(ang)*rr);
}
ctx.closePath(); ctx.fill();
ctx.lineWidth=3; ctx.strokeStyle="rgba(0,0,0,0.35)"; ctx.stroke(); ctx.restore();
}
}
// ===== PowerUps =====
const ITEM_DEFS = {
speed: { name:"⚡加速", color:"hsl(45, 95%, 60%)" , short:"SPD"},
shield: { name:"🛡护盾", color:"hsl(210, 90%, 65%)", short:"SHD"},
magnet: { name:"🧲磁铁", color:"hsl(300, 85%, 70%)", short:"MAG"},
dash: { name:"💨冲刺", color:"hsl(160, 85%, 55%)", short:"DSH"},
double: { name:"✨双倍", color:"hsl(280, 90%, 70%)", short:"DBL"},
freeze: { name:"❄减速光环", color:"hsl(200, 85%, 75%)", short:"FRZ"},
splitReset:{ name:"🌀分裂充能", color:"hsl(30, 90%, 65%)", short:"RST"},
slimePlus:{ name:"🧪粘液强化", color:"hsl(110, 80%, 55%)", short:"SLM"},
};
const ITEM_TYPES = Object.keys(ITEM_DEFS);
class PowerUp{
constructor(x,y,type){
this.x=x; this.y=y; this.type=type;
this.r=22; this.alive=true; this.mass=0; this.isPowerUp=true;
this.color=ITEM_DEFS[type].color; this.name=ITEM_DEFS[type].name; this.short=ITEM_DEFS[type].short;
}
draw(ctx){
ctx.save(); ctx.translate(this.x,this.y);
ctx.fillStyle=this.color; ctx.beginPath(); ctx.arc(0,0,this.r,0,Math.PI*2); ctx.fill();
ctx.fillStyle="rgba(255,255,255,0.9)";
ctx.beginPath(); for(let i=0;i<6;i++){
const a=i*Math.PI/3; const rr=(i%2===0)?this.r*0.55:this.r*0.25;
ctx.lineTo(Math.cos(a)*rr,Math.sin(a)*rr);
} ctx.closePath(); ctx.fill();
ctx.fillStyle="rgba(0,0,0,0.8)";
ctx.font="10px sans-serif"; ctx.textAlign="center"; ctx.textBaseline="middle";
ctx.fillText(this.short,0,1);
ctx.fillStyle="rgba(255,255,255,0.95)";
ctx.font="12px sans-serif"; ctx.textAlign="center"; ctx.textBaseline="top";
ctx.fillText(this.name,0,this.r+4);
ctx.restore();
}
}
// ===== Game =====
class Game{
constructor(canvas,minimap){
this.canvas=canvas; this.ctx=canvas.getContext("2d");
this.minimap=minimap; this.mctx=minimap.getContext("2d");
this.w=canvas.width=innerWidth; this.h=canvas.height=innerHeight;
this.worldSize=5600;
this.playerCells=[]; this.bots=[]; this.foods=[]; this.viruses=[]; this.items=[];
this.running=false; this.gameOver=false; this.lastTime=performance.now();
this.inputX=this.w/2; this.inputY=this.h/2;
this.splitCooldown=0; this.ejectCooldown=0;
this.foodRespawnTimers=[];
// buffs
this.speedBoostTimer=0; this.speedBoostMul=1.6;
this.magnetTimer=0;
this.doubleTimer=0;
this.freezeTimer=0; this.freezeMul=0.6; this.freezeRadius=700;
this.playerShield=0;
this.forceMergeTimer=0;
this.slimePlusTimer=0; // 粘液强化
this.slowMul=0.55; // 被粘液减速倍率
this.itemSpawnTimer=0;
this.config={
foodInitial:2200, foodRespawnDelay:60, foodRadiusRange:[3.2,7.2],
botTarget:32, botSpawnPerSec:2, maxPlayerCells:8,
virusCount:12, virusRadius:38, virusExplodeRadius:55,
virusFeedGain:18, virusSplitThreshold:220, virusShootSpeed:760,
itemMax:7, itemSpawnEvery:12,
};
addEventListener("resize",()=>this.resize());
this.bindInput(); this.resize();
}
bindInput(){
const setTarget=(x,y)=>{this.inputX=x; this.inputY=y;};
this.canvas.addEventListener("mousemove",e=>setTarget(e.clientX,e.clientY));
this.canvas.addEventListener("mousedown",e=>setTarget(e.clientX,e.clientY));
this.canvas.addEventListener("contextmenu",e=>e.preventDefault());
this.canvas.addEventListener("mouseup",e=>{ if(e.button===2) this.eject(); });
addEventListener("keydown",(e)=>{
if(!this.running) return;
const k=e.key.toLowerCase();
if(k==="w"){ e.preventDefault(); this.split(); }
if(k==="s"){ e.preventDefault(); this.forceMerge(); }
if(k==="e"){ e.preventDefault(); this.eject(); }
});
}
resize(){ this.w=this.canvas.width=innerWidth; this.h=this.canvas.height=innerHeight; this.minimap.width=this.minimap.height=160; }
start(){ this.reset(); this.running=true; this.gameOver=false; }
reset(){
this.playerCells=[new PlayerCell(this.worldSize/2,this.worldSize/2,24,hsl(195,85,55))];
this.playerCells[0].name="YOU";
this.bots=[]; for(let i=0;i<this.config.botTarget;i++) this.spawnBot();
this.foods=[]; for(let i=0;i<this.config.foodInitial;i++) this.foods.push(this.spawnFood());
this.viruses=[]; for(let i=0;i<this.config.virusCount;i++) this.viruses.push(this.spawnVirus());
this.items=[]; this.foodRespawnTimers=[];
this.splitCooldown=this.ejectCooldown=0;
this.speedBoostTimer=this.magnetTimer=this.doubleTimer=this.freezeTimer=0;
this.playerShield=0; this.forceMergeTimer=0; this.slimePlusTimer=0;
this.itemSpawnTimer=this.config.itemSpawnEvery;
}
spawnBot(){
const r=rand(12,28);
const name=BOT_NAMES[Math.floor(rand(0,BOT_NAMES.length))];
this.bots.push(new Bot(rand(100,this.worldSize-100),rand(100,this.worldSize-100),r,hsl(Math.floor(rand(0,360)),75,60),name));
}
spawnFood(){
const r=rand(...this.config.foodRadiusRange);
return new Food(rand(0,this.worldSize),rand(0,this.worldSize),r,hsl(Math.floor(rand(0,360)),90,65));
}
spawnVirus(){
return new Virus(rand(200,this.worldSize-200),rand(200,this.worldSize-200),this.config.virusRadius);
}
spawnItem(){
const t=ITEM_TYPES[Math.floor(rand(0,ITEM_TYPES.length))];
return new PowerUp(rand(150,this.worldSize-150),rand(150,this.worldSize-150),t);
}
allCells(){ return [...this.playerCells,...this.bots]; }
playerMass(){ return this.playerCells.reduce((s,c)=>s+c.mass,0); }
playerScore(){ return Math.round(this.playerMass()); }
playerCenter(){
const n=this.playerCells.length; let x=0,y=0;
for(const c of this.playerCells){ x+=c.x; y+=c.y; }
return {x:x/n,y:y/n};
}
applySoftBounds(obj,dt,isFood=false){
const margin=120, k=isFood?0.5:1.1, restit=isFood?0.25:0.5;
if(obj.x<obj.r+margin){ const pen=(obj.r+margin)-obj.x; obj.vx+=pen*k*dt; if(obj.x<obj.r){ obj.x=obj.r; obj.vx=Math.abs(obj.vx)*restit; } }
if(obj.x>this.worldSize-obj.r-margin){ const pen=obj.x-(this.worldSize-obj.r-margin); obj.vx-=pen*k*dt; if(obj.x>this.worldSize-obj.r){ obj.x=this.worldSize-obj.r; obj.vx=-Math.abs(obj.vx)*restit; } }
if(obj.y<obj.r+margin){ const pen=(obj.r+margin)-obj.y; obj.vy+=pen*k*dt; if(obj.y<obj.r){ obj.y=obj.r; obj.vy=Math.abs(obj.vy)*restit; } }
if(obj.y>this.worldSize-obj.r-margin){ const pen=obj.y-(this.worldSize-obj.r-margin); obj.vy-=pen*k*dt; if(obj.y>this.worldSize-obj.r){ obj.y=this.worldSize-obj.r; obj.vy=-Math.abs(obj.vy)*restit; } }
obj.x=clamp(obj.x,obj.r,this.worldSize-obj.r);
obj.y=clamp(obj.y,obj.r,this.worldSize-obj.r);
}
split(){
if(this.splitCooldown>0||this.playerCells.length>=this.config.maxPlayerCells) return;
const dx=this.inputX-this.w/2, dy=this.inputY-this.h/2;
const len=Math.hypot(dx,dy)||1, ux=dx/len, uy=dy/len;
this.playerCells.sort((a,b)=>b.mass-a.mass);
const newCells=[];
for(const c of this.playerCells){
if(this.playerCells.length+newCells.length>=this.config.maxPlayerCells) break;
if(c.mass<120) continue;
const half=c.mass*0.5; c.setMass(half);
const nc=new PlayerCell(c.x+ux*(c.r+8),c.y+uy*(c.r+8),c.r,c.color);
nc.setMass(half); nc.mergeCooldown=7; c.mergeCooldown=7;
const boost=620/Math.sqrt(nc.r/16);
nc.applyBoost(ux*boost,uy*boost,0.95);
newCells.push(nc);
}
this.playerCells.push(...newCells);
this.splitCooldown=0.7;
}
// 粘液喷射
eject(){
if(this.ejectCooldown>0) return;
const c=this.playerCells.reduce((a,b)=>a.mass>b.mass?a:b);
if(!c||c.mass<90) return;
const dx=this.inputX-this.w/2, dy=this.inputY-this.h/2;
const len=Math.hypot(dx,dy)||1, ux=dx/len, uy=dy/len;
const plus = this.slimePlusTimer>0;
const ejectMass = plus ? 28 : 22;
c.addMass(-ejectMass);
const r=Math.sqrt(ejectMass/Math.PI) * (plus ? 1.25 : 1.0);
const speed= plus ? 820 : 700;
const slowDuration = plus ? 4.0 : 2.5;
const s=new SlimeBall(
c.x+ux*(c.r+r+2), c.y+uy*(c.r+r+2),
r, plus ? "hsl(110,80%,55%)" : "hsl(200,75%,60%)",
ux*speed, uy*speed, slowDuration
);
this.foods.push(s);
this.ejectCooldown=0.12;
}
forceMerge(){
if(this.playerCells.length<=1) return;
for(const c of this.playerCells) c.mergeCooldown=0;
this.forceMergeTimer=2.2;
}
explodeCell(cell,towardMouse=false){
const isPlayer=cell.isPlayer, totalMass=cell.mass;
const maxPieces=isPlayer?this.config.maxPlayerCells:10;
const pieces=clamp(Math.floor(cell.r/14),4,maxPieces);
const baseMass=totalMass/pieces;
const dx=this.inputX-this.w/2, dy=this.inputY-this.h/2;
const baseAng=Math.atan2(dy,dx);
cell.alive=false;
const newOnes=[];
for(let i=0;i<pieces;i++){
const ang=towardMouse?baseAng+(i-(pieces-1)/2)*(Math.PI/10):rand(0,Math.PI*2);
const m=baseMass, r=Math.sqrt(m/Math.PI);
const nx=cell.x+Math.cos(ang)*(cell.r+r+4);
const ny=cell.y+Math.sin(ang)*(cell.r+r+4);
const nc=isPlayer?new PlayerCell(nx,ny,r,cell.color):new Bot(nx,ny,r,cell.color,cell.name);
nc.setMass(m); nc.mergeCooldown=isPlayer?7:0;
const boost=820/Math.sqrt(r/16);
nc.applyBoost(Math.cos(ang)*boost,Math.sin(ang)*boost,1.05);
newOnes.push(nc);
}
if(isPlayer){
this.playerCells.push(...newOnes);
if(this.playerCells.length>this.config.maxPlayerCells){
this.playerCells.sort((a,b)=>b.mass-a.mass);
this.playerCells.length=this.config.maxPlayerCells;
}
}else this.bots.push(...newOnes);
}
update(dt){
if(!this.running||this.gameOver||this.playerCells.length===0) return;
if(this.splitCooldown>0) this.splitCooldown-=dt;
if(this.ejectCooldown>0) this.ejectCooldown-=dt;
// buff timers
if(this.speedBoostTimer>0) this.speedBoostTimer-=dt;
if(this.magnetTimer>0) this.magnetTimer-=dt;
if(this.doubleTimer>0) this.doubleTimer-=dt;
if(this.freezeTimer>0) this.freezeTimer-=dt;
if(this.forceMergeTimer>0) this.forceMergeTimer-=dt;
if(this.slimePlusTimer>0) this.slimePlusTimer-=dt;
for(const c of this.playerCells) c.update(dt,this);
// 玩家分身软碰撞
for(let i=0;i<this.playerCells.length;i++){
for(let j=i+1;j<this.playerCells.length;j++){
const a=this.playerCells[i], b=this.playerCells[j];
const d=dist(a.x,a.y,b.x,b.y), min=a.r+b.r;
if(d<min&&d>0.0001){
const push=(min-d)/min*0.6, ux=(a.x-b.x)/d, uy=(a.y-b.y)/d;
a.x+=ux*push*b.r; a.y+=uy*push*b.r;
b.x-=ux*push*a.r; b.y-=uy*push*a.r;
}
}
}
for(const b of this.bots) b.update(dt,this);
for(const f of this.foods) if(f.isEjecta) f.update(dt,this);
this.applyMergeAttraction(dt);
this.applyMagnet(dt);
for(const c of this.playerCells) this.handleFoodEat(c);
for(const b of this.bots) this.handleFoodEat(b);
this.handleSlimeHit(); // 新玩法核心
this.handleVirusInteractions();
this.handleBallEat();
this.handlePlayerMerge();
this.updateFoodRespawn(dt);
this.updateItems(dt);
if(this.bots.length<this.config.botTarget){
const addBot=Math.floor(this.config.botSpawnPerSec*dt)+1;
for(let i=0;i<addBot;i++) this.spawnBot();
}
this.foods=this.foods.filter(f=>f.alive);
this.bots=this.bots.filter(b=>b.alive);
this.playerCells=this.playerCells.filter(c=>c.alive);
this.viruses=this.viruses.filter(v=>v.alive);
this.items=this.items.filter(it=>it.alive);
while(this.viruses.length<this.config.virusCount) this.viruses.push(this.spawnVirus());
if(this.playerCells.length===0){ this.gameOver=true; this.running=false; }
}
applyMergeAttraction(dt){
if(this.playerCells.length<=1) return;
const center=this.playerCenter();
const forceMul=(this.forceMergeTimer>0)?3.2:1.0;
for(const c of this.playerCells){
if(c.mergeCooldown>0) continue;
const dx=center.x-c.x, dy=center.y-c.y;
const d=Math.hypot(dx,dy); if(d<1) continue;
const pull=(d*0.8*forceMul)/(c.r+30);
c.vx+=(dx/d*pull)*dt*60; c.vy+=(dy/d*pull)*dt*60;
if(d<c.r*1.6){ c.vx*=Math.pow(0.55,dt); c.vy*=Math.pow(0.55,dt); }
}
}
applyMagnet(dt){
if(this.magnetTimer<=0) return;
const center=this.playerCenter(), radius=520;
for(const f of this.foods){
if(!f.alive||f.isEjecta) continue;
const d=dist(center.x,center.y,f.x,f.y);
if(d<radius&&d>1){
const ux=(center.x-f.x)/d, uy=(center.y-f.y)/d;
f.x+=ux*dt*140; f.y+=uy*dt*140;
}
}
}
handleFoodEat(ball){
for(const f of this.foods){
if(!f.alive) continue;
if(dist2(ball.x,ball.y,f.x,f.y)<ball.r*ball.r){
f.alive=false;
const gain=(this.doubleTimer>0)?f.mass*2:f.mass;
ball.addMass(gain);
if(!f.isEjecta) this.foodRespawnTimers.push(this.config.foodRespawnDelay);
}
}
}
// 粘液弹命中:减速 + 可被吃
handleSlimeHit(){
const cells=this.allCells().filter(c=>c.alive);
for(const s of this.foods){
if(!s.alive||!s.isSlime) continue;
for(const c of cells){
const d=dist(s.x,s.y,c.x,c.y);
if(d<s.r+c.r){
// 大球吃掉粘液弹
if(c.r>s.r*1.15){
s.alive=false;
const gain=(this.doubleTimer>0)?s.mass*2:s.mass;
c.addMass(gain);
break;
}else{
// 命中减速(叠加取最大)
c.slowTimer=Math.max(c.slowTimer,s.slowDuration);
// 轻微冲量让命中有反馈
const ux=(c.x-s.x)/(d||1), uy=(c.y-s.y)/(d||1);
c.vx+=ux*40; c.vy+=uy*40;
s.vx*=-0.35; s.vy*=-0.35;
}
}
}
}
}
handleVirusInteractions(){
for(const v of this.viruses){
for(const f of this.foods){
if(!f.alive||!f.isEjecta) continue;
const d=dist(v.x,v.y,f.x,f.y);
if(d<v.r+f.r){
f.alive=false;
v.addMass(this.config.virusFeedGain);
v.feedMass+=this.config.virusFeedGain;
const sp=Math.hypot(f.vx,f.vy)||1;
v.lastFeedDir={x:f.vx/sp,y:f.vy/sp};
if(v.feedMass>=this.config.virusSplitThreshold){
v.feedMass=0;
const dir=v.lastFeedDir;
const nv=new Virus(v.x+dir.x*(v.r*1.6),v.y+dir.y*(v.r*1.6),this.config.virusRadius);
nv.vx=dir.x*this.config.virusShootSpeed;
nv.vy=dir.y*this.config.virusShootSpeed;
nv.age=0;
v.addMass(-this.config.virusSplitThreshold*0.45);
this.viruses.push(nv);
}
}
}
}
for(const v of this.viruses){
if(v.vx||v.vy){
v.age=(v.age||0)+0.016;
const t=clamp((v.age||0)/1.1,0,1);
const drag=lerp(0.03,0.25,t*t);
const factor=Math.exp(-drag*0.016*60);
v.vx*=factor; v.vy*=factor;
v.x+=v.vx*0.016; v.y+=v.vy*0.016;
this.applySoftBounds(v,0.016,true);
if(Math.hypot(v.vx,v.vy)<8){ v.vx=0; v.vy=0; }
}
}
const all=this.allCells().filter(e=>e.alive);
for(const v of this.viruses){
for(const c of all){
const d=dist(v.x,v.y,c.x,c.y);
if(d<v.r+c.r*0.9 && c.r>=this.config.virusExplodeRadius){
this.explodeCell(c,c.isPlayer);
v.alive=false; break;
}
}
}
}
handleBallEat(){
const all=this.allCells().filter(e=>e.alive);
for(let i=0;i<all.length;i++){
const a=all[i];
for(let j=i+1;j<all.length;j++){
const b=all[j];
let big=a, small=b;
if(b.r>a.r){ big=b; small=a; }
const d=dist(big.x,big.y,small.x,small.y);
const normalEat=(d+small.r<=big.r*0.98 && big.r>small.r*1.05);
const boostEat=(big.boostDuration>0 && big.r>small.r*1.05 && d<big.r);
if(normalEat||boostEat){
if(small.isPlayer && this.playerShield>0){
this.playerShield=0;
const ux=(small.x-big.x)/(d||1), uy=(small.y-big.y)/(d||1);
small.applyBoost(ux*420,uy*420,0.6);
continue;
}
small.alive=false; big.addMass(small.mass);
}
}
}
}
handlePlayerMerge(){
for(let i=0;i<this.playerCells.length;i++){
for(let j=i+1;j<this.playerCells.length;j++){
const a=this.playerCells[i], b=this.playerCells[j];
if(a.mergeCooldown>0||b.mergeCooldown>0) continue;
const d=dist(a.x,a.y,b.x,b.y);
if(d<Math.max(a.r,b.r)*0.5){
let big=a, small=b; if(b.r>a.r){ big=b; small=a; }
small.alive=false; big.addMass(small.mass);
}
}
}
}
updateFoodRespawn(dt){
for(let i=0;i<this.foodRespawnTimers.length;i++) this.foodRespawnTimers[i]-=dt;
this.foodRespawnTimers=this.foodRespawnTimers.filter(t=>{
if(t<=0){ this.foods.push(this.spawnFood()); return false; }
return true;
});
}
updateItems(dt){
this.itemSpawnTimer-=dt;
if(this.itemSpawnTimer<=0){
this.itemSpawnTimer=this.config.itemSpawnEvery*rand(0.7,1.3);
if(this.items.length<this.config.itemMax) this.items.push(this.spawnItem());
}
const center=this.playerCenter();
const pr=Math.sqrt(this.playerMass()/Math.PI);
for(const it of this.items){
if(!it.alive) continue;
if(dist2(center.x,center.y,it.x,it.y)<(pr+it.r)*(pr+it.r)){
it.alive=false;
this.applyItem(it.type);
}
}
}
applyItem(type){
const def=ITEM_DEFS[type];
showToast(`获得道具:${def.name}`);
if(type==="speed") this.speedBoostTimer=8;
else if(type==="shield") this.playerShield=1;
else if(type==="magnet") this.magnetTimer=10;
else if(type==="double") this.doubleTimer=12;
else if(type==="freeze") this.freezeTimer=10;
else if(type==="dash"){
const c=this.playerCells.reduce((a,b)=>a.mass>b.mass?a:b);
const dx=this.inputX-this.w/2, dy=this.inputY-this.h/2;
const len=Math.hypot(dx,dy)||1, ux=dx/len, uy=dy/len;
const boost=1100/Math.sqrt(c.r/16);
c.applyBoost(ux*boost,uy*boost,1.0);
}
else if(type==="splitReset"){
this.splitCooldown=0;
for(const c of this.playerCells) c.mergeCooldown=0;
this.forceMergeTimer=1.6;
}
else if(type==="slimePlus"){
this.slimePlusTimer=10;
}
}
getCameraScale(){
const totalR=Math.sqrt(this.playerMass()/Math.PI);
const s=1/clamp(totalR/60,1,4.2);
return clamp(s,0.22,1);
}
render(){
const ctx=this.ctx;
ctx.clearRect(0,0,this.w,this.h);
if(this.playerCells.length===0){ ctx.fillStyle="#0b0f14"; ctx.fillRect(0,0,this.w,this.h); return; }
const center=this.playerCenter(), scale=this.getCameraScale();
ctx.save();
ctx.translate(this.w/2,this.h/2);
ctx.scale(scale,scale);
ctx.translate(-center.x,-center.y);
const viewLeft=center.x-this.w/(2*scale);
const viewTop=center.y-this.h/(2*scale);
const viewRight=viewLeft+this.w/scale;
const viewBottom=viewTop+this.h/scale;
this.drawBackground(ctx,viewLeft,viewTop,viewRight,viewBottom);
for(const v of this.viruses) v.draw(ctx);
for(const it of this.items) it.draw(ctx);
for(const f of this.foods) f.draw(ctx);
for(const b of this.bots) b.draw(ctx);
for(const c of this.playerCells) c.draw(ctx);
ctx.restore();
this.drawMinimap();
}
drawBackground(ctx,viewLeft,viewTop,viewRight,viewBottom){
ctx.fillStyle="#0b0f14"; ctx.fillRect(0,0,this.worldSize,this.worldSize);
const grid=90;
ctx.strokeStyle="rgba(255,255,255,0.04)"; ctx.lineWidth=1;
const startX=Math.floor(viewLeft/grid)*grid;
const startY=Math.floor(viewTop/grid)*grid;
ctx.beginPath();
for(let x=startX;x<=viewRight;x+=grid){ ctx.moveTo(x,viewTop); ctx.lineTo(x,viewBottom); }
for(let y=startY;y<=viewBottom;y+=grid){ ctx.moveTo(viewLeft,y); ctx.lineTo(viewRight,y); }
ctx.stroke();
ctx.strokeStyle="rgba(255,255,255,0.10)"; ctx.lineWidth=6;
ctx.strokeRect(0,0,this.worldSize,this.worldSize);
}
drawMinimap(){
if(this.playerCells.length===0) return;
const mctx=this.mctx, size=this.minimap.width;
mctx.clearRect(0,0,size,size);
mctx.fillStyle="rgba(0,0,0,0.4)"; mctx.fillRect(0,0,size,size);
const scale=size/this.worldSize;
mctx.fillStyle="rgba(255,255,255,0.2)";
for(let i=0;i<this.foods.length;i+=30){
const f=this.foods[i];
mctx.fillRect(f.x*scale,f.y*scale,2,2);
}
for(const v of this.viruses){
mctx.beginPath(); mctx.fillStyle="rgba(120,255,120,0.9)";
mctx.arc(v.x*scale,v.y*scale,Math.max(2,v.r*scale),0,Math.PI*2); mctx.fill();
}
for(const it of this.items){
mctx.beginPath(); mctx.fillStyle="rgba(255,255,255,0.9)";
mctx.arc(it.x*scale,it.y*scale,3,0,Math.PI*2); mctx.fill();
}
for(const b of this.bots){
mctx.beginPath(); mctx.fillStyle="rgba(255,80,80,0.9)";
mctx.arc(b.x*scale,b.y*scale,Math.max(2,b.r*scale),0,Math.PI*2); mctx.fill();
}
const center=this.playerCenter();
const pr=Math.sqrt(this.playerMass()/Math.PI);
mctx.beginPath(); mctx.fillStyle="rgba(80,200,255,1)";
mctx.arc(center.x*scale,center.y*scale,Math.max(3,pr*scale),0,Math.PI*2); mctx.fill();
mctx.strokeStyle="rgba(255,255,255,0.4)"; mctx.lineWidth=2; mctx.strokeRect(0,0,size,size);
}
updateLeaderboard(){
const list=[{name:"YOU",mass:this.playerMass(),isMe:true}];
for(const b of this.bots) if(b.alive) list.push({name:b.name,mass:b.mass,isMe:false});
list.sort((a,b)=>b.mass-a.mass);
const top5=list.slice(0,5);
const lbEl=document.getElementById("lbList");
lbEl.innerHTML="";
top5.forEach((p,idx)=>{
const li=document.createElement("li"); li.className=p.isMe?"me":"";
li.textContent=`${idx+1}. ${p.name} (${Math.round(p.mass)})`;
lbEl.appendChild(li);
});
}
updateUI(){
document.getElementById("mass").textContent=Math.round(this.playerMass());
document.getElementById("cells").textContent=this.playerCells.length;
document.getElementById("score").textContent=this.playerScore();
document.getElementById("bots").textContent=this.bots.length;
document.getElementById("viruses").textContent=this.viruses.length;
document.getElementById("foods").textContent=this.foods.filter(f=>f.alive&&!f.isEjecta).length;
document.getElementById("items").textContent=this.items.length;
const buffs=[];
if(this.speedBoostTimer>0) buffs.push(`${ITEM_DEFS.speed.name} ${this.speedBoostTimer.toFixed(1)}s`);
if(this.magnetTimer>0) buffs.push(`${ITEM_DEFS.magnet.name} ${this.magnetTimer.toFixed(1)}s`);
if(this.doubleTimer>0) buffs.push(`${ITEM_DEFS.double.name} ${this.doubleTimer.toFixed(1)}s`);
if(this.freezeTimer>0) buffs.push(`${ITEM_DEFS.freeze.name} ${this.freezeTimer.toFixed(1)}s`);
if(this.slimePlusTimer>0) buffs.push(`${ITEM_DEFS.slimePlus.name} ${this.slimePlusTimer.toFixed(1)}s`);
if(this.playerShield>0) buffs.push(`${ITEM_DEFS.shield.name} x${this.playerShield}`);
document.getElementById("buffs").textContent=buffs.join(" ");
this.updateLeaderboard();
if(this.gameOver){
hint.classList.remove("hidden");
hint.innerHTML=`你被吃掉了 😵<br/><small>得分:${this.playerScore()} 点击"重新开始"</small>`;
}
}
loop=(t)=>{
const dt=Math.min(0.033,(t-this.lastTime)/1000);
this.lastTime=t;
this.update(dt);
this.render();
this.updateUI();
requestAnimationFrame(this.loop);
}
}
// ===== Toast =====
const toastEl=document.getElementById("toast");
let toastTimer=0;
function showToast(text){
toastEl.textContent=text;
toastEl.classList.add("show");
toastTimer=1.8;
}
function updateToast(dt){
if(toastTimer>0){
toastTimer-=dt;
if(toastTimer<=0) toastEl.classList.remove("show");
}
}
// ===== init =====
const canvas=document.getElementById("c");
const minimap=document.getElementById("minimap");
const game=new Game(canvas,minimap);
const hint=document.getElementById("hint");
document.getElementById("startBtn").onclick=()=>{ hint.classList.add("hidden"); game.start(); };
document.getElementById("restartBtn").onclick=()=>{ hint.classList.add("hidden"); game.start(); };
let lastT=performance.now();
function outerLoop(t){
const dt=Math.min(0.033,(t-lastT)/1000);
lastT=t;
updateToast(dt);
requestAnimationFrame(outerLoop);
}
requestAnimationFrame(outerLoop);
requestAnimationFrame(game.loop);
})();
</script>
</body>
</html>
在线体验
https://qiu.zhangsan.shop
GPt5.1/Gemini3pro/Grok4.1/香蕉2模型等,使用网站:
https://share.zhangsan.cool
https://share-hk.zhangsan.cool
https://share.searchknowledge.cloud
https://hello.aiforme.cloud
AI群:967915168