🎮ArcadeLab

表情包吞噬

by QuantumPenguin79
362 lines16.7 KB
▶ Play
<!DOCTYPE html>
 <html lang="zh-CN">
 <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>
 *{margin:0;padding:0;box-sizing:border-box;font-family:system-ui,sans-serif}
 html,body{width:100vw;height:100vh;overflow:hidden;background:#4477dd;color:#fff}
 #app{width:100%;height:100%;position:relative}
 /* 竖屏强制显示旋转提示,横屏隐藏 */
 #rotateWarn{
   position:absolute;top:0;left:0;width:100%;height:100%;
   background:#111;z-index:999;
   display:flex;flex-direction:column;align-items:center;justify-content:center;
   font-size:28px;text-align:center;padding:20px;
 }
 @media screen and (orientation:landscape){
   #rotateWarn{display:none;}
 }
 #menuScreen{position:absolute;top:0;left:0;width:100%;height:100%;background:#4477dd;z-index:50;}
 .menu-btn{
   position:absolute;left:50px;width:170px;padding:14px 0;
   background:#2c4eb8;border:none;border-radius:10px;color:#fff;
   font-size:21px;text-align:center;cursor:pointer;
 }
 #bShop{top:130px}
 #bSuite{top:200px}
 #bStart{top:270px}
 #bAch{top:340px}
 #rolePreview{
   position:absolute;right:180px;top:260px;
   font-size:140px;pointer-events:none;
 }
 #ptsLabel{position:absolute;top:20px;right:20px;font-size:22px;color:#ffdd44}
 #roundLabel{position:absolute;top:52px;right:20px;font-size:17px;color:#ffdd44}
 #gameContainer{position:absolute;top:0;left:0;width:100%;height:100%;background:#222;display:none;z-index:40;}
 #canvas{width:100%;height:100%;display:block;background:#fff;}
 .tipText{position:absolute;top:12px;left:0;width:100%;text-align:center;font-size:22px;color:#000;pointer-events:none;z-index:21;}
 #joystickBase{
   position:absolute;left:20px;bottom:20px;width:120px;height:120px;
   border-radius:50%;background:rgba(60,60,60,0.45);
   border:2px solid rgba(255,255,255,0.55);touch-action:none;z-index:24;
 }
 #joystickKnob{
   position:absolute;left:50%;top:50%;width:44px;height:44px;margin:-22px;
   border-radius:50%;background:rgba(30,120,240,0.8);
   border:2px solid rgba(255,255,255,0.7);pointer-events:none;
   transform:translate(-50%,-50%);
 }
 .popEnd{
   position:absolute;top:0;left:0;width:100%;height:100%;
   background:rgba(0,0,0,0.8);display:none;flex-direction:column;
   align-items:center;justify-content:center;z-index:60;font-size:26px;padding:20px;
 }
 .popEnd button{
   margin-top:20px;padding:10px 22px;font-size:18px;border:none;
   border-radius:8px;background:#448aff;color:#fff;cursor:pointer;
 }
 .subPage{
   position:absolute;top:0;left:0;width:100%;height:100%;
   background:#4477dd;display:none;z-index:55;padding:20px;overflow-y:auto;
 }
 .backBtn{
   position:absolute;top:10px;left:10px;padding:8px 16px;
   background:#2c4eb8;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:16px;
 }
 .shopTabWrap{display:flex;justify-content:center;gap:10px;margin-bottom:20px}
 .shopTab{padding:10px 20px;background:#2c4eb8;border:none;color:#fff;border-radius:6px;cursor:pointer}
 .shopTab.active{background:#1c3a96}
 .shopGrid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
 .itemBox{background:#2c4eb8;padding:10px;border-radius:8px;text-align:center}
 .itemName{font-size:16px;margin-bottom:4px}
 .itemPrice{color:#ffdd44;font-size:14px}
 .buyBtn,.equipBtn{margin-top:6px;padding:4px 10px;border:none;color:#fff;border-radius:4px;cursor:pointer}
 .buyBtn{background:#22aa44}
 .equipBtn{background:#2196F3}
 .achBox{background:#2c4eb8;padding:14px;margin:10px;border-radius:8px;text-align:center;font-size:17px}
 .achDone{color:#7cfc00}
 .achLock{color:#cccccc}
 </style>
 </head>
 <body>
 <div id="app">
   <div id="rotateWarn">请把手机横过来<br>横着拿才能玩游戏</div>
   <div id="menuScreen">
     <button class="menu-btn" onclick="openShop()">商店</button>
     <button class="menu-btn" onclick="openSuite()">套装</button>
     <button class="menu-btn" onclick="startGame()">开始游戏</button>
     <button class="menu-btn" onclick="openAchievement()">成就</button>
     <div id="rolePreview">😀</div>
     <div id="ptsLabel">积分:0</div>
     <div id="roundLabel"></div>
   </div>
   <div class="subPage" id="shopPage">
     <button class="backBtn" onclick="closeShop()">返回</button>
     <div class="shopTabWrap">
       <button class="shopTab active">皮肤</button>
       <button class="shopTab">装饰</button>
     </div>
     <div class="shopGrid" id="shopContent"></div>
   </div>
   <div class="subPage" id="suitePage">
     <button class="backBtn" onclick="closeSuite()">返回</button>
     <div class="achBox"><div>圣诞节套装</div><div>10000积分</div><button class="buyBtn">购买并穿戴</button></div>
     <div class="achBox"><div>武士套装</div><div>10000积分</div><button class="buyBtn">购买并穿戴</button></div>
     <div class="achBox"><div>魔法套装</div><div>10000积分</div><button class="buyBtn">购买并穿戴</button></div>
   </div>
   <div class="subPage" id="achPage">
     <button class="backBtn" onclick="closeAchievement()">返回</button>
     <div class="achBox">
       <div>🏅生存大师</div>
       <div>单局完整坚持20分钟</div>
       <div id="ach1Txt">未解锁|奖励:500积分</div>
     </div>
     <div class="achBox">
       <div>💀厄运挑战者</div>
       <div>连续失败100局,通关即重置</div>
       <div id="ach2Txt">未解锁|奖励:10000积分</div>
     </div>
     <div class="achBox">
       <div>🍪吞噬达人</div>
       <div>连续对局累计吞噬100个表情包,失败即清零</div>
       <div id="ach3Txt">未解锁|奖励:20000积分</div>
     </div>
     <div style="margin-top:14px;font-size:16px">
       <div>连续失败局数:<span id="failStreakTxt">0</span></div>
       <div>连续吞噬表情包数:<span id="eatStreakTxt">0</span></div>
     </div>
   </div>
   <div id="gameContainer">
     <canvas id="canvas"></canvas>
     <div class="tipText" id="tipText"></div>
     <div id="joystickBase"><div id="joystickKnob"></div></div>
     <div class="popEnd" id="popWin"><div>🎉恭喜通关!升到最高等级Lv10!</div><div id="winPt"></div><button onclick="backToMenu()">返回主界面</button></div>
     <div class="popEnd" id="popEdge"><div>💥跑出地图红线范围!游戏结束</div><div id="edgePt"></div><button onclick="backToMenu()">返回主界面</button></div>
     <div class="popEnd" id="popTimeUp"><div>⏱20分钟时间到!</div><div id="timePt"></div><button onclick="backToMenu()">返回主界面</button></div>
     <div class="popEnd" id="popDangerZone"><div>⚠️奖励区关闭时还在区内!出局</div><div id="dzPt"></div><button onclick="backToMenu()">返回主界面</button></div>
     <div class="popEnd" id="popHitBad"><div>☠️碰到❌危险表情包!游戏失败</div><div id="badPt"></div><button onclick="backToMenu()">返回主界面</button></div>
   </div>
 </div>
 <script>
 const canvas=document.getElementById("canvas");
 const ctx=canvas.getContext("2d");
 const tipDom=document.getElementById("tipText");
 const joyBase=document.getElementById("joystickBase");
 const joyKnob=document.getElementById("joystickKnob");
 const LVL_FACE=["😀","🙂","😊","😃","😄","😆","😎","🤩","😇","🥳"];
 const MAP_MM=3000;
 const PLAYER_SPEED=42;
 const AI_SPEED=32;
 const SAFE_R=220;
 const GAME_DUR=1200;
 const BONUS_OPEN_T=600;
 const BONUS_CLOSE_T=780;
 const FRUITS=["🍎","🍊","🍇","🍓","🍌"];
 let PX_PER_MM=1;
 let save=JSON.parse(localStorage.getItem("emoSave")||JSON.stringify({
   pts:0,skin:0,ach:{survive:false,streakFail:false,streakEat:false},
   failStreak:0,eatStreak:0
 }));
 function saveWrite(){localStorage.setItem("emoSave",JSON.stringify(save));refreshUI();}
 function refreshUI(){
   document.getElementById("ptsLabel").innerText="积分:"+save.pts;
   document.getElementById("failStreakTxt").innerText=save.failStreak;
   document.getElementById("eatStreakTxt").innerText=save.eatStreak;
   document.getElementById("ach1Txt").className=save.ach.survive?"achDone":"achLock";
   document.getElementById("ach1Txt").innerText=save.ach.survive?"✅已解锁|奖励:500积分":"未解锁|奖励:500积分";
   document.getElementById("ach2Txt").className=save.ach.streakFail?"achDone":"achLock";
   document.getElementById("ach2Txt").innerText=save.ach.streakFail?"✅已解锁|奖励:10000积分":"未解锁|奖励:10000积分";
   document.getElementById("ach3Txt").className=save.ach.streakEat?"achDone":"achLock";
   document.getElementById("ach3Txt").innerText=save.ach.streakEat?"✅已解锁|奖励:20000积分":"未解锁|奖励:20000积分";
   document.getElementById("rolePreview").innerText=LVL_FACE[save.skin];
 }
 let gameRun=false;
 let player,emolist,fruitlist,followers;
 let timeLeft,tipEnd,bonusOpen,bonusClosed,roundPt,lastT;
 let joyDown=false,jx=0,jy=0;
 function resetJoystick(){jx=0;jy=0;joyKnob.style.transform="translate(-50%,-50%)";}
 joyBase.addEventListener("touchstart",e=>{joyDown=true;joyMove(e.touches[0]);e.preventDefault()});
 joyBase.addEventListener("touchmove",e=>{joyMove(e.touches[0]);e.preventDefault()});
 joyBase.addEventListener("touchend",()=>{joyDown=false;resetJoystick()});
 joyBase.addEventListener("touchcancel",()=>{joyDown=false;resetJoystick()});
 function joyMove(t){
   const r=60;
   const rect=joyBase.getBoundingClientRect();
   let dx=t.clientX-(rect.left+rect.width/2);
   let dy=t.clientY-(rect.top+rect.height/2);
   const d=Math.hypot(dx,dy);
   if(d>r){dx=dx/d*r;dy=dy/d*r;}
   jx=dx/r;jy=dy/r;
   joyKnob.style.transform=`translate(calc(-50% + ${dx}px),calc(-50% + ${dy}px))`;
 }
 function showTip(s){tipDom.innerText=s;tipEnd=Date.now()+4500;}
 function clearTip(){tipDom.innerText="";}
 function resize(){
   const gc=document.getElementById("gameContainer");
   canvas.width=gc.clientWidth;canvas.height=gc.clientHeight;
   PX_PER_MM=Math.min(canvas.width,canvas.height)/MAP_MM;
 }
 window.addEventListener("resize",resize);
 function backToMenu(isWin=false){
   gameRun=false;clearTip();resetJoystick();
   document.querySelectorAll(".popEnd").forEach(p=>p.style.display="none");
   document.getElementById("gameContainer").style.display="none";
   document.getElementById("menuScreen").style.display="block";
   document.getElementById("roundLabel").innerText=roundPt>0?"本局获得:"+roundPt+"积分":"本局无积分";
   if(!isWin){save.failStreak++;save.eatStreak=0;}
   else save.failStreak=0;
   save.pts+=roundPt;saveWrite();
 }
 function startGame(){
   document.getElementById("menuScreen").style.display="none";
   document.getElementById("gameContainer").style.display="block";
   resize();gameRun=true;roundPt=0;
   timeLeft=GAME_DUR;bonusOpen=false;bonusClosed=false;
   player={x:MAP_MM/2,y:MAP_MM/2,lv:0};
   emolist=[];fruitlist=[];followers=[];
   lastT=performance.now();
   requestAnimationFrame(gameLoop);
 }
 function inPlayerView(x,y){
   const camW=canvas.width/PX_PER_MM,camH=canvas.height/PX_PER_MM;
   return Math.abs(x-player.x)<camW*0.42&&Math.abs(y-player.y)<camH*0.42;
 }
 function spawnEmote(){
   let x,y,ok=false,tryN=0;
   while(!ok&&tryN<220){
     x=Math.random()*MAP_MM;y=Math.random()*MAP_MM;
     const dc=Math.hypot(x-MAP_MM/2,y-MAP_MM/2);
     if(dc<SAFE_R){tryN++;continue;}
     if(inPlayerView(x,y)){tryN++;continue;}
     ok=true;
   }
   if(!ok)return;
   const badCross=Math.random()<0.11;
   const lv=Math.floor(Math.random()*LVL_FACE.length);
   emolist.push({x,y,lv,badCross,dir:{x:(Math.random()-0.5)*1.2,y:(Math.random()-0.5)*1.2}});
 }
 function spawnFruit(){
   if(!bonusOpen||bonusClosed)return;
   let x=MAP_MM+180+Math.random()*750;
   let y=MAP_MM/2-380+Math.random()*760;
   fruitlist.push({x,y,type:FRUITS[Math.floor(Math.random()*FRUITS.length)]});
 }
 function checkMerge(){
   const same=followers.filter(f=>f.lv===player.lv);
   if(same.length>=3){
     let cnt=0;
     for(let i=followers.length-1;i>=0&&cnt<3;i--){
       if(followers[i].lv===player.lv){followers.splice(i,1);cnt++;}
     }
     player.lv++;
     showTip("合成成功!升级到Lv"+(player.lv+1));
     if(player.lv>=LVL_FACE.length){
       document.getElementById("winPt").innerText="本局积分:"+roundPt;
       document.getElementById("popWin").style.display="flex";
       gameRun=false;backToMenu(true);
     }
   }
 }
 function gameLoop(t){
   if(!gameRun)return;
   const dt=Math.min((t-lastT)/1000,0.05);lastT=t;timeLeft-=dt;
   if(timeLeft<=BONUS_OPEN_T&&!bonusOpen){bonusOpen=true;showTip("奖励区开启!✅可以收表情,❌千万别碰");}
   if(timeLeft<=BONUS_CLOSE_T&&!bonusClosed){bonusClosed=true;showTip("奖励区关闭!请尽快离开");}
   if(joyDown){
     player.x+=jx*PLAYER_SPEED*dt;
     player.y+=jy*PLAYER_SPEED*dt;
   }
   if(player.x<0||player.x>MAP_MM||player.y<0||player.y>MAP_MM){
     document.getElementById("edgePt").innerText="本局积分:"+roundPt;
     document.getElementById("popEdge").style.display="flex";
     gameRun=false;return;
   }
   for(let e of emolist){
     e.x+=e.dir.x*AI_SPEED*dt;e.y+=e.dir.y*AI_SPEED*dt;
     if(e.x<0||e.x>MAP_MM)e.dir.x*=-1;
     if(e.y<0||e.y>MAP_MM)e.dir.y*=-1;
   }
   for(let i=emolist.length-1;i>=0;i--){
     const e=emolist[i];
     const d=Math.hypot(e.x-player.x,e.y-player.y);
     if(d<52){
       if(e.badCross){
         document.getElementById("badPt").innerText="本局积分:"+roundPt;
         document.getElementById("popHitBad").style.display="flex";
         gameRun=false;return;
       }
       save.eatStreak++;roundPt+=10;
       emolist.splice(i,1);
       followers.push({
         x:player.x+(Math.random()-0.5)*140,
         y:player.y+(Math.random()-0.5)*140,
         lv:e.lv,
         angle:Math.random()*Math.PI*2
       });
       checkMerge();
     }
   }
   for(let f of followers){
     f.angle+=0.6*dt;
     const orbit=70+followers.indexOf(f)*22;
     f.x=player.x+Math.cos(f.angle)*orbit;
     f.y=player.y+Math.sin(f.angle)*orbit;
   }
   for(let i=fruitlist.length-1;i>=0;i--){
     const f=fruitlist[i];
     if(Math.hypot(f.x-player.x,f.y-player.y)<50){roundPt+=1;fruitlist.splice(i,1);}
   }
   if(emolist.length<14&&Math.random()<0.028)spawnEmote();
   if(fruitlist.length<7&&Math.random()<0.016)spawnFruit();
   if(timeLeft<=0){
     document.getElementById("timePt").innerText="本局积分:"+roundPt;
     document.getElementById("popTimeUp").style.display="flex";
     gameRun=false;
     if(!save.ach.survive){save.ach.survive=true;save.pts+=500;}
     backToMenu(true);return;
   }
   if(bonusClosed&&player.x>MAP_MM){
     document.getElementById("dzPt").innerText="本局积分:"+roundPt;
     document.getElementById("popDangerZone").style.display="flex";
     gameRun=false;return;
   }
   ctx.clearRect(0,0,canvas.width,canvas.height);
   const cx=canvas.width/2-player.x*PX_PER_MM;
   const cy=canvas.height/2-player.y*PX_PER_MM;
   ctx.strokeStyle="red";ctx.lineWidth=3;
   ctx.strokeRect(cx,cy,MAP_MM*PX_PER_MM,MAP_MM*PX_PER_MM);
   ctx.fillStyle="#aa2222";
   ctx.fillRect(cx+MAP_MM*PX_PER_MM,cy,90*PX_PER_MM,780*PX_PER_MM);
   ctx.fillRect(cx+MAP_MM*PX_PER_MM+90*PX_PER_MM,cy+MAP_MM/2*PX_PER_MM-390*PX_PER_MM,760*PX_PER_MM,780*PX_PER_MM);
   ctx.strokeStyle="#dddddd";ctx.lineWidth=1;
   for(let i=0;i<=MAP_MM;i+=500){
     ctx.beginPath();ctx.moveTo(cx+i*PX_PER_MM,cy);ctx.lineTo(cx+i*PX_PER_MM,cy+MAP_MM*PX_PER_MM);ctx.stroke();
     ctx.beginPath();ctx.moveTo(cx,cy+i*PX_PER_MM);ctx.lineTo(cx+MAP_MM*PX_PER_MM,cy+i*PX_PER_MM);ctx.stroke();
   }
   ctx.font="24px sans‑serif";ctx.textAlign="center";ctx.textBaseline="middle";
   for(let e of emolist){
     ctx.fillText(e.badCross?"❌":LVL_FACE[e.lv],cx+e.x*PX_PER_MM,cy+e.y*PX_PER_MM);
   }
   for(let f of followers){
     ctx.fillText(LVL_FACE[f.lv],cx+f.x*PX_PER_MM,cy+f.y*PX_PER_MM);
   }
   for(let f of fruitlist){
     ctx.fillText(f.type,cx+f.x*PX_PER_MM,cy+f.y*PX_PER_MM);
   }
   ctx.font="34px sans‑serif";
   ctx.fillText(LVL_FACE[player.lv],cx+player.x*PX_PER_MM,cy+player.y*PX_PER_MM);
   if(Date.now()>tipEnd)clearTip();
   requestAnimationFrame(gameLoop);
 }
 function openShop(){document.getElementById("menuScreen").style.display="none";document.getElementById("shopPage").style.display="block";}
 function closeShop(){document.getElementById("shopPage").style.display="none";document.getElementById("menuScreen").style.display="block";}
 function openSuite(){document.getElementById("menuScreen").style.display="none";document.getElementById("suitePage").style.display="block";}
 function closeSuite(){document.getElementById("suitePage").style.display="none";document.getElementById("menuScreen").style.display="block";}
 function openAchievement(){document.getElementById("menuScreen").style.display="none";document.getElementById("achPage").style.display="block";}
 function closeAchievement(){document.getElementById("achPage").style.display="none";document.getElementById("menuScreen").style.display="block";}
 refreshUI();
 </script>
 </body>
 </html>

Game Source: 表情包吞噬

Creator: QuantumPenguin79

Libraries: none

Complexity: complex (362 lines, 16.7 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-quantumpenguin79" to link back to the original. Then publish at arcadelab.ai/publish.