mario.html 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. <!DOCTYPE html>
  2. <html lang="zh">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width,initial-scale=1">
  6. <title>致敬超级马里奥 · 小游戏</title>
  7. <style>
  8. html,body{margin:0;padding:0;background:#1a1a2e;font-family:"Microsoft YaHei",system-ui,sans-serif;}
  9. #wrap{max-width:1000px;margin:0 auto;padding:16px;}
  10. h1{color:#fff;text-align:center;font-size:22px;margin:8px 0 12px;letter-spacing:2px;}
  11. h1 span{color:#e52521;}
  12. #hud{display:flex;justify-content:space-between;align-items:center;background:#000;color:#fff;
  13. font-family:Consolas,monospace;font-size:15px;padding:8px 16px;border-radius:8px 8px 0 0;
  14. border:3px solid #333;border-bottom:none;}
  15. #hud b{color:#ffd800;}
  16. #stage{position:relative;}
  17. canvas{display:block;width:100%;background:#5c94fc;border:3px solid #333;border-radius:0 0 8px 8px;image-rendering:pixelated;}
  18. #overlay{position:absolute;inset:0;display:flex;flex-direction:column;justify-content:center;align-items:center;
  19. background:rgba(0,0,0,.55);color:#fff;text-align:center;border-radius:0 0 8px 8px;}
  20. #overlay h2{font-size:34px;margin:0 0 10px;color:#ffd800;text-shadow:3px 3px 0 #e52521;}
  21. #overlay p{font-size:15px;line-height:1.9;margin:4px 0;}
  22. #overlay button{margin-top:16px;padding:10px 34px;font-size:17px;font-weight:700;color:#fff;background:#e52521;
  23. border:none;border-radius:8px;cursor:pointer;box-shadow:0 4px 0 #8c1410;}
  24. #overlay button:active{transform:translateY(3px);box-shadow:0 1px 0 #8c1410;}
  25. .tips{color:#9aa4c9;text-align:center;font-size:13px;margin-top:10px;line-height:1.8;}
  26. kbd{background:#333;color:#ffd800;border-radius:4px;padding:1px 7px;font-family:Consolas,monospace;}
  27. </style>
  28. </head>
  29. <body>
  30. <div id="wrap">
  31. <h1>🍄 致敬<span>超级马里奥</span> · MINI GAME</h1>
  32. <div id="hud">
  33. <div>SCORE <b id="hud-score">000000</b></div>
  34. <div>🪙 ×<b id="hud-coins">00</b></div>
  35. <div>WORLD <b>1-1</b></div>
  36. <div>TIME <b id="hud-time">300</b></div>
  37. <div>❤️ ×<b id="hud-lives">3</b></div>
  38. </div>
  39. <div id="stage">
  40. <canvas id="game" width="960" height="480"></canvas>
  41. <div id="overlay">
  42. <h2 id="ov-title">致敬超级马里奥</h2>
  43. <p id="ov-text">一个用 Canvas 手写的小小致敬作品<br>吃金币、顶问号砖、踩扁栗子怪、冲向旗杆!</p>
  44. <button id="ov-btn" onclick="startGame()">开始游戏</button>
  45. </div>
  46. </div>
  47. <div class="tips">
  48. <kbd>←</kbd><kbd>→</kbd> 或 <kbd>A</kbd><kbd>D</kbd> 移动 | <kbd>空格</kbd> / <kbd>↑</kbd> / <kbd>W</kbd> 跳跃 |
  49. 从上方踩敌人可消灭 | 掉坑 / 碰敌人会损失生命 | 冲到旗杆即通关
  50. </div>
  51. </div>
  52. <script>
  53. 'use strict';
  54. /* ================= 基础常量 ================= */
  55. var TILE=32, W=170, H=15, CW=960, CH=480;
  56. var cv=document.getElementById('game'), ctx=cv.getContext('2d');
  57. var SOLID={1:1,2:1,3:1,4:1,5:1,6:1,7:1};
  58. var keys={};
  59. addEventListener('keydown',function(e){
  60. if(['ArrowLeft','ArrowRight','ArrowUp','Space'].indexOf(e.code)>=0) e.preventDefault();
  61. keys[e.code]=true;
  62. if(state==='ready'&&e.code==='Enter') startGame();
  63. });
  64. addEventListener('keyup',function(e){ keys[e.code]=false; });
  65. /* ================= 音效(WebAudio 小蜂鸣) ================= */
  66. var AC=null;
  67. function beep(freq,dur,type,slide){
  68. try{
  69. if(!AC) AC=new (window.AudioContext||window.webkitAudioContext)();
  70. var o=AC.createOscillator(), g=AC.createGain();
  71. o.type=type||'square'; o.frequency.setValueAtTime(freq,AC.currentTime);
  72. if(slide) o.frequency.exponentialRampToValueAtTime(slide,AC.currentTime+dur);
  73. g.gain.setValueAtTime(.12,AC.currentTime);
  74. g.gain.exponentialRampToValueAtTime(.001,AC.currentTime+dur);
  75. o.connect(g); g.connect(AC.destination); o.start(); o.stop(AC.currentTime+dur);
  76. }catch(e){}
  77. }
  78. function sJump(){ beep(320,.18,'square',660); }
  79. function sCoin(){ beep(988,.08,'square'); setTimeout(function(){beep(1319,.22,'square');},70); }
  80. function sStomp(){ beep(300,.15,'sawtooth',80); }
  81. function sBump(){ beep(140,.1,'square',90); }
  82. function sDie(){ [494,466,440,392,330,262].forEach(function(f,i){ setTimeout(function(){beep(f,.14,'triangle');},i*110); }); }
  83. function sWin(){ [523,659,784,1047,784,1047].forEach(function(f,i){ setTimeout(function(){beep(f,.15,'square');},i*120); }); }
  84. /* ================= 关卡数据 ================= */
  85. var grid, coins, enemies, popups, flagX;
  86. function buildLevel(){
  87. grid=[]; for(var y=0;y<H;y++){ grid.push(new Array(W).fill(0)); }
  88. function ground(a,b){ for(var x=a;x<=b;x++){ grid[13][x]=1; grid[14][x]=1; } }
  89. ground(0,69); ground(73,119); ground(123,169);
  90. function pipe(x,h){ for(var i=0;i<h;i++){ grid[12-i][x]=5; grid[12-i][x+1]=5; } }
  91. pipe(18,2); pipe(36,3); pipe(54,4);
  92. function set(x,y,v){ grid[y][x]=v; }
  93. set(12,9,3);
  94. set(15,9,2); set(16,9,3); set(17,9,2); set(18,9,3); set(19,9,2); // 注意:18,9 在管道上方,可站可顶
  95. set(17,5,3);
  96. set(40,9,2); set(41,9,2); set(42,9,3); set(43,9,2); set(44,9,2);
  97. set(41,6,2); set(42,6,3); set(43,6,2);
  98. set(62,9,3); set(64,9,3); set(66,9,3);
  99. set(80,9,2); set(81,9,2); set(82,9,3); set(83,9,2); set(84,9,2);
  100. set(95,8,2); set(96,8,2); set(97,8,2); set(98,8,2);
  101. set(108,9,3); set(110,9,3);
  102. // 终点前阶梯
  103. for(var i=0;i<4;i++){ for(var j=0;j<=i;j++){ grid[12-j][140+i]=6; } }
  104. for(var i2=0;i2<4;i2++){ for(var j2=0;j2<3-i2;j2++){ grid[12-j2][145+i2]=6; } }
  105. set(152,12,6);
  106. flagX=152;
  107. // 可踩木箱(辅助跳上高台/高砖)
  108. function crate(x,y){ grid[y][x]=7; }
  109. crate(8,12); // 起点热身箱
  110. crate(14,12); // 辅助跳上 15-19 砖排
  111. crate(22,12); crate(23,12); // 管道后垫脚箱
  112. crate(39,12); crate(39,11); crate(39,10); crate(39,9); // 高塔箱:跳上 41-43 高台
  113. crate(52,12); crate(52,11); crate(52,10); // 三层箱:爬上高水管
  114. crate(60,12); // 辅助顶 62/64/66 问号
  115. crate(69,12); // 坑前垫脚箱
  116. crate(93,12); crate(93,11); // 双层箱:跳上 95-98 砖排
  117. crate(106,12); // 辅助顶 108/110 问号
  118. crate(119,12); // 坑前垫脚箱
  119. crate(138,12); // 阶梯前垫脚箱
  120. // 金币(空中)
  121. coins=[
  122. {x:24,y:8},{x:25,y:8},{x:26,y:8},
  123. {x:41,y:4},{x:42,y:4},{x:43,y:4},
  124. {x:47,y:10},{x:48,y:10},{x:49,y:10},
  125. {x:70,y:9},{x:71,y:8},{x:72,y:9},
  126. {x:95,y:6},{x:96,y:6},{x:97,y:6},{x:98,y:6},
  127. {x:112,y:10},{x:113,y:10},{x:114,y:10},
  128. {x:126,y:10},{x:127,y:10},{x:128,y:10}
  129. ].map(function(c){ c.taken=false; return c; });
  130. // 敌人
  131. enemies=[22,28,41,47,60,66,76,82,90,100,110,126,132].map(function(x){
  132. return {x:x*TILE+4,y:11*TILE,w:24,h:26,vx:-1,vy:0,dead:false,deadT:0,anim:0};
  133. });
  134. popups=[];
  135. }
  136. /* ================= 状态 ================= */
  137. var state='ready'; // ready | play | dead | win | over
  138. var score=0, coinCount=0, lives=3, timeLeft=300, camX=0, stateT=0;
  139. var player={x:64,y:11*TILE,w:22,h:28,vx:0,vy:0,onGround:false,face:1,anim:0};
  140. function resetPlayer(){ player.x=64; player.y=11*TILE; player.vx=0; player.vy=0; player.face=1; camX=0; }
  141. function startGame(){
  142. score=0; coinCount=0; lives=3; timeLeft=300;
  143. buildLevel(); resetPlayer();
  144. state='play';
  145. document.getElementById('overlay').style.display='none';
  146. }
  147. function showOverlay(title,text,btn){
  148. var ov=document.getElementById('overlay');
  149. ov.style.display='flex';
  150. document.getElementById('ov-title').textContent=title;
  151. document.getElementById('ov-text').innerHTML=text;
  152. document.getElementById('ov-btn').textContent=btn;
  153. }
  154. /* ================= 碰撞工具 ================= */
  155. function tileAt(px,py){
  156. var tx=Math.floor(px/TILE), ty=Math.floor(py/TILE);
  157. if(tx<0||tx>=W) return 1;
  158. if(ty<0||ty>=H) return 0;
  159. return grid[ty][tx];
  160. }
  161. function solidPx(px,py){ return !!SOLID[tileAt(px,py)]; }
  162. function hitBlock(tx,ty){
  163. var v=grid[ty][tx];
  164. if(v===3){
  165. grid[ty][tx]=4;
  166. coinCount++; score+=200;
  167. popups.push({x:tx*TILE+TILE/2,y:ty*TILE,vy:-6,t:0,kind:'coin'});
  168. sCoin();
  169. }else if(v===2){
  170. score+=10; sBump();
  171. }
  172. }
  173. /* ================= 更新逻辑 ================= */
  174. function update(dt){
  175. if(state==='dead'||state==='win'||state==='over'){
  176. stateT-=dt;
  177. if(state==='dead'&&stateT<=0){
  178. if(lives<=0){ state='over'; sDie(); showOverlay('GAME OVER','最终得分 <b style="color:#ffd800">'+score+'</b><br>再接再厉,致敬永不放弃的马里奥精神!','重新开始'); }
  179. else { buildLevel(); resetPlayer(); state='play'; }
  180. }
  181. if(state==='win'&&stateT<=0){ showOverlay('🚩 通关!','恭喜冲上旗杆!最终得分 <b style="color:#ffd800">'+score+'</b><br>金币 ×'+coinCount+' | 剩余生命 ×'+lives,'再玩一次'); state='over'; }
  182. return;
  183. }
  184. timeLeft-=dt;
  185. if(timeLeft<=0){ die(); return; }
  186. var p=player;
  187. // 输入
  188. var acc=.4, maxV=4.2;
  189. if(keys.ArrowLeft||keys.KeyA){ p.vx=Math.max(p.vx-acc,-maxV); p.face=-1; }
  190. else if(keys.ArrowRight||keys.KeyD){ p.vx=Math.min(p.vx+acc,maxV); p.face=1; }
  191. else { p.vx*= .82; if(Math.abs(p.vx)<.1) p.vx=0; }
  192. if((keys.Space||keys.ArrowUp||keys.KeyW)&&p.onGround){ p.vy=-11.5; p.onGround=false; sJump(); }
  193. p.vy=Math.min(p.vy+.55,14);
  194. // 水平
  195. p.x+=p.vx;
  196. if(p.x<0) p.x=0;
  197. if(p.vx>0){
  198. if(solidPx(p.x+p.w,p.y+3)||solidPx(p.x+p.w,p.y+p.h-3)){
  199. p.x=Math.floor((p.x+p.w)/TILE)*TILE-p.w-.01; p.vx=0;
  200. }
  201. }else if(p.vx<0){
  202. if(solidPx(p.x,p.y+3)||solidPx(p.x,p.y+p.h-3)){
  203. p.x=(Math.floor(p.x/TILE)+1)*TILE+.01; p.vx=0;
  204. }
  205. }
  206. // 垂直
  207. p.y+=p.vy; p.onGround=false;
  208. if(p.vy>0){
  209. if(solidPx(p.x+2,p.y+p.h)||solidPx(p.x+p.w-2,p.y+p.h)){
  210. p.y=Math.floor((p.y+p.h)/TILE)*TILE-p.h-.01; p.vy=0; p.onGround=true;
  211. }
  212. }else if(p.vy<0){
  213. if(solidPx(p.x+2,p.y)||solidPx(p.x+p.w-2,p.y)){
  214. var ty=Math.floor(p.y/TILE);
  215. var tx=Math.floor((p.x+p.w/2)/TILE);
  216. p.y=(ty+1)*TILE+.01; p.vy=0;
  217. hitBlock(tx,ty);
  218. }
  219. }
  220. if(Math.abs(p.vx)>.2) p.anim+=Math.abs(p.vx)*.06;
  221. // 掉坑
  222. if(p.y>H*TILE+60){ die(); return; }
  223. // 金币
  224. coins.forEach(function(c){
  225. if(c.taken) return;
  226. var cx=c.x*TILE+TILE/2, cy=c.y*TILE+TILE/2;
  227. if(Math.abs(p.x+p.w/2-cx)<22 && Math.abs(p.y+p.h/2-cy)<24){
  228. c.taken=true; coinCount++; score+=100; sCoin();
  229. popups.push({x:cx,y:cy-8,vy:-4,t:0,kind:'spark'});
  230. }
  231. });
  232. // 敌人
  233. enemies.forEach(function(e){
  234. if(e.dead){ e.deadT+=dt; return; }
  235. e.anim+=.1;
  236. e.vy=Math.min(e.vy+.5,12);
  237. e.x+=e.vx;
  238. if(solidPx(e.x,e.y+e.h/2)||solidPx(e.x+e.w,e.y+e.h/2)){ e.vx*=-1; e.x+=e.vx*2; }
  239. e.y+=e.vy;
  240. if(solidPx(e.x+2,e.y+e.h)||solidPx(e.x+e.w-2,e.y+e.h)){
  241. e.y=Math.floor((e.y+e.h)/TILE)*TILE-e.h-.01; e.vy=0;
  242. }
  243. if(e.y>H*TILE+60){ e.dead=true; return; }
  244. // 与玩家碰撞
  245. if(p.x<e.x+e.w&&p.x+p.w>e.x&&p.y<e.y+e.h&&p.y+p.h>e.y){
  246. if(p.vy>0 && (p.y+p.h-e.y)<14){
  247. e.dead=true; score+=300; p.vy=-8; sStomp();
  248. popups.push({x:e.x+e.w/2,y:e.y,vy:-3,t:0,kind:'score'});
  249. }else{ die(); }
  250. }
  251. });
  252. // 旗杆
  253. if(p.x+p.w>=flagX*TILE+10){
  254. state='win'; stateT=1.6; score+=1000+Math.max(0,Math.floor(timeLeft))*5; sWin();
  255. }
  256. // 弹出动画
  257. popups.forEach(function(o){ o.t+=dt; o.y+=o.vy; o.vy+=.25; });
  258. popups=popups.filter(function(o){ return o.t<.7; });
  259. // 相机
  260. camX=Math.max(0,Math.min(p.x-320,W*TILE-CW));
  261. // HUD
  262. document.getElementById('hud-score').textContent=('000000'+score).slice(-6);
  263. document.getElementById('hud-coins').textContent=('00'+coinCount).slice(-2);
  264. document.getElementById('hud-time').textContent=Math.max(0,Math.ceil(timeLeft));
  265. document.getElementById('hud-lives').textContent=lives;
  266. }
  267. function die(){
  268. lives--; state='dead'; stateT=1.2; sDie();
  269. }
  270. /* ================= 绘制 ================= */
  271. function drawCloud(x,y,s){
  272. ctx.fillStyle='rgba(255,255,255,.9)';
  273. ctx.beginPath();
  274. ctx.arc(x,y,14*s,0,7); ctx.arc(x+18*s,y-8*s,16*s,0,7); ctx.arc(x+38*s,y,14*s,0,7);
  275. ctx.fill();
  276. }
  277. function draw(){
  278. // 天空
  279. var g=ctx.createLinearGradient(0,0,0,CH);
  280. g.addColorStop(0,'#5c94fc'); g.addColorStop(1,'#8ec9ff');
  281. ctx.fillStyle=g; ctx.fillRect(0,0,CW,CH);
  282. // 云(视差)
  283. for(var i=0;i<14;i++){
  284. var cx=((i*467)%(W*TILE))-camX*.5;
  285. if(cx>-120&&cx<CW+60) drawCloud(cx,60+(i*53)%120,1+(i%3)*.3);
  286. }
  287. // 远山
  288. ctx.fillStyle='#3fa65b';
  289. for(var h2=0;h2<10;h2++){
  290. var hx=h2*640-camX*.7;
  291. if(hx>-300&&hx<CW+100){
  292. ctx.beginPath(); ctx.moveTo(hx,CH-64); ctx.quadraticCurveTo(hx+140,CH-260,hx+280,CH-64); ctx.fill();
  293. }
  294. }
  295. ctx.save(); ctx.translate(-Math.floor(camX),0);
  296. // 砖块
  297. for(var ty=0;ty<H;ty++){
  298. for(var tx=Math.floor(camX/TILE)-1;tx<Math.floor((camX+CW)/TILE)+2;tx++){
  299. if(tx<0||tx>=W) continue;
  300. var v=grid[ty][tx]; if(!v) continue;
  301. var x=tx*TILE,y=ty*TILE;
  302. if(v===1){ // 地面
  303. ctx.fillStyle='#c8722a'; ctx.fillRect(x,y,TILE,TILE);
  304. ctx.fillStyle='#8a4a12'; ctx.fillRect(x,y+TILE-4,TILE,4); ctx.fillRect(x,y,2,TILE);
  305. ctx.fillStyle='#e8975a'; ctx.fillRect(x+3,y+3,TILE-6,3);
  306. }else if(v===2){ // 砖
  307. ctx.fillStyle='#b5502a'; ctx.fillRect(x,y,TILE,TILE);
  308. ctx.strokeStyle='#7a2f14'; ctx.lineWidth=2;
  309. ctx.strokeRect(x+1,y+1,TILE-2,TILE-2);
  310. ctx.beginPath(); ctx.moveTo(x,y+16); ctx.lineTo(x+TILE,y+16);
  311. ctx.moveTo(x+16,y); ctx.lineTo(x+16,y+16); ctx.moveTo(x+8,y+16); ctx.lineTo(x+8,y+TILE);
  312. ctx.moveTo(x+24,y+16); ctx.lineTo(x+24,y+TILE); ctx.stroke();
  313. }else if(v===3){ // 问号
  314. ctx.fillStyle='#ffb800'; ctx.fillRect(x,y,TILE,TILE);
  315. ctx.strokeStyle='#8a5a00'; ctx.lineWidth=2; ctx.strokeRect(x+1,y+1,TILE-2,TILE-2);
  316. ctx.fillStyle='#fff'; ctx.font='bold 18px Consolas'; ctx.textAlign='center';
  317. ctx.fillText('?',x+TILE/2,y+22);
  318. }else if(v===4){ // 已顶
  319. ctx.fillStyle='#9a6a30'; ctx.fillRect(x,y,TILE,TILE);
  320. ctx.strokeStyle='#5f3d16'; ctx.lineWidth=2; ctx.strokeRect(x+1,y+1,TILE-2,TILE-2);
  321. }else if(v===5){ // 管道
  322. var top = ty===0 || grid[ty-1][tx]!==5;
  323. ctx.fillStyle='#2ea043'; ctx.fillRect(x,y,TILE,TILE);
  324. ctx.fillStyle='#1b6e2c'; ctx.fillRect(x+TILE-5,y,5,TILE);
  325. ctx.fillStyle='#7ee294'; ctx.fillRect(x+3,y,5,TILE);
  326. if(top){ ctx.fillStyle='#2ea043'; ctx.fillRect(x-2,y,TILE+4,10);
  327. ctx.strokeStyle='#1b6e2c'; ctx.strokeRect(x-2,y,TILE+4,10); }
  328. }else if(v===6){ // 硬块
  329. ctx.fillStyle='#b8b8c8'; ctx.fillRect(x,y,TILE,TILE);
  330. ctx.strokeStyle='#6a6a7a'; ctx.lineWidth=2; ctx.strokeRect(x+1,y+1,TILE-2,TILE-2);
  331. ctx.fillStyle='#e8e8f2'; ctx.fillRect(x+3,y+3,TILE-6,3);
  332. }else if(v===7){ // 可踩木箱
  333. ctx.fillStyle='#d8a24a'; ctx.fillRect(x,y,TILE,TILE);
  334. ctx.strokeStyle='#8a5a20'; ctx.lineWidth=3; ctx.strokeRect(x+2,y+2,TILE-4,TILE-4);
  335. ctx.lineWidth=2;
  336. ctx.beginPath();
  337. ctx.moveTo(x+3,y+3); ctx.lineTo(x+TILE-3,y+TILE-3);
  338. ctx.moveTo(x+TILE-3,y+3); ctx.lineTo(x+3,y+TILE-3);
  339. ctx.stroke();
  340. ctx.fillStyle='#f2c987'; ctx.fillRect(x+4,y+4,TILE-8,3);
  341. }
  342. }
  343. }
  344. // 旗杆
  345. var fx=flagX*TILE+TILE/2;
  346. ctx.fillStyle='#8a8a9a'; ctx.fillRect(fx-2,3*TILE,4,9*TILE);
  347. ctx.fillStyle='#ffd800'; ctx.beginPath(); ctx.arc(fx,3*TILE,6,0,7); ctx.fill();
  348. ctx.fillStyle='#2ea043'; ctx.beginPath();
  349. ctx.moveTo(fx-2,3*TILE+4); ctx.lineTo(fx-30,3*TILE+16); ctx.lineTo(fx-2,3*TILE+28); ctx.fill();
  350. // 金币
  351. var t=Date.now()/200;
  352. coins.forEach(function(c){
  353. if(c.taken) return;
  354. var cx=c.x*TILE+TILE/2, cy=c.y*TILE+TILE/2;
  355. var w=Math.abs(Math.sin(t+c.x))*10+3;
  356. ctx.fillStyle='#ffd800'; ctx.strokeStyle='#b8860b'; ctx.lineWidth=2;
  357. ctx.beginPath(); ctx.ellipse(cx,cy,w,12,0,0,7); ctx.fill(); ctx.stroke();
  358. });
  359. // 敌人(栗子怪风格)
  360. enemies.forEach(function(e){
  361. if(e.dead&&e.deadT>.5) return;
  362. ctx.save();
  363. if(e.dead) ctx.globalAlpha=Math.max(0,1-e.deadT*2);
  364. var x=e.x,y=e.y;
  365. ctx.fillStyle='#8a4a12';
  366. ctx.beginPath(); ctx.ellipse(x+e.w/2,y+10,e.w/2,10,0,Math.PI,0); ctx.fill();
  367. ctx.fillRect(x,y+10,e.w,8);
  368. ctx.fillStyle='#3d2008';
  369. var step=Math.sin(e.anim*3)>0?2:0;
  370. ctx.fillRect(x+2,y+18,8,8-step); ctx.fillRect(x+e.w-10,y+18,8,8-(2-step));
  371. ctx.fillStyle='#fff';
  372. ctx.fillRect(x+5,y+8,5,6); ctx.fillRect(x+e.w-10,y+8,5,6);
  373. ctx.fillStyle='#000';
  374. ctx.fillRect(x+7,y+11,2,3); ctx.fillRect(x+e.w-8,y+11,2,3);
  375. ctx.restore();
  376. });
  377. // 玩家(红帽蓝背带裤小人)
  378. var p=player;
  379. ctx.save();
  380. ctx.translate(p.x+p.w/2,p.y);
  381. ctx.scale(p.face,1);
  382. ctx.translate(-p.w/2,0);
  383. var leg=Math.sin(p.anim*6)>0&&Math.abs(p.vx)>.2?2:0;
  384. ctx.fillStyle='#e52521'; ctx.fillRect(2,0,18,7); ctx.fillRect(14,2,10,4); // 帽子
  385. ctx.fillStyle='#ffd9b3'; ctx.fillRect(4,7,16,8); // 脸
  386. ctx.fillStyle='#000'; ctx.fillRect(14,9,3,4); // 眼
  387. ctx.fillStyle='#5a3218'; ctx.fillRect(4,13,4,3); // 胡子
  388. ctx.fillStyle='#2a56c6'; ctx.fillRect(3,15,18,9); // 背带裤
  389. ctx.fillStyle='#e52521'; ctx.fillRect(0,15,4,7); ctx.fillRect(20,15,4,7); // 手臂
  390. ctx.fillStyle='#5a3218'; ctx.fillRect(3,24,7,4-leg); ctx.fillRect(13,24,7,4-(2-leg)); // 鞋
  391. ctx.restore();
  392. // 弹出物
  393. popups.forEach(function(o){
  394. if(o.kind==='coin'){
  395. ctx.fillStyle='#ffd800'; ctx.strokeStyle='#b8860b';
  396. ctx.beginPath(); ctx.ellipse(o.x,o.y,7,10,0,0,7); ctx.fill(); ctx.stroke();
  397. }else if(o.kind==='score'){
  398. ctx.fillStyle='#fff'; ctx.font='bold 13px Consolas'; ctx.textAlign='center';
  399. ctx.fillText('+300',o.x,o.y);
  400. }else{
  401. ctx.fillStyle='rgba(255,255,255,'+(1-o.t/.7)+')';
  402. ctx.beginPath(); ctx.arc(o.x,o.y,6+o.t*20,0,7); ctx.fill();
  403. }
  404. });
  405. ctx.restore();
  406. }
  407. /* ================= 主循环 ================= */
  408. var last=performance.now();
  409. function loop(now){
  410. var dt=Math.min((now-last)/1000,.05); last=now;
  411. update(dt);
  412. draw();
  413. requestAnimationFrame(loop);
  414. }
  415. buildLevel();
  416. requestAnimationFrame(loop);
  417. </script>
  418. </body>
  419. </html>