<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>🌸 花瓣防卫 · 九宫格区域</title>
<style>
*{box-sizing:border-box;user-select:none}
body{margin:0;min-height:100vh;background:linear-gradient(135deg,#0a1a0a,#1a2e1a);display:flex;justify-content:center;align-items:center;font-family:'Segoe UI',Arial,sans-serif;padding:16px}
#game-wrapper{background:#1a2e1a;border-radius:48px;padding:20px;box-shadow:0 30px 60px rgba(0,0,0,.9),inset 0 0 0 2px #3a5a3a;border:3px solid #b8a87c;position:relative}
canvas{display:block;width:1200px;height:780px;max-width:100%;max-height:85vh;background:radial-gradient(circle at 30% 30%,#2a4525,#0f1f0a);border-radius:32px;border:4px solid #6e9a5e;cursor:crosshair;touch-action:none;box-shadow:inset 0 0 40px rgba(0,0,0,.5)}
.game-title{text-align:center;margin-bottom:8px}
.game-title h1{margin:0;color:#f3e9c0;font-size:28px;text-shadow:3px 3px 0 #2a4a2a;letter-spacing:4px}
.control-hint{color:#8a9a7a;text-align:center;margin-top:8px;font-size:14px}
.lobby{position:absolute;top:10px;right:10px;display:flex;gap:5px;background:#000a;padding:5px;border-radius:8px}
button{padding:8px 12px;font-size:14px;cursor:pointer}
</style>
</head>
<body>
<div style="display:flex;flex-direction:column">
<div class="game-title"><h1>🌸 花瓣防卫 · 九宫格区域 🌸</h1></div>
<div id="game-wrapper">
<canvas id="gameCanvas"></canvas>
<div class="lobby">
<button id="createBtn">创建房间</button>
<button id="joinBtn">加入房间</button>
<button id="setRemoteBtn" disabled>设置远程Answer</button>
<span id="statusText" style="color:#ff0"></span>
</div>
</div>
<div class="control-hint">🖱️ 鼠标控制角色 | ESC暂停 | 联机:主机创建→复制Offer→客机粘贴并回传Answer→主机粘贴Answer</div>
</div>
<script>
(function() {
"use strict";
const canvas=document.getElementById('gameCanvas');
const ctx=canvas.getContext('2d');
const CW=1200,CH=780; canvas.width=CW;canvas.height=CH;
const ROOM_W=2000,ROOM_H=1600;
const WORLD_W=ROOM_W*3,WORLD_H=ROOM_H*3;
const WALL_THICK=40,GAP_HALF=50;
const ZONE_GRADE = [[0,1,2],[5,4,3],[6,7,8]];
function buildWalls(){
let r=[];
const VWALL_X=[ROOM_W,ROOM_W*2],HWALL_Y=[ROOM_H,ROOM_H*2];
const noGap=[[0,0],[0,1],[1,1],[1,2]];
for(let xi=0;xi<VWALL_X.length;xi++){
let x=VWALL_X[xi];
for(let seg=0;seg<3;seg++){
let ys=seg*ROOM_H,ye=ys+ROOM_H,gy=ys+ROOM_H/2;
let n=noGap.some(n=>n[0]===xi&&n[1]===seg);
if(n) r.push({x:x-WALL_THICK/2,y:ys,w:WALL_THICK,h:ROOM_H});
else{
r.push({x:x-WALL_THICK/2,y:ys,w:WALL_THICK,h:gy-GAP_HALF-ys});
r.push({x:x-WALL_THICK/2,y:gy+GAP_HALF,w:WALL_THICK,h:ye-(gy+GAP_HALF)});
}
}
}
for(let yi=0;yi<HWALL_Y.length;yi++){
let y=HWALL_Y[yi];
for(let seg=0;seg<3;seg++){
let xs=seg*ROOM_W,xe=xs+ROOM_W,gx=xs+ROOM_W/2;
r.push({x:xs,y:y-WALL_THICK/2,w:gx-GAP_HALF-xs,h:WALL_THICK});
r.push({x:gx+GAP_HALF,y:y-WALL_THICK/2,w:xe-(gx+GAP_HALF),h:WALL_THICK});
}
}
r.push({x:0,y:0,w:WORLD_W,h:WALL_THICK},{x:0,y:WORLD_H-WALL_THICK,w:WORLD_W,h:WALL_THICK},{x:0,y:0,w:WALL_THICK,h:WORLD_H},{x:WORLD_W-WALL_THICK,y:0,w:WALL_THICK,h:WORLD_H});
return r;
}
const walls=buildWalls();
const State={MAIN:0,WORLD_SELECT:1,AREA_SELECT:2,EXPLORING:3,BACKPACK:4,TALENT:5,DEAD:6,PAUSED:7};
let currentState=State.MAIN,previousState=State.MAIN;
let playerLevel=1,playerExp=0,talentPoints=0;
let talents=[0,0,0,0,0];
const TALENT_MAX=[8,6,8,5,6];
const TALENT_NAMES=['❤️ 生命','⚡ 移速','🗡️ 伤害','🌀 转速','📏 范围'];
const TALENT_BASE=[100,3.2,1.0,0.024,90];
const TALENT_SCALE=[28,0.45,0.18,0.0045,14];
let inventory=[],slots=new Array(7).fill(null);
const GRADE_NAMES=['Common','Unusual','Rare','Epic','Legendary','Mythic','Ultra','Super','Unique'];
const GRADE_SHORT=['Com','Uns','Rar','Epi','Lgd','Mth','Ult','Sup','Unq'];
const GRADE_COLORS=['#2e8b57','#ffff00','#4169e1','#9932cc','#ff0000','#00ffff','#ff69b4','#39ff14','#000000'];
const MAX_GRADE=8;
const COMBINE_CHANCES=[0.64,0.32,0.16,0.08,0.04,0.02,0.01,0.005,0.0025];
const gradeMult=g=>Math.pow(3,g);
const WORLD_NAMES={garden:'🌿 花园',desert:'🏜️ 沙漠',ocean:'🌊 海洋',tw:'🖼️ 图文'};
const PETAL_STATS={
basic:{dmg:8,hp:20,cd:40,icon:'⚪',name:'基础',effect:'基础花瓣'},
stinger:{dmg:24,hp:12,cd:75,icon:'🦂',name:'毒刺',effect:'高伤害,冷却长'},
missle:{dmg:14,hp:16,cd:35,icon:'🚀',name:'导弹',effect:'较快攻速'},
web:{dmg:3,hp:30,cd:55,icon:'🕸️',name:'蛛网',effect:'生成减速区域'},
beetle_egg:{dmg:16,hp:22,cd:65,icon:'🥚',name:'甲虫卵',effect:'25%几率召唤甲虫'},
totem:{dmg:0,hp:50,cd:0,icon:'🗿',name:'图腾',effect:'击杀怪物时回血'},
lightning:{dmg:12,hp:18,cd:45,icon:'⚡',name:'闪电',effect:'连锁2个额外目标'},
turtle:{dmg:7,hp:60,cd:55,icon:'🐢',name:'龟壳',effect:'高生命值'},
starfish:{dmg:5,hp:25,cd:35,icon:'⭐',name:'海星',effect:'血量≤80%时回复'},
mystic_powder:{dmg:8,hp:20,cd:40,icon:'✨',name:'神秘粉末',effect:'提升移速(乘算叠加)'},
vector:{dmg:24,hp:20,cd:40,icon:'➡️',name:'矢量',effect:'击中后3秒内敌人反向移动'},
paper:{dmg:40,hp:1,cd:40,icon:'📄',name:'打印纸',effect:'增加同等级怪物掉落判定'}
};
const POWDER_SPEED_MULT=[1.1,1.2,1.3,1.4,1.5,1.6,1.7,1.8,2.0];
const MONSTER_STATS={
bee:{hp:50,dmg:12,spd:2.2,exp:12,color:'#e0b040',icon:'🐝',name:'蜜蜂'},
hornet:{hp:40,dmg:10,spd:1.7,exp:10,color:'#c08030',icon:'🐝',name:'黄蜂'},
spider:{hp:29,dmg:28,spd:3.4,exp:14,color:'#604080',icon:'🕷️',name:'毒蛛'},
beetle:{hp:58,dmg:14,spd:1.7,exp:13,color:'#8b5e3c',icon:'🪲',name:'甲虫'},
bandage_beetle:{hp:50,dmg:11,spd:1.6,exp:15,color:'#a0522d',icon:'🩹',name:'绷带甲虫'},
tomb:{hp:150,dmg:0,spd:0,exp:25,color:'#6a5a4a',icon:'🪦',name:'古墓'},
desert_centipede:{hp:19,dmg:8,spd:8.5,exp:20,color:'#d2b48c',icon:'🪱',name:'沙漠蜈蚣'},
jellyfish:{hp:44,dmg:7,spd:1.1,exp:11,color:'#ff69b4',icon:'🎐',name:'水母'},
starfish:{hp:40,dmg:10,spd:1.6,exp:10,color:'#ffa500',icon:'⭐',name:'海星'},
turtle:{hp:120,dmg:6,spd:0.9,exp:18,color:'#228b22',icon:'🐢',name:'海龟'},
tai_yang:{hp:174,dmg:42,spd:0.34,exp:30,color:'#c0c0c0',icon:'台',name:'台阳'},
yi_you:{hp:580,dmg:42,spd:0,exp:50,color:'#c0a000',icon:'益',name:'益友'}
};
const DROP_TABLE={
bee:'stinger',hornet:'missle',spider:'web',beetle:'beetle_egg',
bandage_beetle:'beetle_egg',tomb:'totem',desert_centipede:'mystic_powder',
jellyfish:'lightning',starfish:'starfish',turtle:'turtle',
tai_yang:'vector',yi_you:'paper'
};
const DROP_GRADE_TABLE=[
[[0,1.0]],[[0,0.2],[1,1.0]],[[0,0.25],[1,0.75],[2,1.0]],
[[1,0.2],[2,0.84],[3,1.0]],[[2,0.4],[3,0.92],[4,1.0]],
[[3,0.5],[4,0.96],[5,1.0]],[[4,0.6],[5,0.98],[6,1.0]],
[[5,0.7],[6,0.99],[7,1.0]],[[5,0.7],[6,0.99],[7,1.0]]
];
let worldId='garden',monsters=[],drops=[],projectiles=[],webs=[],summons=[];
const MAX_SUMMONS=8,MAX_PROJECTILES=30,MAX_WEBS=15;
let player={
x:WORLD_W/2,y:WORLD_H/2,hp:100,maxHp:100,speed:3.2,dmgMul:1.0,
orbitSpeed:0.024,range:90,invincible:0,healStacks:[],radius:24,
pendingPoison:0,poisonTickRate:0
};
let petals=[],frame=0,mouse={x:CW/2,y:CH/2},targetPos={x:WORLD_W/2,y:WORLD_H/2};
let backpackOpen=false,backpackScrollY=0;
let selectedInvIndex=-1,hoverInvIndex=-1,hoverSlot=-1,hoverMonsterIdx=-1,hoverDropIdx=-1;
let messages=[],killCount=0;
let selectedWorld=null;
function clamp(v,mn,mx){return Math.max(mn,Math.min(mx,v));}
function random(a,b){return a+Math.random()*(b-a);}
function dist(x1,y1,x2,y2){return Math.hypot(x2-x1,y2-y1);}
function addMessage(t,c='#fff'){messages.push({text:t,color:c,timer:120});if(messages.length>5)messages.shift();}
function resolveWallCollision(o,r=20){
for(const w of walls){
let cx=clamp(o.x,w.x,w.x+w.w),cy=clamp(o.y,w.y,w.y+w.h);
let d=dist(o.x,o.y,cx,cy);
if(d<r){
let ov=r-d,dx=o.x-cx,dy=o.y-cy,l=Math.hypot(dx,dy);
if(l<0.001){o.x+=r;continue;}
o.x+=(dx/l)*ov;o.y+=(dy/l)*ov;
}
}
}
function applyPoison(target,amount,tickRatePerSec){
target.pendingPoison+=amount;
if(tickRatePerSec>target.poisonTickRate)target.poisonTickRate=tickRatePerSec;
}
function processPoison(target){
if(target.pendingPoison<=0){target.poisonTickRate=0;return;}
let dmgThisFrame=target.poisonTickRate/60;
let actual=Math.min(dmgThisFrame,target.pendingPoison);
target.hp-=actual;
target.pendingPoison-=actual;
if(target.pendingPoison<=0)target.poisonTickRate=0;
}
function initGame(){
inventory=[]; addToInventory('basic',0,7);
for(let i=0;i<7;i++) if(inventory[i]) slots[i]=i;
updatePlayerStats();
addMessage('🌸 欢迎!选择世界与区域开始冒险','#ffd700');
}
function addToInventory(type,grade,count=1){
let e=inventory.find(i=>i.type===type&&i.grade===grade);
if(e) e.count+=count; else inventory.push({type,grade,count});
}
function getPetalStats(type,grade){
let b=PETAL_STATS[type]||PETAL_STATS.basic;
let gm=gradeMult(grade);
return {dmg:b.dmg*gm,hp:type==='paper'?1:b.hp*gm,cd:b.cd,icon:b.icon,name:b.name};
}
function applySpeed(){
let m=1;
for(let s=0;s<7;s++){
let idx=slots[s];
if(idx!==null&&inventory[idx]?.type==='mystic_powder')m*=POWDER_SPEED_MULT[inventory[idx].grade]||1;
}
player.speed=(TALENT_BASE[1]+talents[1]*TALENT_SCALE[1])*m;
}
function rebuildPetals(){
petals=[];
let valid=slots.filter(i=>i!==null&&inventory[i]?.count>0);
if(!valid.length){applySpeed();return;}
let step=(Math.PI*2)/valid.length;
valid.forEach((invIdx,i)=>{
let item=inventory[invIdx],stats=getPetalStats(item.type,item.grade);
let hm=1+talents[0]*0.1;
petals.push({
type:item.type,grade:item.grade,
hp:item.type==='paper'?1:stats.hp*hm,maxHp:item.type==='paper'?1:stats.hp*hm,
dmg:stats.dmg*player.dmgMul,cd:0,maxCd:stats.cd,
angle:i*step,range:player.range,
icon:stats.icon,name:stats.name,
speedOffset:0.8+item.grade*0.1+talents[3]*0.02
});
});
applySpeed();
}
function updatePlayerStats(){
player.maxHp=Math.floor(TALENT_BASE[0]+talents[0]*TALENT_SCALE[0]);
player.dmgMul=TALENT_BASE[2]+talents[2]*TALENT_SCALE[2];
player.orbitSpeed=TALENT_BASE[3]+talents[3]*TALENT_SCALE[3];
player.range=TALENT_BASE[4]+talents[4]*TALENT_SCALE[4];
if(player.hp>player.maxHp)player.hp=player.maxHp;
rebuildPetals();
}
function addExperience(amt){
playerExp+=amt;
let need=playerLevel*25;
let loops=0;
while(playerExp>=need&&playerLevel<40&&loops<100){
playerLevel++;talentPoints++;playerExp-=need;
need=playerLevel*25;loops++;
addMessage(`✨ 升级! 等级 ${playerLevel} 天赋点 +1`,'#ffd700');
}
updatePlayerStats();
}
function getMonsterGradeForZone(zoneGrade){
let r=Math.random();
switch(zoneGrade){
case 0:return r<0.8?0:1;
case 1:return r<0.7?1:(r<0.9?0:2);
case 2:return r<0.6?2:(r<0.85?1:(r<0.95?3:0));
case 3:return r<0.6?3:(r<0.85?2:(r<0.95?4:1));
case 4:return r<0.6?4:(r<0.85?3:(r<0.95?5:2));
case 5:return r<0.6?5:(r<0.85?4:(r<0.95?6:3));
case 6:return r<0.6?6:(r<0.85?5:(r<0.95?7:4));
case 7:return r<0.7?7:(r<0.95?6:8);
case 8:return r<0.8?8:7;
}
return 0;
}
function spawnMonster(){
if(monsters.length>=15+playerLevel*2)return;
let types={
garden:['bee','hornet','spider'],
desert:['beetle','bandage_beetle','tomb','desert_centipede'],
ocean:['jellyfish','starfish','turtle'],
tw:['tai_yang','yi_you']
}[worldId];
let type=types[Math.floor(Math.random()*types.length)];
let base=MONSTER_STATS[type];
let {col,row}=getZoneIndex(player.x,player.y);
let zoneGrade=ZONE_GRADE[row][col];
let grade=getMonsterGradeForZone(zoneGrade);
let gm=gradeMult(grade);
let minDist=player.range*3;
let x,y,attempts=0;
do{
x=random(60,WORLD_W-60);y=random(60,WORLD_H-60);
attempts++;
}while(dist(x,y,player.x,player.y)<minDist&&attempts<200);
let radius=(type==='tai_yang'?3:type==='yi_you'?2:1)*22*Math.pow(1.1,grade);
if(type==='desert_centipede'){
const SEGMENTS=5;let prev=null;
for(let i=0;i<SEGMENTS;i++){
let seg={
type,grade,x:x-i*25,y,
hp:base.hp*gm*4,maxHp:base.hp*gm*4,
damage:base.dmg*gm,speed:base.spd,expValue:base.exp*gm,
color:base.color,icon:base.icon,name:base.name,
radius,invincible:0,knockback:{x:0,y:0},reverseTimer:0,
chain:{prev,next:null,isHead:i===0}
};
if(prev)prev.chain.next=seg;
monsters.push(seg);
prev=seg;
}
}else{
monsters.push({
type,grade,x,y,
hp:base.hp*gm*4,maxHp:base.hp*gm*4,
damage:base.dmg*gm,speed:base.spd,expValue:base.exp*gm,
color:base.color,icon:base.icon,name:base.name,
radius,invincible:0,knockback:{x:0,y:0},reverseTimer:0,chain:null
});
}
}
function getZoneIndex(x,y){
let col=Math.floor(x/ROOM_W),row=Math.floor(y/ROOM_H);
return {col:clamp(col,0,2),row:clamp(row,0,2)};
}
function updateCentipedeChains(){
for(const m of monsters){
if(!m.chain||m.chain.isHead)continue;
const prev=m.chain.prev;
if(!prev)continue;
const targetDist=(prev.radius+m.radius)*0.9;
let dx=m.x-prev.x,dy=m.y-prev.y,d=Math.hypot(dx,dy);
if(d>targetDist){
let moveX=(prev.x-m.x)/d*Math.min(m.speed,d-targetDist);
let moveY=(prev.y-m.y)/d*Math.min(m.speed,d-targetDist);
m.x+=moveX;m.y+=moveY;
}
m.x=clamp(m.x,20,WORLD_W-20);m.y=clamp(m.y,20,WORLD_H-20);
resolveWallCollision(m,m.radius);
}
}
function resolveEntityCollisions(){
let entities=[];
for(let m of monsters)entities.push({obj:m,radius:m.radius||22,isMonster:true});
for(let s of summons)entities.push({obj:s,radius:s.radius||14,isMonster:false});
for(let i=0;i<entities.length;i++){
for(let j=i+1;j<entities.length;j++){
let a=entities[i].obj,b=entities[j].obj;
if(a.chain&&b.chain&&(a.chain.prev===b||a.chain.next===b||b.chain.prev===a||b.chain.next===a))continue;
let r1=entities[i].radius,r2=entities[j].radius;
let d=dist(a.x,a.y,b.x,b.y),minDist=r1+r2;
if(d<minDist){
let ov=minDist-d,dx=b.x-a.x,dy=b.y-a.y;
if(d<0.001){dx=1;dy=0;}else{dx/=d;dy/=d;}
let aMov=!(a.type==='tomb'||a.type==='yi_you'),bMov=!(b.type==='tomb'||b.type==='yi_you');
if(!aMov&&!bMov)continue;
if(!aMov){b.x+=dx*ov;b.y+=dy*ov;}
else if(!bMov){a.x-=dx*ov;a.y-=dy*ov;}
else{
if(r1>r2){b.x+=dx*ov;b.y+=dy*ov;}
else if(r2>r1){a.x-=dx*ov;a.y-=dy*ov;}
else{let h=ov/2;a.x-=dx*h;a.y-=dy*h;b.x+=dx*h;b.y+=dy*h;}
}
}
}
}
for(let e of entities){
e.obj.x=clamp(e.obj.x,20,WORLD_W-20);e.obj.y=clamp(e.obj.y,20,WORLD_H-20);
resolveWallCollision(e.obj,e.radius);
}
}
function updateMonsters(){
for(const m of monsters){
if(m.type==='tomb'||(m.chain&&!m.chain.isHead))continue;
let slow=1;
for(const w of webs)if(dist(m.x,m.y,w.x,w.y)<w.radius){slow=w.slow;break;}
if(m.knockback){m.x+=m.knockback.x;m.y+=m.knockback.y;m.knockback.x*=0.8;m.knockback.y*=0.8;}
let dx=player.x-m.x,dy=player.y-m.y,d=Math.hypot(dx,dy);
if(m.reverseTimer>0){dx=m.x-player.x;dy=m.y-player.y;d=Math.hypot(dx,dy);}
if(d>1){m.x+=(dx/d)*m.speed*slow;m.y+=(dy/d)*m.speed*slow;}
if(m.reverseTimer>0)m.reverseTimer--;
m.x=clamp(m.x,20,WORLD_W-20);m.y=clamp(m.y,20,WORLD_H-20);
resolveWallCollision(m,m.radius);
if(dist(m.x,m.y,player.x,player.y)<24+m.radius&&player.invincible<=0){
if(m.type==='spider'){
let normalDmg=m.damage/4;
player.hp-=normalDmg;
applyPoison(player,m.damage*0.75,normalDmg);
addMessage(`🕷️ 中毒!每秒损失 ${normalDmg.toFixed(1)} 生命`,'#a020f0');
}else{player.hp-=m.damage;}
player.invincible=18;
let ang=Math.atan2(player.y-m.y,player.x-m.x);
player.x+=Math.cos(ang)*30;player.y+=Math.sin(ang)*30;
player.x=clamp(player.x,28,WORLD_W-28);player.y=clamp(player.y,28,WORLD_H-28);
resolveWallCollision(player,24);
targetPos.x=player.x;targetPos.y=player.y;
}
if(m.hp<=0)killMonster(m);
}
updateCentipedeChains();
monsters=monsters.filter(m=>m.hp>0);
}
function handleCentipedeDeath(m){
if(!m.chain)return;
let prev=m.chain.prev,next=m.chain.next;
if(prev)prev.chain.next=next;
if(next)next.chain.prev=prev;
if(m.chain.isHead&&next){next.chain.isHead=true;next.chain.prev=null;}
else if(next){next.chain.prev=null;next.chain.isHead=true;}
}
function updateSummons(){
for(const s of summons){
let closest=null,cd=Infinity;
for(const m of monsters){let d=dist(s.x,s.y,m.x,m.y);if(d<cd){cd=d;closest=m;}}
if(closest){let dx=closest.x-s.x,dy=closest.y-s.y,d=Math.hypot(dx,dy);if(d>1){s.x+=(dx/d)*s.speed;s.y+=(dy/d)*s.speed;}}
if(s.attackCd>0)s.attackCd--;
if(s.attackCd<=0&&monsters.length){
let best=null,bestDist=Infinity;
for(const m of monsters){
let ar=(s.radius||14)+(m.radius||22)+15;
let d=dist(s.x,s.y,m.x,m.y);
if(d<ar&&m.invincible<=0&&d<bestDist){best=m;bestDist=d;}
}
if(best){
best.hp-=s.dmg;s.hp-=best.damage;s.attackCd=20;
let ang=Math.atan2(best.y-s.y,best.x-s.x);
best.knockback=best.knockback||{x:0,y:0};
best.knockback.x+=Math.cos(ang)*15;best.knockback.y+=Math.sin(ang)*15;
}
}
s.x=clamp(s.x,20,WORLD_W-20);s.y=clamp(s.y,20,WORLD_H-20);
resolveWallCollision(s,s.radius||14);
}
summons=summons.filter(s=>s.hp>0);
}
function rollDropGrade(grade){
let table=DROP_GRADE_TABLE[grade];
let r=Math.random();
for(let[g,prob]of table)if(r<prob)return g;
return table[table.length-1][0];
}
function killMonster(m){
if(m.chain)handleCentipedeDeath(m);
addExperience(m.expValue);killCount++;
let totems=petals.filter(p=>p.type==='totem');
if(totems.length){
let heal=totems.reduce((s,p)=>s+3*gradeMult(p.grade),0);
player.healStacks.push({power:heal,duration:150});
addMessage(`🗿 图腾回血 +${heal.toFixed(0)}`,'#90ff90');
}
if(!m.chain||m.chain.isHead){
let dropType=DROP_TABLE[m.type];
if(dropType){
let rolls=(m.grade===8)?50:5;
let paperCount=0;
for(let s=0;s<7;s++){let idx=slots[s];if(idx!==null&&inventory[idx]?.type==='paper'&&inventory[idx].grade===m.grade)paperCount++;}
rolls+=paperCount;
for(let i=0;i<rolls;i++)drops.push({type:dropType,grade:rollDropGrade(m.grade),x:m.x,y:m.y});
}
}
}
function updatePetals(){
let px=player.x,py=player.y;
for(const p of petals){
p.angle+=player.orbitSpeed*p.speedOffset;
if(p.cd>0){p.cd--;continue;}
if(p.dmg<=0)continue;
for(const m of monsters){
if(dist(px,py,m.x,m.y)<p.range+(m.radius||22)+10){
if(m.invincible<=0){
m.hp-=p.dmg;m.knockback=m.knockback||{x:0,y:0};
let a=Math.atan2(m.y-py,m.x-px);
m.knockback.x=Math.cos(a)*20;m.knockback.y=Math.sin(a)*20;
if(p.type==='vector')m.reverseTimer=180;
}
p.cd=p.maxCd;
if(p.type==='lightning')monsters.filter(m2=>m2!==m&&dist(m.x,m.y,m2.x,m2.y)<90).slice(0,2).forEach(m2=>{if(m2.invincible<=0)m2.hp-=p.dmg*0.6;});
if(p.type==='starfish'&&player.hp/player.maxHp<=0.8)player.hp=Math.min(player.maxHp,player.hp+7*gradeMult(p.grade));
if(p.type==='beetle_egg'&&Math.random()<0.25&&summons.length<MAX_SUMMONS){
let gm=gradeMult(p.grade),hm=1+talents[0]*0.1;
summons.push({x:px+random(-30,30),y:py+random(-30,30),hp:40*gm*2*hm,maxHp:40*gm*2*hm,dmg:16*gm*player.dmgMul,speed:3.0+p.grade*0.15,attackCd:0,radius:14*Math.pow(1.05,p.grade)});
}
break;
}
}
}
}
function selectWorld(w){
selectedWorld=w;
currentState=State.AREA_SELECT;
}
function enterArea(col,row){
worldId=selectedWorld;
monsters=[];drops=[];projectiles=[];webs=[];summons=[];
let spawnX,spawnY,attempts=0;
do{
spawnX=col*ROOM_W+random(60,ROOM_W-60);
spawnY=row*ROOM_H+random(60,ROOM_H-60);
attempts++;
}while(attempts<100);
player.x=spawnX;player.y=spawnY;
targetPos.x=player.x;targetPos.y=player.y;
resolveWallCollision(player,24);
player.hp=player.maxHp;player.invincible=0;player.healStacks=[];
player.pendingPoison=0;player.poisonTickRate=0;
rebuildPetals();
for(let i=0;i<8;i++)spawnMonster();
currentState=State.EXPLORING;backpackOpen=false;backpackScrollY=0;
addMessage(`进入 ${WORLD_NAMES[worldId]} ${GRADE_NAMES[ZONE_GRADE[row][col]]}区`,'#a0d080');
}
function update(){
if(currentState!==State.EXPLORING||backpackOpen)return;
let dx=targetPos.x-player.x,dy=targetPos.y-player.y,d=Math.hypot(dx,dy);
if(d>5){let spd=Math.min(player.speed,d);player.x+=(dx/d)*spd;player.y+=(dy/d)*spd;}
player.x=clamp(player.x,28,WORLD_W-28);player.y=clamp(player.y,28,WORLD_H-28);
resolveWallCollision(player,24);
if(player.invincible>0)player.invincible--;
processPoison(player);
for(let i=player.healStacks.length-1;i>=0;i--){
if(frame%8===0)player.hp=Math.min(player.maxHp,player.hp+player.healStacks[i].power);
if(--player.healStacks[i].duration<=0)player.healStacks.splice(i,1);
}
updatePetals();updateMonsters();updateSummons();resolveEntityCollisions();
for(let i=drops.length-1;i>=0;i--)if(dist(player.x,player.y,drops[i].x,drops[i].y)<45){addToInventory(drops[i].type,drops[i].grade,1);drops.splice(i,1);}
if(frame%70===0)spawnMonster();
if(player.hp<=0){currentState=State.DEAD;addMessage('💀 你倒下了...','#ffaaaa');}
frame++;
for(const msg of messages)msg.timer--;
messages=messages.filter(m=>m.timer>0);
}
function draw(){
ctx.clearRect(0,0,CW,CH);
if(currentState===State.MAIN)drawMainMenu();
else if(currentState===State.WORLD_SELECT)drawWorldSelect();
else if(currentState===State.AREA_SELECT)drawAreaSelect();
else if(currentState===State.TALENT)drawTalent();
else if(currentState===State.DEAD)drawDead();
else if(currentState===State.PAUSED)drawPaused();
else{
let vx=player.x-CW/2,vy=player.y-CH/2;
for(let row=0;row<3;row++)for(let col=0;col<3;col++){
let x=col*ROOM_W-vx,y=row*ROOM_H-vy;
ctx.fillStyle=GRADE_COLORS[ZONE_GRADE[row][col]]+'22';
ctx.fillRect(x,y,ROOM_W,ROOM_H);
}
ctx.strokeStyle='#3a5a3a44';ctx.lineWidth=1;
for(let x=-100;x<CW+100;x+=100)ctx.strokeRect(x-(vx%100),0,0,CH);
for(let y=-100;y<CH+100;y+=100)ctx.strokeRect(0,y-(vy%100),CW,0);
ctx.fillStyle='#4a4a4a';
for(const w of walls){let sx=w.x-vx,sy=w.y-vy;ctx.fillRect(sx,sy,w.w,w.h);}
ctx.fillStyle='#5db0e0';ctx.beginPath();ctx.arc(CW/2,CH/2,24,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#fff';ctx.font='16px Arial';ctx.fillText('⚪',CW/2-10,CH/2+6);
for(const p of petals){
let px=player.x+Math.cos(p.angle)*player.range-vx,py=player.y+Math.sin(p.angle)*player.range-vy;
ctx.fillStyle=GRADE_COLORS[p.grade];ctx.beginPath();ctx.arc(px,py,14,0,Math.PI*2);ctx.fill();
if(p.cd>0){ctx.fillStyle='#0008';ctx.beginPath();ctx.arc(px,py,14,0,Math.PI*2);ctx.fill();}
ctx.fillStyle='#fff';ctx.font='14px Arial';ctx.fillText(p.icon,px-10,py+6);
}
for(const m of monsters){
let sx=m.x-vx,sy=m.y-vy,r=m.radius||22;
ctx.fillStyle=GRADE_COLORS[m.grade]+'88';ctx.beginPath();ctx.arc(sx,sy,r+6,0,Math.PI*2);ctx.fill();
ctx.fillStyle=m.color;ctx.beginPath();ctx.arc(sx,sy,r,0,Math.PI*2);ctx.fill();
let bw=r*2+8,bh=6,bx=sx-bw/2,by=sy-r-12;
ctx.fillStyle='#300';ctx.fillRect(bx,by,bw,bh);
ctx.fillStyle='#6f6';ctx.fillRect(bx,by,bw*(m.hp/m.maxHp),bh);
ctx.fillStyle='#fff';ctx.font=`${Math.min(16+m.grade*2,24)}px Arial`;ctx.fillText(m.icon,sx-10,sy+6);
}
for(const s of summons){
let sx=s.x-vx,sy=s.y-vy,r=s.radius||14;
ctx.fillStyle='#8a6';ctx.beginPath();ctx.arc(sx,sy,r,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#fff';ctx.font='12px Arial';ctx.fillText('🪲',sx-8,sy+5);
}
for(const d of drops){
ctx.fillStyle=GRADE_COLORS[d.grade];ctx.beginPath();ctx.arc(d.x-vx,d.y-vy,12,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#fff';ctx.font='12px Arial';ctx.fillText(PETAL_STATS[d.type]?.icon,d.x-vx-8,d.y-vy+5);
}
ctx.fillStyle='#1a2a1aaa';ctx.fillRect(18,18,264,50);
ctx.fillStyle='#c44';ctx.fillRect(20,20,260*(player.hp/player.maxHp),26);
let needExp=playerLevel*25;
ctx.fillStyle='#4da6ff';ctx.fillRect(20,48,260*(playerExp/needExp),14);
if(player.pendingPoison>0){
ctx.fillStyle='#a020f0';ctx.fillRect(20,64,50,8);
ctx.fillStyle='#fff';ctx.font='10px Arial';ctx.fillText(`毒 ${player.pendingPoison.toFixed(0)}`,75,72);
}
ctx.fillStyle='#fff';ctx.font='bold 18px Arial';
ctx.fillText(`❤️ ${Math.floor(player.hp)}/${player.maxHp}`,30,40);
ctx.fillText(`Lv.${playerLevel}`,CW-200,65);ctx.fillText(`击杀:${killCount}`,CW-200,100);
ctx.font='12px Arial';ctx.fillText(`EXP ${playerExp}/${needExp}`,30,60);
drawButton('⏸️',CW-180,30,80,50);drawButton('🎒',CW-90,30,80,50);
if(backpackOpen)drawBackpack();
}
drawTooltip();
}
function drawMainMenu(){
ctx.fillStyle='#f3e9c0';ctx.font='bold 54px Arial';ctx.textAlign='center';ctx.fillText('🌸 花瓣防卫',CW/2,130);
drawButton('🌍 世界',350,220,200,70);drawButton('✨ 天赋',600,220,200,70);drawButton('🎒 背包',850,220,200,70);
ctx.font='28px Arial';ctx.fillStyle='#ddd';ctx.fillText(`等级 ${playerLevel} 天赋点 ${talentPoints}`,CW/2,400);
}
function drawWorldSelect(){
ctx.fillStyle='#efe6c3';ctx.font='44px Arial';ctx.textAlign='center';ctx.fillText('选择世界',CW/2,150);
drawButton('🌿 花园',200,300,180,80);drawButton('🏜️ 沙漠',500,300,180,80);
drawButton('🌊 海洋',800,300,180,80);drawButton('🖼️ 图文',350,450,180,80);
drawButton('← 返回',80,650,140,60);
}
function drawAreaSelect(){
ctx.fillStyle='#efe6c3';ctx.font='38px Arial';ctx.textAlign='center';ctx.fillText('选择出生区域',CW/2,70);
for(let row=0;row<3;row++)for(let col=0;col<3;col++){
let g=ZONE_GRADE[row][col];
drawButton(`${GRADE_NAMES[g]}`,300+col*300,200+row*180,200,60,GRADE_COLORS[g]);
}
drawButton('← 返回',80,650,140,60);
}
function drawTalent(){
ctx.fillStyle='#efe6c3';ctx.font='44px Arial';ctx.textAlign='center';ctx.fillText('✨ 天赋树',CW/2,100);
for(let i=0;i<5;i++)drawButton(`${TALENT_NAMES[i]} ${talents[i]}/${TALENT_MAX[i]}`,250,250+i*70,320,50,hoverSlot===i?'#b89e5a':'#8a6e4a');
drawButton('返回',900,600,140,60);
ctx.fillStyle='#ffd700';ctx.font='28px Arial';ctx.fillText(`剩余天赋点: ${talentPoints}`,CW/2,650);
}
function drawDead(){
ctx.fillStyle='#200a';ctx.fillRect(250,150,700,450);
ctx.fillStyle='#faa';ctx.font='48px Arial';ctx.textAlign='center';ctx.fillText('💀 你倒下了',CW/2,320);
drawButton('返回主界面',CW/2-100,480,200,70);
}
function drawPaused(){
ctx.fillStyle='#1a2a1aee';ctx.fillRect(300,200,600,400);
ctx.fillStyle='#f0e6c0';ctx.font='44px Arial';ctx.textAlign='center';ctx.fillText('⏸️ 暂停',CW/2,300);
drawButton('继续',CW/2-100,380,200,60);drawButton('返回主界面',CW/2-100,460,200,60);
}
function drawBackpack(){
ctx.fillStyle='#1e2e1eec';ctx.fillRect(200,80,800,600);
ctx.save();ctx.beginPath();ctx.rect(200,80,800,600);ctx.clip();
ctx.fillStyle='#f0e6c0';ctx.font='28px Arial';ctx.textAlign='center';ctx.fillText('🎒 背包',CW/2,130);
let startY=170,ih=55,totalH=startY+inventory.length*ih+20,maxScroll=Math.max(0,totalH-600);
backpackScrollY=clamp(backpackScrollY,0,maxScroll);
for(let i=0;i<inventory.length;i++){
let item=inventory[i],y=startY+i*ih-backpackScrollY;
ctx.fillStyle=selectedInvIndex===i?'#ffd700':GRADE_COLORS[item.grade];
ctx.fillRect(330,y,330,40);
ctx.fillStyle='#fff';ctx.font='16px Arial';
ctx.fillText(`${PETAL_STATS[item.type].icon} ${PETAL_STATS[item.type].name} [${GRADE_SHORT[item.grade]}] x${item.count}`,350,y+26);
if(item.count>=5&&item.grade<MAX_GRADE)drawButton('合成',730,y+20,70,35);
}
ctx.restore();
for(let s=0;s<7;s++){
ctx.fillStyle='#4a6a3a';ctx.fillRect(840,190+s*65,100,42);
if(slots[s]!==null)ctx.fillText(PETAL_STATS[inventory[slots[s]].type].icon,890,218+s*65);
}
drawButton('关闭',600,640,100,40);
}
function drawButton(text,x,y,w,h,col='#8a6e4a'){
ctx.fillStyle=col;ctx.beginPath();ctx.roundRect(x-w/2,y-h/2,w,h,12);ctx.fill();
ctx.fillStyle='#fff';ctx.font='bold 22px Arial';ctx.textAlign='center';ctx.fillText(text,x,y+8);
}
CanvasRenderingContext2D.prototype.roundRect=function(x,y,w,h,r){
if(w<2*r)r=w/2;if(h<2*r)r=h/2;
this.moveTo(x+r,y);this.lineTo(x+w-r,y);this.quadraticCurveTo(x+w,y,x+w,y+r);
this.lineTo(x+w,y+h-r);this.quadraticCurveTo(x+w,y+h,x+w-r,y+h);
this.lineTo(x+r,y+h);this.quadraticCurveTo(x,y+h,x,y+h-r);this.lineTo(x,y+r);
this.quadraticCurveTo(x,y,x+r,y);return this;
};
function updateHoverInfo(mx,my){
hoverInvIndex=-1;hoverSlot=-1;hoverMonsterIdx=-1;hoverDropIdx=-1;
if(currentState===State.BACKPACK||(currentState===State.EXPLORING&&backpackOpen)){
const adj=my+backpackScrollY;
for(let i=0;i<inventory.length;i++)if(btn(370,170+i*55,340,45,mx,adj)){hoverInvIndex=i;break;}
for(let s=0;s<7;s++)if(btn(850,190+s*65,110,45,mx,my)){hoverSlot=s;break;}
}
if(currentState===State.EXPLORING&&!backpackOpen){
const vx=player.x-CW/2,vy=player.y-CH/2;
for(let i=monsters.length-1;i>=0;i--){
let m=monsters[i],sx=m.x-vx,sy=m.y-vy;
if(dist(mx,my,sx,sy)<(m.radius||22)+6){hoverMonsterIdx=i;break;}
}
for(let i=drops.length-1;i>=0;i--){
let d=drops[i],sx=d.x-vx,sy=d.y-vy;
if(dist(mx,my,sx,sy)<16){hoverDropIdx=i;break;}
}
}
}
function drawTooltip(){
let tipText='',tipSubText='',tipColor='#fff';
if(hoverInvIndex>=0&&inventory[hoverInvIndex]){
let item=inventory[hoverInvIndex],stats=getPetalStats(item.type,item.grade);
let hm=1+talents[0]*0.1;
tipText=`${stats.icon} ${stats.name} [${GRADE_NAMES[item.grade]}]`;
tipSubText=`数量:${item.count} 伤害:${(stats.dmg*player.dmgMul).toFixed(1)} HP:${(stats.hp*hm).toFixed(0)} CD:${stats.cd}\n${PETAL_STATS[item.type].effect}`;
if(item.type==='mystic_powder')tipSubText+=`\n移速倍率:×${POWDER_SPEED_MULT[item.grade].toFixed(1)}`;
if(item.grade<MAX_GRADE)tipSubText+=`\n合成率:${Math.round(COMBINE_CHANCES[item.grade]*100)}%`;
tipColor=GRADE_COLORS[item.grade];
}else if(hoverSlot>=0){
let idx=slots[hoverSlot];
if(idx!==null&&inventory[idx]){
let item=inventory[idx],stats=getPetalStats(item.type,item.grade);
let hm=1+talents[0]*0.1;
tipText=`槽${hoverSlot+1}: ${stats.icon} ${stats.name} [${GRADE_NAMES[item.grade]}]`;
tipSubText=`伤害:${(stats.dmg*player.dmgMul).toFixed(1)} HP:${(stats.hp*hm).toFixed(0)} CD:${stats.cd}\n${PETAL_STATS[item.type].effect}`;
tipColor=GRADE_COLORS[item.grade];
}else{tipText=`槽${hoverSlot+1}: 空`;tipColor='#aaa';}
}else if(hoverMonsterIdx>=0){
let m=monsters[hoverMonsterIdx];
tipText=`${m.icon} ${m.name} [${GRADE_NAMES[m.grade]}]`;
tipSubText=`HP:${Math.ceil(m.hp)}/${Math.ceil(m.maxHp)} 伤:${Math.ceil(m.damage)} EXP:${m.expValue}`;
tipColor=GRADE_COLORS[m.grade];
}else if(hoverDropIdx>=0){
let d=drops[hoverDropIdx];
tipText=`${PETAL_STATS[d.type]?.icon} ${GRADE_NAMES[d.grade]}`;
tipColor=GRADE_COLORS[d.grade];
}
if(tipText){
let tx=mouse.x+20,ty=mouse.y-20;
ctx.font='bold 18px Arial';
let lines=tipSubText?tipSubText.split('\n'):[];
let maxW=ctx.measureText(tipText).width;
for(let l of lines){ctx.font='15px Arial';maxW=Math.max(maxW,ctx.measureText(l).width);}
let tw=maxW+28,th=32+(lines.length*22);
let x=clamp(tx,10,CW-tw-10),y=clamp(ty-th,10,CH-th-10);
ctx.fillStyle='#000e';ctx.fillRect(x,y,tw,th);
ctx.fillStyle=tipColor;ctx.font='bold 18px Arial';ctx.fillText(tipText,x+14,y+26);
if(lines.length){ctx.fillStyle='#ddd';ctx.font='15px Arial';for(let i=0;i<lines.length;i++)ctx.fillText(lines[i],x+14,y+52+i*22);}
}
}
function combinePetals(idx){
let item=inventory[idx];
if(!item||item.count<5||item.grade>=MAX_GRADE)return false;
let chance=COMBINE_CHANCES[item.grade];
if(Math.random()<chance){
item.count-=5;if(item.count<=0)inventory.splice(idx,1);
addToInventory(item.type,item.grade+1,1);
rebuildPetals();addMessage('✨ 合成成功!','#90ff90');
}else{
item.count-=4;if(item.count<=0)inventory.splice(idx,1);
rebuildPetals();addMessage('💔 合成失败','#ff9090');
}
return true;
}
function btn(x,y,w,h,mx,my){return mx>x-w/2&&mx<x+w/2&&my>y-h/2&&my<y+h/2;}
canvas.addEventListener('mousemove',e=>{
let rect=canvas.getBoundingClientRect();
mouse.x=(e.clientX-rect.left)*(CW/rect.width);
mouse.y=(e.clientY-rect.top)*(CH/rect.height);
if(currentState===State.EXPLORING&&!backpackOpen){
targetPos.x=player.x+(mouse.x-CW/2);targetPos.y=player.y+(mouse.y-CH/2);
}
updateHoverInfo(mouse.x,mouse.y);
});
canvas.addEventListener('mousedown',e=>{
let mx=mouse.x,my=mouse.y;
if(currentState===State.MAIN){
if(btn(350,220,200,70,mx,my))currentState=State.WORLD_SELECT;
else if(btn(600,220,200,70,mx,my))currentState=State.TALENT;
else if(btn(850,220,200,70,mx,my)){backpackOpen=true;currentState=State.BACKPACK;}
}else if(currentState===State.WORLD_SELECT){
if(btn(200,300,180,80,mx,my))selectWorld('garden');
else if(btn(500,300,180,80,mx,my))selectWorld('desert');
else if(btn(800,300,180,80,mx,my))selectWorld('ocean');
else if(btn(350,450,180,80,mx,my))selectWorld('tw');
else if(btn(80,650,140,60,mx,my))currentState=State.MAIN;
}else if(currentState===State.AREA_SELECT){
for(let row=0;row<3;row++)for(let col=0;col<3;col++){
if(btn(300+col*300,200+row*180,200,60,mx,my)){enterArea(col,row);return;}
}
if(btn(80,650,140,60,mx,my))currentState=State.WORLD_SELECT;
}else if(currentState===State.EXPLORING||currentState===State.BACKPACK){
if(btn(CW-90,30,80,50,mx,my)){backpackOpen=!backpackOpen;currentState=backpackOpen?State.BACKPACK:State.EXPLORING;}
else if(btn(CW-180,30,80,50,mx,my)){previousState=currentState;currentState=State.PAUSED;}
if(backpackOpen)handleBackpackClick(mx,my);
}else if(currentState===State.TALENT){
for(let i=0;i<5;i++)if(btn(250,250+i*70,320,50,mx,my)){
if(talentPoints>0&&talents[i]<TALENT_MAX[i]){talents[i]++;talentPoints--;updatePlayerStats();addMessage(`${TALENT_NAMES[i]} +1`,'#aad4ff');}
else addMessage('无法升级','#ffaa00');
}
if(btn(900,600,140,60,mx,my))currentState=State.MAIN;
}else if(currentState===State.DEAD){if(btn(CW/2-100,480,200,70,mx,my)){player.hp=player.maxHp;currentState=State.MAIN;}}
else if(currentState===State.PAUSED){
if(btn(CW/2-100,350,200,60,mx,my))currentState=previousState;
if(btn(CW/2-100,430,200,60,mx,my))currentState=State.MAIN;
}
});
canvas.addEventListener('wheel',e=>{if(backpackOpen)backpackScrollY+=e.deltaY*0.8;});
function handleBackpackClick(mx,my){
let adj=my+backpackScrollY;
for(let i=0;i<inventory.length;i++){
if(btn(370,170+i*55,340,45,mx,adj)){selectedInvIndex=i;return;}
if(inventory[i]?.count>=5&&inventory[i].grade<MAX_GRADE&&btn(730,170+i*55,70,35,mx,adj)){combinePetals(i);return;}
}
for(let s=0;s<7;s++){
if(btn(850,190+s*65,110,45,mx,my)){
if(selectedInvIndex!==-1){if(inventory[selectedInvIndex]?.count>0)slots[s]=selectedInvIndex;}
else slots[s]=null;
rebuildPetals();selectedInvIndex=-1;return;
}
}
if(btn(600,640,100,40,mx,my)){backpackOpen=false;currentState=State.EXPLORING;}
}
window.addEventListener('keydown',e=>{
if(e.key==='Escape'){
if(currentState===State.BACKPACK){backpackOpen=false;currentState=State.EXPLORING;}
else if(currentState===State.EXPLORING){previousState=currentState;currentState=State.PAUSED;}
else if(currentState===State.PAUSED)currentState=previousState;
}
});
initGame();
function gameLoop(){update();draw();requestAnimationFrame(gameLoop);}
gameLoop();
})();
</script>
</body>
</html>