吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 443|回复: 10
上一主题 下一主题
收起左侧

[学习记录] 暖阳弹球

[复制链接]
跳转到指定楼层
楼主
fssq121 发表于 2026-8-18 16:24 回帖奖励
哈喽朋友们,又让千问写了一个物理弹球游戏。
六七年前,在九游上玩过一个小游戏,叫物理弹球,发射一串小球在一堆球里弹来弹去,看着很解压,可惜没多久九游下了小游戏,今日想起就让千问3.8-max写了一个类似的。
还是单html文件,复制后存为html用浏览器打开即可畅玩,手机/pc皆适配。
上次的坦克大战没贴图,这次学会怎么贴了:

html代码如下:
[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">&#9876; 攻击 <b id="uiAtk">1</b></div>
    <div class="chip">● 弹球 <b id="uiBalls">1</b></div>
    <div class="chip">&#127942; 最高 <b id="uiBest">0</b></div>
    <button id="btnMute">&#128266;</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&#178;*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,'&#9876; +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,'&#9876; +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?'&#128263;':'&#128266;';};

/* ---------------- 主循环 ---------------- */
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>

免费评分

参与人数 3吾爱币 +3 热心值 +3 收起 理由
barrygao + 1 + 1 我很赞同!
steam0825 + 1 + 1 我很赞同!
xuhao4577062311 + 1 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
fjjowhs 发表于 2026-8-19 05:13
楼主厉害,试了,好玩。
3#
Async404 发表于 2026-8-19 07:26
4#
lfer 发表于 2026-8-19 07:57
这种单html文件,确实可以缓解多端复用的问题。
5#
lbdcyy 发表于 2026-8-19 08:01
这个可以有
6#
lhrlyz1314 发表于 2026-8-19 08:14
像极了小时候玩的那种弹球
7#
gs18090 发表于 2026-8-19 09:32
很好玩啊,就是点击声音会乱码
8#
Wuaipojielong 发表于 2026-8-19 09:36
这摸鱼用挺好
9#
angiefans 发表于 2026-8-19 09:54
这个也太好玩了,停不下来
10#
牛人王老五 发表于 2026-8-19 10:03
好玩! 玩到157关.
反馈一个小bug, 右上角的喇叭图案, 点击后会有乱码
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-8-19 15:44

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表