[Asm] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>暖阳弹球</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{height:100%;overflow:hidden}
body{
display:flex;align-items:center;justify-content:center;touch-action:none;
/* 暖白色背景:奶油色到浅杏色的径向渐变 */
background:radial-gradient(1200px 900px at 50% 0%, #fffdf5, #fdf6e3 40%, #f5ebe0 100%);
font-family:"PingFang SC","Microsoft YaHei",system-ui,sans-serif;color:#5d4e37;user-select:none;
}
#wrap{display:flex;flex-direction:column;align-items:center;gap:8px;padding:10px;width:100%}
/* UI 调整为暖色系 */
#hud{display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:center}
.chip{
background:rgba(255,255,255,.65);border:1px solid rgba(180,140,100,.2);
padding:5px 14px;border-radius:999px;font-size:13px;letter-spacing:.5px;
box-shadow:0 2px 8px rgba(180,140,100,.08);color:#6b5744;
}
.chip b{color:#d35400;font-variant-numeric:tabular-nums;margin-left:2px}
#btnMute{
background:rgba(255,255,255,.65);border:1px solid rgba(180,140,100,.2);color:#6b5744;
border-radius:999px;padding:5px 12px;font-size:14px;cursor:pointer;
box-shadow:0 2px 8px rgba(180,140,100,.08);
}
#stage{position:relative}
canvas{
display:block;border-radius:24px;touch-action:none;
width:min(94vw, calc((100vh - 132px) * .6));
/* 柔和的暖色阴影替代冷色发光 */
box-shadow:0 20px 50px rgba(180,140,100,.15), inset 0 0 0 1px rgba(255,255,255,.8);
background:transparent;
}
#overlay{
position:absolute;inset:0;display:none;align-items:center;justify-content:center;
background:rgba(253,246,227,.85);backdrop-filter:blur(8px);border-radius:24px;text-align:center;z-index:5;
}
#overlay.show{display:flex}
.panel{padding:28px;max-width:88%}
.panel h1{
font-size:32px;letter-spacing:4px;margin-bottom:12px;
/* 暖色渐变标题 */
background:linear-gradient(135deg,#e67e22,#d35400,#c0392b);
-webkit-background-clip:text;background-clip:text;color:transparent;
}
.panel .sub{font-size:13px;line-height:2.1;color:#7f6c56;margin-bottom:18px;text-align:left;display:inline-block}
.panel .sub em{font-style:normal;color:#d35400;font-weight:600}
.panel .sub i{font-style:normal;color:#27ae60;font-weight:600}
.stats{font-size:14px;color:#5d4e37;line-height:2;margin-bottom:16px}
.stats b{color:#d35400}
#btnStart{
border:none;cursor:pointer;font-size:17px;letter-spacing:4px;color:#fff;
padding:14px 48px;border-radius:999px;font-weight:700;
/* 暖橙色按钮 */
background:linear-gradient(135deg,#f39c12,#e67e22);
box-shadow:0 8px 24px rgba(230,126,34,.3);transition:transform .1s;
}
#btnStart:active{transform:scale(.96)}
#tip{font-size:12px;color:#a08c76;letter-spacing:1px;margin-top:4px}
</style>
</head>
<body>
<div id="wrap">
<div id="hud">
<div class="chip">波次 <b id="uiWave">1</b></div>
<div class="chip">⚔ 攻击 <b id="uiAtk">1</b></div>
<div class="chip">● 弹球 <b id="uiBalls">1</b></div>
<div class="chip">🏆 最高 <b id="uiBest">0</b></div>
<button id="btnMute">🔊</button>
</div>
<div id="stage">
<canvas id="cv"></canvas>
<div id="overlay"></div>
</div>
<div id="tip">拖动瞄准 · 拖得越远力度越大 · 松手发射</div>
</div>
<script>
'use strict';
/* ================= 暖系解压版数值设计 =================
1. 速度呼吸感: 基础速度降低至 380,随弹射次数/时间线性增至 650
2. 攻击克制: 移除波次乘区,特殊球仅 +1 攻击,避免秒杀
3. 生命膨胀: base = 3 + w*1.8 + w²*0.12,比原版厚 40%+
4. 锚点球: 每 2-3 行生成 1-2 个大球(3x血量),阻挡垂直穿透
5. 密度提升: 每波基础球数 +1,营造弹珠池氛围
================================================== */
const W=480, H=800, ROWS=12;
const FIELD_TOP=64, ROW_H=56, CEIL=56, LAUNCH_Y=750;
const R_TOP=17, R_ANCHOR=22, R_PROJ=8, MAX_BALLS=24;
const rowY = r => FIELD_TOP + 26 + r*ROW_H;
const clamp=(v,a,b)=>v<a?a:(v>b?b:v);
const rnd=(a,b)=>a+Math.random()*(b-a);
// 暖色板定义
const PALETTE = {
normal: [
['#ffb7b2','#ff9e99'], // 蜜桃粉
['#ffdac1','#ffbe9f'], // 奶油杏
['#e2f0cb','#c5e1a5'], // 抹茶绿
['#b5ead7','#95d5c2'], // 薄荷青
['#c7ceea','#aab2d8'], // 淡藤紫
],
anchor: ['#8d6e63','#6d4c41'], // 焦糖棕(锚点球)
atk: ['#ffcc80','#fb8c00'], // 鲜橙
ball: ['#a5d6a7','#43a047'], // 翠绿
proj: ['#fff8e1','#ffe082'], // 暖黄光
bg: '#fdf6e3'
};
const cv=document.getElementById('cv'), ctx=cv.getContext('2d');
const dpr=Math.min(2, window.devicePixelRatio||1);
cv.width=W*dpr; cv.height=H*dpr; ctx.scale(dpr,dpr);
const uiWave=document.getElementById('uiWave'), uiAtk=document.getElementById('uiAtk'),
uiBalls=document.getElementById('uiBalls'), uiBest=document.getElementById('uiBest'),
overlay=document.getElementById('overlay');
let best=parseInt(localStorage.getItem('warm_bounce_best')||'0');
let S, aiming=false, aimPt=null;
/* ---------------- 暖系音效 (更柔和的频率) ---------------- */
let audioCtx=null, muted=false, lastHitSnd=0;
function tone(f,dur,type,vol,slide){
if(muted) return;
try{
if(!audioCtx) audioCtx=new (window.AudioContext||window.webkitAudioContext)();
const c=audioCtx,o=c.createOscillator(),g=c.createGain(),t=c.currentTime;
o.type=type||'sine'; o.frequency.setValueAtTime(f,t);
if(slide) o.frequency.exponentialRampToValueAtTime(Math.max(30,f+slide),t+dur);
g.gain.setValueAtTime(vol||.08,t);
g.gain.exponentialRampToValueAtTime(.0001,t+dur);
o.connect(g); g.connect(c.destination); o.start(t); o.stop(t+dur);
}catch(e){}
}
// 降低音调,使用正弦波为主,减少方波的刺耳感
const sndHit =()=>{const n=performance.now(); if(n-lastHitSnd>35){lastHitSnd=n; tone(rnd(220,320),.08,'sine',.06);}};
const sndBreak =()=>{tone(330,.2,'sine',.12,180); tone(440,.15,'triangle',.06,120);};
const sndBuff =()=>{tone(523,.12,'sine',.1); setTimeout(()=>tone(659,.15,'sine',.1),100);};
const sndLaunch=()=>tone(196,.2,'sine',.07,200);
const sndDown =()=>tone(130,.15,'sine',.06,-20);
const sndOver =()=>{[330,262,196,147].forEach((f,i)=>setTimeout(()=>tone(f,.3,'sine',.1),i*180));};
/* ---------------- 柔光星空背景 ---------------- */
const stars=[];
for(let i=0;i<50;i++) stars.push({
x:Math.random()*W, y:Math.random()*H,
r:rnd(.6,2), a:rnd(.1,.4), tw:rnd(.8,2.5), p:rnd(0,6)
});
/* ---------------- 状态 ---------------- */
function reset(){
S={state:'aim',wave:0,attack:1,balls:1,
launcher:{x:W/2,tx:W/2},
top:[],projs:[],parts:[],floats:[],
pending:0,fireTimer:0,aimDir:null,aimPower:0,
descT:0,shake:0,destroyed:0,turns:0,
sinceAtk:2,sinceBall:3,t:0,over:false};
nextWave(); updateHUD();
}
function updateHUD(){uiWave.textContent=S.wave;uiAtk.textContent=S.attack;uiBalls.textContent=S.balls;uiBest.textContent=best;}
/* ---------------- 生成系统 (含锚点球) ---------------- */
function rollHP(w, isAnchor){
// 基础生命大幅提升
let base = 3 + w*1.8 + w*w*0.12;
if(isAnchor) base *= 3; // 锚点球 3 倍血量
return Math.max(1, Math.round(base * rnd(0.7, 1.2)));
}
function makeBall(x, type, hp, isAnchor){
return {
x, row:0, type, hp:hp||0, maxHp:hp||0,
r: isAnchor ? R_ANCHOR : R_TOP,
isAnchor: !!isAnchor,
flash:0, cd:0, pulse:rnd(0,6), dead:false,
colorIdx: Math.floor(Math.random()*PALETTE.normal.length)
};
}
function spawnWave(w){
const taken=[];
const tryPlace=(minGap)=>{
for(let i=0;i<120;i++){
const x=rnd(30,W-30); let ok=true;
for(const o of taken) if(Math.abs(o.x-x)<minGap){ok=false;break;}
if(ok) for(const b of S.top) if(b.row<=1&&Math.hypot(b.x-x,rowY(b.row)-rowY(0))<minGap){ok=false;break;}
if(ok){taken.push({x});return x;}
} return null;
};
// 1. 生成锚点球 (每 2-3 行一个,放在已有球的下方)
// 找出当前最低行
let minRow = Infinity;
for(const b of S.top) minRow = Math.min(minRow, b.row);
if(S.top.length === 0) minRow = 0;
// 在 minRow-2 或 minRow-3 的位置尝试放锚点球
const anchorRowTarget = Math.max(0, minRow - (w % 3 === 0 ? 3 : 2));
// 注意:新球总是生成在第0行,所以锚点球也是从第0行开始,只是它们会随下坠到达目标层
// 为了简化,我们直接在新生成的球中标记哪些是锚点球,它们会在后续下坠中自然分布
// 2. 普通球数量增加 (2 -> 3~5)
const nNormal = Math.min(2 + Math.floor(w/1.5), 5);
for(let i=0;i<nNormal;i++){
const x = tryPlace(R_TOP*2+14);
if(x!=null) S.top.push(makeBall(x,'normal',rollHP(w,false),false));
}
// 3. 锚点球生成 (每波有 30%-50% 概率生成 1 个)
if(Math.random() < 0.4 + w*0.02){
const x = tryPlace(R_ANCHOR*2+20);
if(x!=null) S.top.push(makeBall(x,'anchor',rollHP(w,true),true));
}
// 4. 特殊球保底
S.sinceAtk++; S.sinceBall++;
if(S.sinceAtk>=3 || Math.random()<0.22){
S.sinceAtk=0;
const x=tryPlace(R_TOP*2+16);
if(x!=null) S.top.push(makeBall(x,'atk',0,false));
}
if(S.sinceBall>=4 || Math.random()<0.18){
S.sinceBall=0;
const x=tryPlace(R_TOP*2+16);
if(x!=null) S.top.push(makeBall(x,'ball',0,false));
}
}
function nextWave(){
S.wave++; spawnWave(S.wave); S.state='aim';
S.floats.push({x:W/2,y:170,text:'第 '+S.wave+' 波',color:'#d35400',life:1,big:true});
updateHUD();
}
/* ---------------- 发射 ---------------- */
function currentAim(){
if(!aimPt) return {valid:false};
const ox=S.launcher.x, oy=LAUNCH_Y-12;
const dx=aimPt.x-ox, dy=aimPt.y-oy, d=Math.hypot(dx,dy);
if(d<24||dy>-16) return {valid:false};
return {valid:true,dir:{x:dx/d,y:dy/d},power:clamp((d-24)/240,0.12,1)};
}
function startFire(dir,power){
S.state='firing'; S.aimDir=dir; S.aimPower=power;
S.pending=S.balls; S.fireTimer=0; S.turns++;
sndLaunch();
}
function startDescend(){
for(const b of S.top) b.row++;
S.state='descend'; S.descT=0; sndDown();
}
function gameOver(){
S.over=true; S.state='over'; S.shake=6;
if(S.wave>best){best=S.wave;localStorage.setItem('warm_bounce_best',''+best);}
sndOver();
setTimeout(showOverPanel,650);
}
/* ---------------- 碰撞结算 (攻击成长放缓) ---------------- */
function hitTop(b,by,p){
if(b.dead) return;
if(b.type==='atk'){
// 攻击力成长改为固定 +1,不再随波次膨胀
S.attack += 1;
b.dead=true;
addFloat(b.x,by-8,'⚔ +1','#fb8c00');
burst(b.x,by,PALETTE.atk[0],18); S.shake=Math.min(4,S.shake+1.5); sndBuff(); updateHUD();
}else if(b.type==='ball'){
if(S.balls<MAX_BALLS){S.balls++; addFloat(b.x,by-8,'● +1','#43a047');}
else {S.attack++; addFloat(b.x,by-8,'⚔ +1','#fb8c00');}
b.dead=true;
burst(b.x,by,PALETTE.ball[0],18); S.shake=Math.min(4,S.shake+1.5); sndBuff(); updateHUD();
}else{
b.hp -= S.attack; b.flash=1;
addFloat(b.x+rnd(-6,6),by-b.r-4,'-'+S.attack,'rgba(100,80,60,.85)');
sndHit();
if(b.hp<=0){
b.dead=true; S.destroyed++;
const cols = b.isAnchor ? PALETTE.anchor : PALETTE.normal[b.colorIdx];
burst(b.x,by,cols[0],20);
S.shake=Math.min(5,S.shake+2); sndBreak();
}
}
}
function addFloat(x,y,text,color,big){
if(S.floats.length>35) S.floats.shift();
S.floats.push({x,y,text,color,life:1,big:!!big});
}
function burst(x,y,color,n){
for(let i=0;i<n;i++){
const a=Math.random()*Math.PI*2, sp=rnd(40,200);
S.parts.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-40,life:rnd(.5,1),maxLife:1,color,size:rnd(2,5)});
}
}
/* ---------------- 物理 (动态速度核心) ---------------- */
function moveProjectiles(dt){
for(let i=S.projs.length-1;i>=0;i--){
const p=S.projs[i];
// ★ 动态速度计算:随弹射次数和时间增加
// 基础速度 380,最大 650
// 每弹射一次 +8,每秒滞空 +15,上限封顶
const speedBonus = Math.min(270, p.bounces * 8 + p.age * 15);
const currentSpeed = 380 + speedBonus + 200 * p.power;
// 归一化当前速度向量并应用新速率
const oldSpd = Math.hypot(p.vx,p.vy) || 1;
const scale = currentSpeed / oldSpd;
p.vx *= scale; p.vy *= scale;
p.age += dt;
const steps=Math.max(1,Math.ceil(currentSpeed*dt/4));
let landed=false;
for(let s=0;s<steps&&!landed;s++){
p.x+=p.vx*dt/steps; p.y+=p.vy*dt/steps;
if(p.x<p.r+2){p.x=p.r+2;p.vx=Math.abs(p.vx);}
if(p.x>W-p.r-2){p.x=W-p.r-2;p.vx=-Math.abs(p.vx);}
if(p.y<CEIL+p.r){p.y=CEIL+p.r;p.vy=Math.abs(p.vy);}
for(const b of S.top){
if(b.dead) continue;
const by=rowY(b.row), dx=p.x-b.x, dy=p.y-by, rr=p.r+b.r;
if(dx*dx+dy*dy<rr*rr){
const d=Math.sqrt(dx*dx+dy*dy)||.001, nx=dx/d, ny=dy/d;
p.x=b.x+nx*(rr+.6); p.y=by+ny*(rr+.6);
const dot=p.vx*nx+p.vy*ny;
if(dot<0){p.vx-=2*dot*nx;p.vy-=2*dot*ny;}
p.bounces++; // ★ 记录弹射次数
hitTop(b,by,p);
break;
}
}
if(p.y>LAUNCH_Y+6&&p.vy>0) landed=true;
// 防水平死循环
const s2=Math.hypot(p.vx,p.vy);
if(Math.abs(p.vy)<s2*.14){
const sg=Math.sign(p.vy)||(Math.random()<.5?-1:1);
p.vy=sg*s2*.14;
const k=s2/Math.hypot(p.vx,p.vy); p.vx*=k; p.vy*=k;
}
}
p.trail.push({x:p.x,y:p.y}); if(p.trail.length>18) p.trail.shift();
if(landed){
S.launcher.tx=clamp(p.x,34,W-34);
burst(p.x,LAUNCH_Y,'rgba(255,224,130,.8)',4);
S.projs.splice(i,1);
}
}
}
/* ---------------- 主更新 ---------------- */
function update(dt){
S.t+=dt;
for(let i=S.parts.length-1;i>=0;i--){const q=S.parts[i];q.life-=dt;
if(q.life<=0){S.parts.splice(i,1);continue;} q.x+=q.vx*dt;q.y+=q.vy*dt;q.vy+=250*dt;}
for(let i=S.floats.length-1;i>=0;i--){const f=S.floats[i];f.life-=dt/(f.big?1.3:.85);
if(f.life<=0){S.floats.splice(i,1);continue;} f.y-=35*dt;}
S.shake*=Math.exp(-dt*6);
S.launcher.x+=(S.launcher.tx-S.launcher.x)*Math.min(1,dt*8);
if(S.over) return;
if(S.state==='firing'){
S.fireTimer-=dt;
if(S.pending>0&&S.fireTimer<=0){
S.fireTimer=.12; S.pending--;
// 初始速度降低,由 moveProjectiles 动态接管
const sp=380+200*S.aimPower;
S.projs.push({
x:S.launcher.x,y:LAUNCH_Y-12,
vx:S.aimDir.x*sp,vy:S.aimDir.y*sp,
r:R_PROJ,trail:[],power:S.aimPower,
bounces:0, age:0 // ★ 新增追踪变量
});
burst(S.launcher.x,LAUNCH_Y-10,'#ffe082',3);
}
if(S.pending===0&&S.projs.length===0) startDescend();
}else if(S.state==='descend'){
S.descT+=dt/.35; // 下坠也稍慢一点
if(S.descT>=1){
S.descT=0;
if(S.top.some(b=>b.row>=ROWS-1)){gameOver();return;}
nextWave();
}
}
moveProjectiles(dt);
S.top=S.top.filter(b=>!b.dead);
for(const b of S.top){b.flash=Math.max(0,b.flash-dt*4);b.cd=Math.max(0,b.cd-dt);}
}
/* ---------------- 绘制 (暖色渲染) ---------------- */
function ballColors(b){
if(b.type==='atk') return PALETTE.atk;
if(b.type==='ball') return PALETTE.ball;
if(b.isAnchor) return PALETTE.anchor;
return PALETTE.normal[b.colorIdx];
}
function ballDrawY(b){
if(S.state==='descend'){const t=1-Math.pow(1-Math.min(1,S.descT),3);return rowY(b.row)-(1-t)*ROW_H;}
return rowY(b.row);
}
function drawBall(b,y){
const special=b.type!=='normal';
const r=b.r*(1+.1*b.flash)+(special?Math.sin(S.t*3+b.pulse)*1:0);
const [c1,c2]=ballColors(b);
ctx.save();
// 暖色球用更柔和的阴影
ctx.shadowColor=c2; ctx.shadowBlur=special?16:8;
const g=ctx.createRadialGradient(b.x-r*.3,y-r*.35,r*.1,b.x,y,r);
g.addColorStop(0,'rgba(255,255,255,.9)');
g.addColorStop(.35,c1);
g.addColorStop(1,c2);
ctx.fillStyle=g;
ctx.beginPath();ctx.arc(b.x,y,r,0,7);ctx.fill();
ctx.restore();
ctx.textAlign='center';ctx.textBaseline='middle';
if(b.type==='normal'||b.isAnchor){
ctx.fillStyle=b.isAnchor?'rgba(255,255,255,.9)':'rgba(90,70,50,.85)';
const fs = b.hp<100?13:b.hp<1000?11:9;
ctx.font=(b.isAnchor?'bold ':'')+fs+'px sans-serif';
ctx.fillText(b.hp,b.x,y+.5);
// 锚点球额外画一个环标识
if(b.isAnchor){
ctx.strokeStyle='rgba(255,255,255,.4)';ctx.lineWidth=2;
ctx.beginPath();ctx.arc(b.x,y,r-4,0,7);ctx.stroke();
}
}else if(b.type==='atk'){
ctx.strokeStyle='#fff';ctx.lineWidth=2.4;ctx.lineCap='round';ctx.lineJoin='round';
ctx.beginPath();ctx.moveTo(b.x-5,y+1);ctx.lineTo(b.x,y-4);ctx.lineTo(b.x+5,y+1);
ctx.moveTo(b.x-5,y+7);ctx.lineTo(b.x,y+2);ctx.lineTo(b.x+5,y+7);ctx.stroke();
}else{
ctx.fillStyle='#fff';
[[0,-4],[-4,3],[4,3]].forEach(o=>{ctx.beginPath();ctx.arc(b.x+o[0],y+o[1],3,0,7);ctx.fill();});
}
if(b.flash>0){ctx.fillStyle='rgba(255,255,255,'+(b.flash*.4)+')';ctx.beginPath();ctx.arc(b.x,y,r,0,7);ctx.fill();}
}
function computePath(dir){
const pts=[];let x=S.launcher.x,y=LAUNCH_Y-12,vx=dir.x,vy=dir.y;
for(let i=0;i<1200;i++){
x+=vx*5;y+=vy*5;
if(x<10){x=10;vx=Math.abs(vx);} if(x>W-10){x=W-10;vx=-Math.abs(vx);}
if(y<CEIL+R_PROJ){y=CEIL+R_PROJ;vy=Math.abs(vy);}
let hit=false;
for(const b of S.top){if(!b.dead&&Math.hypot(b.x-x,rowY(b.row)-y)<b.r+R_PROJ){hit=true;break;}}
if(hit){pts.push({x,y,hit:true});break;}
if(i%3===0)pts.push({x,y});
if(y>LAUNCH_Y)break;
}
return pts;
}
function draw(){
ctx.clearRect(0,0,W,H);
// 暖白画布底色
ctx.fillStyle='#fdf8ef';
ctx.fillRect(0,0,W,H);
// 柔光星点
for(const s of stars){
ctx.globalAlpha=s.a*(.5+.5*Math.sin(S.t*s.tw+s.p));
ctx.fillStyle='#e8d5b7';ctx.beginPath();ctx.arc(s.x,s.y,s.r,0,7);ctx.fill();
}
ctx.globalAlpha=1;
ctx.save();
if(S.shake>.1)ctx.translate(rnd(-1,1)*S.shake,rnd(-1,1)*S.shake);
// 层线 (暖灰色)
ctx.strokeStyle='rgba(160,140,118,.12)';ctx.lineWidth=1;
for(let i=0;i<=ROWS;i++){const y=rowY(0)-ROW_H/2+i*ROW_H;
ctx.beginPath();ctx.moveTo(12,y);ctx.lineTo(W-12,y);ctx.stroke();}
// 底线 (暖红虚线)
const dangerY=rowY(ROWS-1)+R_TOP+10;
ctx.strokeStyle='rgba(211,84,0,.4)';ctx.setLineDash([8,6]);ctx.lineWidth=1.5;
ctx.beginPath();ctx.moveTo(14,dangerY);ctx.lineTo(W-14,dangerY);ctx.stroke();ctx.setLineDash([]);
ctx.fillStyle='rgba(211,84,0,.45)';ctx.font='10px sans-serif';ctx.textAlign='right';ctx.textBaseline='bottom';
ctx.fillText('底 线',W-16,dangerY-4);
// 危险区暖光晕
const maxRow=S.top.reduce((m,b)=>Math.max(m,b.row),0);
if(maxRow>=ROWS-3&&S.top.length){
const a=(maxRow-(ROWS-4))/3*.18+.04+.03*Math.sin(S.t*4);
const gg=ctx.createLinearGradient(0,rowY(ROWS-3)-20,0,dangerY);
gg.addColorStop(0,'rgba(230,126,34,0)');gg.addColorStop(1,'rgba(230,126,34,'+a+')');
ctx.fillStyle=gg;ctx.fillRect(0,rowY(ROWS-3)-20,W,dangerY-rowY(ROWS-3)+20);
}
// 瞄准预览 (暖色渐变)
if(S.state==='aim'&&aiming){
const a=currentAim();
if(a.valid){
const pts=computePath(a.dir);
// 力度映射到暖色: 低力=浅黄, 高力=深橙
const hue = 45 - 30*a.power;
for(let i=0;i<pts.length;i++){const q=pts[i];
if(q.hit){
ctx.strokeStyle='hsla('+hue+',90%,55%,.8)';ctx.lineWidth=2;
ctx.beginPath();ctx.arc(q.x,q.y,R_TOP+5,0,7);ctx.stroke();break;
}
ctx.globalAlpha=Math.max(.06,.6-i/pts.length*.6);
ctx.fillStyle='hsl('+hue+',85%,60%)';
ctx.beginPath();ctx.arc(q.x,q.y,2.4,0,7);ctx.fill();
}
ctx.globalAlpha=1;
ctx.fillStyle='hsl('+hue+',80%,45%)';ctx.font='bold 13px sans-serif';ctx.textAlign='center';ctx.textBaseline='middle';
ctx.fillText('力度 '+Math.round(a.power*100)+'%',S.launcher.x,LAUNCH_Y+28);
}
}
// 顶部小球
for(const b of S.top) drawBall(b,ballDrawY(b));
// 弹球拖尾 (暖黄)
for(const p of S.projs){
for(let i=0;i<p.trail.length;i++){const q=p.trail[i];
ctx.globalAlpha=(i+1)/p.trail.length*.35;
ctx.fillStyle=PALETTE.proj[1];
ctx.beginPath();ctx.arc(q.x,q.y,R_PROJ*(i/p.trail.length*.8+.2),0,7);ctx.fill();}
ctx.globalAlpha=1;
ctx.save();ctx.shadowColor=PALETTE.proj[1];ctx.shadowBlur=12;
const pg=ctx.createRadialGradient(p.x-2,p.y-2,1,p.x,p.y,p.r+1);
pg.addColorStop(0,'#ffffff');pg.addColorStop(.5,PALETTE.proj[0]);pg.addColorStop(1,PALETTE.proj[1]);
ctx.fillStyle=pg;ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,7);ctx.fill();
ctx.restore();
}
// 粒子
for(const q of S.parts){ctx.globalAlpha=Math.max(0,q.life/q.maxLife);
ctx.fillStyle=q.color;ctx.beginPath();ctx.arc(q.x,q.y,q.size,0,7);ctx.fill();}
ctx.globalAlpha=1;
// 漂浮文字
for(const f of S.floats){
ctx.globalAlpha=Math.max(0,Math.min(1,f.life*1.5));
ctx.fillStyle=f.color;ctx.textAlign='center';ctx.textBaseline='middle';
ctx.font=f.big?'bold 24px sans-serif':'bold 13px sans-serif';
ctx.fillText(f.text,f.x,f.y);
}
ctx.globalAlpha=1;
// 发射器 (暖金)
const lx=S.launcher.x;
ctx.save();ctx.shadowColor='#f0c27f';ctx.shadowBlur=14;
const lg=ctx.createRadialGradient(lx-3,LAUNCH_Y-15,2,lx,LAUNCH_Y-12,13);
lg.addColorStop(0,'#ffffff');lg.addColorStop(.4,'#ffeaa7');lg.addColorStop(1,'#fdcb6e');
ctx.fillStyle=lg;ctx.beginPath();ctx.arc(lx,LAUNCH_Y-12,12,0,7);ctx.fill();
ctx.restore();
ctx.strokeStyle='rgba(253,203,110,.4)';ctx.lineWidth=2;
ctx.beginPath();ctx.arc(lx,LAUNCH_Y-12,17+Math.sin(S.t*2.5)*1.2,0,7);ctx.stroke();
if(S.state==='firing'&&S.pending>0){
ctx.fillStyle='rgba(253,203,110,.7)';
for(let i=0;i<S.pending;i++){
const dx2=(i-(S.pending-1)/2)*11;
ctx.beginPath();ctx.arc(lx+dx2,LAUNCH_Y+16,3,0,7);ctx.fill();
}
}
ctx.restore();
}
/* ---------------- 界面 ---------------- */
function showStartPanel(){
overlay.innerHTML=
'<div class="panel"><h1>暖阳弹球</h1>'+
'<div class="sub">'+
'◆ 拖动屏幕瞄准,松手发射,享受慢节奏弹射<br>'+
'◆ 小球初速缓慢,弹射越久速度越快,自然流畅<br>'+
'◆ 每次发射后顶球下坠一层,坠到底线即失败<br>'+
'◆ <em>橙色球</em>:提升攻击力 <i>绿色球</i>:弹球 +1<br>'+
'◆ <em>棕色大球</em>:高血量锚点,让弹射更持久<br>'+
'◆ 发射落点成为下次起点,善用走位延长弹射</div><br>'+
'<button id="btnStart">开始游戏</button></div>';
overlay.classList.add('show');
document.getElementById('btnStart').onclick=startGame;
}
function showOverPanel(){
overlay.innerHTML=
'<div class="panel"><h1>游戏结束</h1>'+
'<div class="stats">抵达波次 <b>'+S.wave+'</b> · 发射 <b>'+S.turns+'</b> 次<br>'+
'击碎顶球 <b>'+S.destroyed+'</b> 颗<br>'+
'最终攻击 <b>'+S.attack+'</b> · 弹球数 <b>'+S.balls+'</b><br>'+
'历史最佳 <b>'+best+'</b> 波</div>'+
'<button id="btnStart">再来一局</button></div>';
overlay.classList.add('show');
document.getElementById('btnStart').onclick=startGame;
}
function startGame(){
if(audioCtx&&audioCtx.state==='suspended')audioCtx.resume();
overlay.classList.remove('show'); reset();
}
/* ---------------- 输入 ---------------- */
function ptr(e){
const r=cv.getBoundingClientRect();
return {x:(e.clientX-r.left)*W/r.width, y:(e.clientY-r.top)*H/r.height};
}
cv.addEventListener('pointerdown',e=>{
e.preventDefault();
if(audioCtx&&audioCtx.state==='suspended')audioCtx.resume();
if(S.over||S.state!=='aim')return;
aiming=true;aimPt=ptr(e);cv.setPointerCapture(e.pointerId);
});
cv.addEventListener('pointermove',e=>{if(aiming){aimPt=ptr(e);e.preventDefault();}});
function release(e){
if(!aiming)return;aiming=false;
if(S.over||S.state!=='aim')return;
const a=currentAim();
if(a.valid)startFire(a.dir,a.power);
}
cv.addEventListener('pointerup',release);
cv.addEventListener('pointercancel',()=>aiming=false);
document.getElementById('btnMute').onclick=function(){muted=!muted;this.textContent=muted?'🔇':'🔊';};
/* ---------------- 主循环 ---------------- */
let last=performance.now();
function frame(now){
const dt=Math.min(.033,(now-last)/1000);last=now;
update(dt);draw();
requestAnimationFrame(frame);
}
reset(); showStartPanel(); requestAnimationFrame(frame);
</script>
</body>
</html>