🎮ArcadeLab

奶蛙夜行

by ApexPhoenix48
284 lines11.4 KB
▶ Play
<!-- ARCADELAB SINGLE FILE GAME -->
<!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,viewport-fit=cover" />
<title>奶蛙夜行</title>
<style>
html,body{margin:0;padding:0;background:#070b18;overflow:hidden;touch-action:none;-webkit-tap-highlight-color:transparent;font-family:"PingFang SC","Microsoft YaHei",sans-serif}
#wrap{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:#070b18}
canvas{background:#0b1030;image-rendering:pixelated}
#cover{position:fixed;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:rgba(3,6,18,.92);z-index:9}
#cover h1{font-size:28px;color:#7fe3a0;margin-bottom:6px;letter-spacing:2px}
#cover .sub{font-size:13px;color:#aac;margin-bottom:36px;text-align:center;line-height:1.8}
#startBtn{padding:16px 40px;font-size:20px;border:0;border-radius:12px;background:linear-gradient(135deg,#7fe3a0,#5bc08a);color:#062;font-weight:700;cursor:pointer;box-shadow:0 4px 20px rgba(127,227,160,.3)}
#startBtn:active{transform:scale(.96)}
#topbar{position:fixed;top:0;left:0;right:0;height:40px;display:none;align-items:center;justify-content:space-between;padding:0 14px;z-index:8;color:#fff}
#topbar .clabel{font-size:13px;opacity:.85}
#musicBtn{font-size:16px;background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.2);color:#fff;border-radius:8px;padding:4px 10px;cursor:pointer}
</style>
</head>
<body>
<div id="wrap"><canvas id="cv"></canvas></div>
<div id="topbar"><span class="clabel" id="clabel">第1章</span><button id="musicBtn">🎵</button></div>
<div id="cover">
  <h1>🐸 奶蛙夜行</h1>
  <div class="sub">把月光从哑剧里抢回来<br>WASD移动 · F吐奶嗝 · 空格防御 · E大笑<br>手机:左半屏摇杆 · 右半屏攻击</div>
  <button id="startBtn">深吸一口气,别忍住</button>
</div>

<script>
const cv=document.getElementById('cv');
const x=cv.getContext('2d');
const W=960,H=540;
cv.width=W;cv.height=H;

function resize(){
  const aw=window.innerWidth,ah=window.innerHeight;
  const r=Math.min(aw/W,ah/H)*.98;
  cv.style.width=W*r+'px';cv.style.height=H*r+'px';
}
window.addEventListener('resize',resize);resize();

/* ===== 音乐 ===== */
const Music=(()=>{
  let ac,on=true,mode='fight',t0=0,timer=null;
  function init(){if(ac)return;ac=new(window.AudioContext||window.webkitAudioContext)();t0=ac.currentTime}
  function blip(f,dur,typ='sine',vol=.06){
    if(!ac||!on)return;
    const o=ac.createOscillator(),g=ac.createGain();
    o.type=typ;o.frequency.value=f;
    g.gain.setValueAtTime(vol,ac.currentTime);
    g.gain.exponentialRampToValueAtTime(.0001,ac.currentTime+dur);
    o.connect(g);g.connect(ac.destination);o.start();o.stop(ac.currentTime+dur+.05);
  }
  function loop(){
    if(!ac||!on)return;
    const t=ac.currentTime-t0;
    if(mode==='fight'){
      if(t%1.4<.05)blip(440,.1,'square',.04);
      if(t%1.4>.7&&t%1.4<.75)blip(554,.08,'square',.03);
    }
    if(mode==='boss'){
      if(t%1.0<.05)blip(220,.15,'sawtooth',.05);
      if(t%1.0>.5&&t%1.0<.55)blip(330,.1,'sawtooth',.04);
    }
    if(mode==='menu'){
      if(t%2.0<.05)blip(523,.2,'triangle',.05);
    }
    timer=setTimeout(loop,120);
  }
  return{
    start(){init();if(ac.state==='suspended')ac.resume();if(!timer)loop()},
    setMode(m){mode=m;t0=ac?ac.currentTime:0},
    toggle(){on=!on;if(!on&&timer){clearTimeout(timer);timer=null}return on},
    shot(){blip(880,.06,'square',.04)},
    hit(){blip(160,.1,'sawtooth',.05)},
    laugh(){blip(660,.1,'square',.05);setTimeout(()=>blip(880,.1,'square',.05),100);setTimeout(()=>blip(550,.12,'square',.05),200)},
    pick(){blip(523,.15,'sine',.06);setTimeout(()=>blip(659,.2,'sine',.06),150)}
  }
})();

/* ===== 游戏 ===== */
let state='cover',chapter=1,waves=0;
const frog={x:160,y:H-100,w:44,h:38,hp:120,max:120,vx:0,vy:0,face:1,def:false,laugh:0,shots:[],cd:0,cdMul:1,multi:1,shield:false,defBonus:0,laughBoost:false,quake:false};
let enemies=[],pickCards=[];
const CHS=['奶笑村','咕嘎小径','回声矿坑','罐头剧场','笑纹回廊','无声钟楼','魔王喉结'];
const keys={};

addEventListener('keydown',e=>{keys[e.key.toLowerCase()]=true;if([' ','arrowup','arrowdown','arrowleft','arrowright'].includes(e.key.toLowerCase()))e.preventDefault()});
addEventListener('keyup',e=>keys[e.key.toLowerCase()]=false);

// 触控
let joyActive=false,joyOX=0,joyOY=0,skillTap=false;
cv.addEventListener('touchstart',e=>{
  e.preventDefault();
  for(const t of e.changedTouches){
    const r=cv.getBoundingClientRect();
    const px=(t.clientX-r.left)/(r.width/W),py=(t.clientY-r.top)/(r.height/H);
    if(px<W/2&&!joyActive){joyActive=true;joyOX=t.clientX;joyOY=t.clientY}
    else if(px>=W/2)fireShot();
  }
},{passive:false});
cv.addEventListener('touchmove',e=>{
  e.preventDefault();
  for(const t of e.changedTouches){
    if(joyActive){
      const dx=t.clientX-joyOX,dy=t.clientY-joyOY;
      keys['a']=dx<-18;keys['d']=dx>18;keys['w']=dy<-18;keys['s']=dy>18;
    }
  }
},{passive:false});
cv.addEventListener('touchend',e=>{
  e.preventDefault();
  for(const t of e.changedTouches){
    if(joyActive&&t.identifier===joyActive){joyActive=false;keys['w']=keys['a']=keys['s']=keys['d']=false}
  }
},{passive:false});

function fireShot(){
  if(state!=='play'||frog.cd>0)return;
  Music.shot();
  frog.shots.push({x:frog.x+frog.face*22,y:frog.y-4,vx:frog.face*10,r:7});
  if(frog.multi>1)for(let k=1;k<frog.multi;k++)frog.shots.push({x:frog.x+frog.face*22,y:frog.y-4+(k-1)*8-(frog.multi-1)*4,vx:frog.face*10,r:7});
  frog.cd=0.22*frog.cdMul;
}

function spawnWave(){
  waves++;
  const c=2+chapter;
  for(let i=0;i<c;i++)enemies.push({x:W+50+i*80,y:H-100-Math.random()*80,r:20,hp:25+chapter*8,vx:-(1.2+chapter*0.3),typ:Math.random()<.3?'jar':'mime'});
}

function dmgFrog(d){
  if(frog.def)d*=(1-(frog.defBonus||0));
  frog.hp-=d;Music.hit();
  if(frog.hp<=0){state='over';Music.setMode('menu')}
}

function openPick(){
  state='pick';Music.pick();
  const pool=[
    {t:'嗝停不下',d:'射速+30%',fn:()=>frog.cdMul*=0.7},
    {t:'回声三连嗝',d:'+2弹幕',fn:()=>frog.multi+=2},
    {t:'肚皮锅盖圆舞',d:'近身击退',fn:()=>frog.shield=true},
    {t:'肚皮反光膜',d:'防御减伤+20%',fn:()=>frog.defBonus+=0.2},
    {t:'笑震踏板',d:'冲撞冲击波',fn:()=>frog.quake=true},
    {t:'整座塔在哼歌',d:'大笑爆发',fn:()=>frog.laughBoost=true}
  ];
  pickCards=[];for(let i=0;i<3;i++)pickCards.push(pool[Math.floor(Math.random()*pool.length)]);
}

function nextCh(){
  chapter++;
  if(chapter>7){state='win';Music.setMode('menu');return}
  frog.hp=frog.max;enemies=[];waves=0;spawnWave();
  Music.setMode(chapter>=6?'boss':'fight');
  document.getElementById('clabel').textContent='第'+chapter+'章 '+CHS[chapter-1];
}

let last=0;
function update(dt){
  if(state!=='play')return;
  let sp=3.4;
  if(keys['a']){frog.vx=-sp;frog.face=-1}
  else if(keys['d']){frog.vx=sp;frog.face=1}
  else frog.vx*=0.82;
  if(keys['w'])frog.vy=-sp;
  else if(keys['s'])frog.vy=sp;
  else frog.vy*=0.82;
  frog.x+=frog.vx;frog.y+=frog.vy;
  frog.x=Math.max(28,Math.min(W-28,frog.x));
  frog.y=Math.max(60,Math.min(H-36,frog.y));
  frog.def=!!keys[' '];
  frog.cd-=dt;
  if((keys['f']||skillTap)&&frog.cd<=0)fireShot();
  if(keys['e']){frog.laugh=22;Music.laugh();if(frog.laughBoost)for(const e of enemies)if(Math.abs(e.x-frog.x)<180)e.hp-=45}
  if(frog.laugh>0)frog.laugh--;
  for(const s of frog.shots)s.x+=s.vx;
  frog.shots=frog.shots.filter(s=>s.x>-20&&s.x<W+20);
  for(const e of enemies){
    e.x+=e.vx;
    for(const s of frog.shots){if(Math.hypot(s.x-e.x,s.y-e.y)<e.r+s.r){e.hp-=14;s.dead=true;Music.hit()}}
    if(frog.shield&&Math.hypot(e.x-frog.x,e.y-frog.y)<55){e.vx=Math.abs(e.vx)+1.5;e.x+=8}
    if(Math.hypot(e.x-frog.x,e.y-frog.y)<e.r+20)dmgFrog(0.5);
  }
  frog.shots=frog.shots.filter(s=>!s.dead);
  enemies=enemies.filter(e=>e.hp>0);
  if(enemies.length===0){if(waves>=2+chapter)openPick();else spawnWave()}
}

function draw(){
  x.fillStyle='#0e1832';x.fillRect(0,0,W,H);
  // 地面
  x.fillStyle='#1a2a4a';x.fillRect(0,H-30,W,30);
  // 月亮
  x.fillStyle='rgba(255,255,255,.08)';x.beginPath();x.arc(W-100,80,38,0,7);x.fill();

  if(state==='cover')return;

  // 敌人
  for(const e of enemies){
    x.fillStyle=e.typ==='jar'?'#caa6ff':'#ff9bb0';
    x.beginPath();x.arc(e.x,e.y,e.r,0,7);x.fill();
    x.fillStyle='#1a0a1a';x.fillRect(e.x-5,e.y-3,2,2);x.fillRect(e.x+3,e.y-3,2,2);
  }

  // 奶蛙
  x.save();x.translate(frog.x,frog.y);
  x.fillStyle='#7fe3a0';x.beginPath();x.ellipse(0,0,22,18,0,0,7);x.fill();
  x.fillStyle='#eafff2';x.beginPath();x.ellipse(0,6,14,10,0,0,7);x.fill();
  x.fillStyle='#06281a';x.beginPath();x.arc(frog.face*8,-6,3.5,0,7);x.fill();
  if(frog.def){x.strokeStyle='rgba(255,255,255,.5)';x.lineWidth=2.5;x.beginPath();x.arc(0,0,30,0,7);x.stroke()}
  if(frog.laugh>0){x.fillStyle='rgba(255,240,180,.4)';x.beginPath();x.arc(0,0,52,0,7);x.fill()}
  x.restore();

  // 奶嗝
  x.fillStyle='#fff4c2';
  for(const s of frog.shots){x.beginPath();x.arc(s.x,s.y,s.r,0,7);x.fill()}

  // HUD
  x.fillStyle='rgba(0,0,0,.4)';x.fillRect(0,0,W,36);
  x.fillStyle='#eaf6ff';x.font='13px sans-serif';
  x.fillText(CHS[chapter-1],12,22);
  x.fillText('HP '+Math.max(0,frog.hp|0)+'/'+frog.max,160,22);
  x.fillText('波 '+waves+'/'+(2+chapter),300,22);

  if(state==='pick'){
    x.fillStyle='rgba(0,0,0,.72)';x.fillRect(0,0,W,H);
    x.fillStyle='#fff';x.font='20px sans-serif';x.textAlign='center';
    x.fillText('选一种闹法,更不像童话',W/2,140);x.textAlign='left';
    pickCards.forEach((c,i)=>{
      const bx=W/2-340+i*230,by=220;
      x.fillStyle='rgba(255,255,255,.08)';x.strokeStyle='#7fe3a0';x.lineWidth=2;
      x.fillRect(bx,by,210,150);x.strokeRect(bx,by,210,150);
      x.fillStyle='#eaf6ff';x.font='16px sans-serif';x.fillText(c.t,bx+14,by+48);
      x.font='12px sans-serif';x.fillText(c.d,bx+14,by+82);
      x.fillStyle='#7fe3a0';x.fillText('点击选择',bx+14,by+120);
    });
  }
  if(state==='over'){
    x.fillStyle='rgba(0,0,0,.75)';x.fillRect(0,0,W,H);
    x.fillStyle='#fff';x.font='22px sans-serif';x.textAlign='center';
    x.fillText('只是肚皮磕了一下,没笑完不算输',W/2,H/2-12);
    x.font='14px sans-serif';x.fillText('点击重来',W/2,H/2+28);x.textAlign='left';
  }
  if(state==='win'){
    x.fillStyle='rgba(0,0,0,.75)';x.fillRect(0,0,W,H);
    x.fillStyle='#ffe';x.font='22px sans-serif';x.textAlign='center';
    x.fillText('🎉 豆公主醒了,整座城开始哼歌',W/2,H/2);x.textAlign='left';
  }
}

function loop(t){
  const dt=Math.min(.05,(t-last)/1000)||.016;last=t;
  update(dt);draw();
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

/* 点击 */
cv.addEventListener('click',e=>{
  if(state==='over'){frog.hp=frog.max;enemies=[];waves=0;spawnWave();state='play';Music.setMode(chapter>=6?'boss':'fight')}
  if(state==='pick'){
    const r=cv.getBoundingClientRect();
    const px=(e.clientX-r.left)/(r.width/W),py=(e.clientY-r.top)/(r.height/H);
    pickCards.forEach((c,i)=>{
      const bx=W/2-340+i*230,by=220;
      if(px>bx&&px<bx+210&&py>by&&py<by+150){c.fn();state='play';if(waves>=2+chapter)openPick();else spawnWave()}
    });
  }
});

document.getElementById('startBtn').onclick=()=>{
  document.getElementById('cover').style.display='none';
  document.getElementById('topbar').style.display='flex';
  Music.start();Music.setMode('fight');
  state='play';frog.hp=frog.max;enemies=[];waves=0;spawnWave();
};
document.getElementById('musicBtn').onclick=function(){this.textContent=Music.toggle()?'🎵':'🔇'};
</script>
</body>
</html>

Game Source: 奶蛙夜行

Creator: ApexPhoenix48

Libraries: none

Complexity: complex (284 lines, 11.4 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-apexphoenix48" to link back to the original. Then publish at arcadelab.ai/publish.