index.html 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. <!doctype html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="utf-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
  6. <title>钢铁突击:异界战线</title>
  7. <style>
  8. html, body {
  9. margin: 0;
  10. width: 100%;
  11. height: 100%;
  12. overflow: hidden;
  13. background: #07101d;
  14. font-family: "Microsoft YaHei", Arial, sans-serif;
  15. }
  16. canvas {
  17. display: block;
  18. width: 100vw;
  19. height: 100vh;
  20. image-rendering: auto;
  21. touch-action: none;
  22. }
  23. </style>
  24. </head>
  25. <body>
  26. <canvas id="game"></canvas>
  27. <script>
  28. (() => {
  29. const canvas = document.getElementById('game');
  30. const ctx = canvas.getContext('2d');
  31. const W = 960;
  32. const H = 540;
  33. const GROUND = 375;
  34. const END_X = 3320;
  35. const assets = {};
  36. const sources = {
  37. ground: '../assets/resources/sprites/ground_strip.png',
  38. player: '../assets/resources/sprites/player.png',
  39. spore: '../assets/resources/sprites/enemy_spore.png',
  40. wing: '../assets/resources/sprites/enemy_wing.png',
  41. turret: '../assets/resources/sprites/enemy_turret.png',
  42. boss: '../assets/resources/sprites/imagegen/frames/boss_idle_pulse.png',
  43. crystal: '../assets/resources/sprites/alien_crystal.png',
  44. rifle: '../assets/resources/sprites/bullet_rifle.png',
  45. flame: '../assets/resources/sprites/bullet_flame.png',
  46. rail: '../assets/resources/sprites/bullet_rail.png',
  47. acid: '../assets/resources/sprites/bullet_acid.png',
  48. button: '../assets/resources/sprites/ui_button.png',
  49. outcomeFail: '../assets/resources/sprites/imagegen/ui_scene/ui/outcome_fail_panel.png',
  50. outcomeWin: '../assets/resources/sprites/imagegen/ui_scene/ui/outcome_win_panel.png',
  51. mobileLeft: '../assets/resources/sprites/imagegen/ui_scene/ui/mobile_left.png',
  52. mobileRight: '../assets/resources/sprites/imagegen/ui_scene/ui/mobile_right.png',
  53. mobileJump: '../assets/resources/sprites/imagegen/ui_scene/ui/mobile_jump.png',
  54. mobileFire: '../assets/resources/sprites/imagegen/ui_scene/ui/mobile_fire.png',
  55. mobileSwitch: '../assets/resources/sprites/imagegen/ui_scene/ui/mobile_switch.png'
  56. };
  57. const weapons = {
  58. rifle: { name: '突击步枪', cd: 0.105, dmg: 4, speed: 880, sprite: 'rifle', pierce: false, life: 0.85 },
  59. flame: { name: '火焰喷射器', cd: 0.055, dmg: 1.35, speed: 520, sprite: 'flame', pierce: false, life: 0.24 },
  60. rail: { name: '电磁炮', cd: 0.58, dmg: 18, speed: 1250, sprite: 'rail', pierce: true, life: 0.85 }
  61. };
  62. const state = {
  63. mode: 'playing',
  64. keys: new Set(),
  65. touchMove: 0,
  66. touchFire: false,
  67. touchJump: false,
  68. pointerDown: false,
  69. switchLock: false,
  70. weapon: 'rifle',
  71. score: 0,
  72. camera: 0,
  73. fireCd: 0,
  74. mercy: 0,
  75. player: { x: 120, y: GROUND - 65, vx: 0, vy: 0, hp: 6, face: 1, grounded: true },
  76. bullets: [],
  77. enemies: [],
  78. spawned: new Set(),
  79. boss: null,
  80. bossTime: 0,
  81. notice: ''
  82. };
  83. window.__steelDebug = { state, fitMetrics: () => fitMetrics() };
  84. const waves = [
  85. { x: 420, type: 'spore', count: 4 },
  86. { x: 860, type: 'wing', count: 3 },
  87. { x: 1280, type: 'turret', count: 2 },
  88. { x: 1680, type: 'spore', count: 5 },
  89. { x: 2200, type: 'wing', count: 4 },
  90. { x: 2700, type: 'spore', count: 3 }
  91. ];
  92. function resize() {
  93. const dpr = Math.min(devicePixelRatio || 1, 2);
  94. canvas.width = Math.floor(innerWidth * dpr);
  95. canvas.height = Math.floor(innerHeight * dpr);
  96. ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  97. }
  98. function fitMetrics() {
  99. const ratio = innerWidth / Math.max(1, innerHeight);
  100. const width = Math.max(W, H * ratio);
  101. const height = Math.max(H, W / Math.max(0.001, ratio));
  102. const left = (W - width) / 2;
  103. const top = (H - height) / 2;
  104. const scale = innerWidth / width;
  105. const ox = (innerWidth - width * scale) / 2 - left * scale;
  106. const oy = (innerHeight - height * scale) / 2 - top * scale;
  107. return { width, height, left, right: left + width, top, bottom: top + height, scale, ox, oy };
  108. }
  109. function fit() {
  110. const metrics = fitMetrics();
  111. ctx.setTransform(metrics.scale, 0, 0, metrics.scale, metrics.ox, metrics.oy);
  112. return metrics;
  113. }
  114. function load() {
  115. return Promise.all(Object.entries(sources).map(([key, src]) => new Promise((resolve) => {
  116. const img = new Image();
  117. img.onload = () => { assets[key] = img; resolve(); };
  118. img.onerror = () => { console.warn(`asset missing: ${src}`); resolve(); };
  119. img.src = src;
  120. })));
  121. }
  122. function reset() {
  123. state.mode = 'playing';
  124. state.weapon = 'rifle';
  125. state.score = 0;
  126. state.camera = 0;
  127. state.fireCd = 0;
  128. state.mercy = 0;
  129. state.touchMove = 0;
  130. state.touchFire = false;
  131. state.touchJump = false;
  132. state.pointerDown = false;
  133. state.switchLock = false;
  134. state.player = { x: 120, y: GROUND - 65, vx: 0, vy: 0, hp: 6, face: 1, grounded: true };
  135. state.bullets = [];
  136. state.enemies = [];
  137. state.spawned = new Set();
  138. state.boss = null;
  139. state.bossTime = 0;
  140. state.notice = '';
  141. }
  142. function spawnEnemy(type, x, i = 0) {
  143. const hp = type === 'turret' ? 24 : type === 'wing' ? 16 : 12;
  144. state.enemies.push({ type, x, y: type === 'wing' ? 245 - i * 18 : GROUND - 48, hp, maxHp: hp, vx: 0, cd: 0.4 + i * 0.2, burn: 0 });
  145. }
  146. function spawnBoss() {
  147. if (state.boss) return;
  148. const boss = { type: 'boss', x: 3290, y: GROUND - 122, hp: 180, maxHp: 180, vx: 0, cd: 0.5, burn: 0 };
  149. state.boss = boss;
  150. state.enemies.push(boss);
  151. state.mode = 'boss';
  152. }
  153. function shoot() {
  154. const w = weapons[state.weapon];
  155. if (state.fireCd > 0) return;
  156. state.fireCd = w.cd;
  157. const p = state.player;
  158. if (state.weapon === 'flame') {
  159. for (let i = 0; i < 2; i++) {
  160. state.bullets.push({ x: p.x + p.face * 42, y: p.y + 18, vx: p.face * w.speed, vy: i ? 70 : -30, dmg: w.dmg, life: w.life, player: true, pierce: false, r: 18, sprite: w.sprite });
  161. }
  162. return;
  163. }
  164. state.bullets.push({ x: p.x + p.face * 42, y: p.y + 18, vx: p.face * w.speed, vy: 0, dmg: w.dmg, life: w.life, player: true, pierce: w.pierce, r: w.pierce ? 16 : 10, sprite: w.sprite });
  165. }
  166. function tryJump() {
  167. if (state.mode === 'win' || state.mode === 'lose' || !state.player.grounded) return;
  168. state.player.vy = -620;
  169. state.player.grounded = false;
  170. }
  171. function enemyShot(x, y, dir, speed = 220) {
  172. state.bullets.push({ x, y, vx: dir * speed, vy: -20, dmg: 1, life: 3, player: false, pierce: false, r: 12, sprite: 'acid' });
  173. }
  174. function hit(a, b, ar, br) {
  175. const dx = a.x - b.x;
  176. const dy = a.y - b.y;
  177. return dx * dx + dy * dy < (ar + br) * (ar + br);
  178. }
  179. function damagePlayer() {
  180. if (state.mercy > 0) return;
  181. state.player.hp -= 1;
  182. state.mercy = 0.55;
  183. if (state.player.hp <= 0) {
  184. state.mode = 'lose';
  185. state.notice = 'MISSION FAILED 按 R 重试';
  186. }
  187. }
  188. function update(dt) {
  189. if (state.mode === 'win' || state.mode === 'lose') return;
  190. const p = state.player;
  191. const left = state.keys.has('a') || state.keys.has('arrowleft') || state.touchMove < 0;
  192. const right = state.keys.has('d') || state.keys.has('arrowright') || state.touchMove > 0;
  193. const jump = state.keys.has(' ') || state.keys.has('w') || state.keys.has('arrowup') || state.touchJump;
  194. const fire = state.keys.has('j') || state.keys.has('enter') || state.touchFire;
  195. p.vx = left ? -270 : right ? 270 : 0;
  196. if (p.vx) p.face = p.vx > 0 ? 1 : -1;
  197. if (jump) tryJump();
  198. p.vy += 1450 * dt;
  199. p.x = Math.max(20, Math.min(END_X - 180, p.x + p.vx * dt));
  200. p.y += p.vy * dt;
  201. if (p.y >= GROUND - 65) {
  202. p.y = GROUND - 65;
  203. p.vy = 0;
  204. p.grounded = true;
  205. }
  206. state.fireCd -= dt;
  207. state.mercy -= dt;
  208. if (fire) shoot();
  209. waves.forEach((wave, index) => {
  210. if (p.x > wave.x - 520 && !state.spawned.has(index)) {
  211. state.spawned.add(index);
  212. for (let i = 0; i < wave.count; i++) spawnEnemy(wave.type, wave.x + i * 58, i);
  213. }
  214. });
  215. if (p.x > 3090) spawnBoss();
  216. for (const e of state.enemies) {
  217. if (e.hp <= 0) continue;
  218. e.cd -= dt;
  219. e.burn = Math.max(0, e.burn - dt);
  220. if (e.burn > 0) e.hp -= 5 * dt;
  221. if (e.type === 'spore') e.vx = e.x > p.x ? -105 : 105;
  222. if (e.type === 'wing') {
  223. e.vx = e.x > p.x ? -82 : 82;
  224. e.y += Math.sin(performance.now() / 260 + e.x * 0.02) * 34 * dt;
  225. if (e.cd <= 0) {
  226. enemyShot(e.x, e.y, p.x > e.x ? 1 : -1);
  227. e.cd = 1.35;
  228. }
  229. }
  230. if (e.type === 'turret') {
  231. e.vx = 0;
  232. if (e.cd <= 0) {
  233. enemyShot(e.x - 20, e.y + 8, -1);
  234. e.cd = 1.05;
  235. }
  236. }
  237. if (e.type === 'boss') {
  238. state.bossTime += dt;
  239. e.vx = Math.sin(state.bossTime * 0.75) * 28;
  240. const phase = e.hp > 115 ? 1 : e.hp > 55 ? 2 : 3;
  241. if (e.cd <= 0) {
  242. const dir = p.x > e.x ? 1 : -1;
  243. enemyShot(e.x - 80, e.y + 58, dir, phase >= 3 ? 270 : 220);
  244. if (phase >= 2) enemyShot(e.x - 80, e.y - 14, dir, 180);
  245. if (phase >= 3 && state.enemies.filter(v => v.type === 'spore').length < 4) spawnEnemy('spore', e.x - 120);
  246. e.cd = phase === 1 ? 1.1 : phase === 2 ? 0.85 : 0.62;
  247. }
  248. }
  249. e.x += e.vx * dt;
  250. if (hit(p, e, 36, e.type === 'boss' ? 92 : 34)) damagePlayer();
  251. }
  252. for (const b of state.bullets) {
  253. b.life -= dt;
  254. b.x += b.vx * dt;
  255. b.y += b.vy * dt;
  256. if (b.player) {
  257. for (const e of state.enemies) {
  258. if (e.hp > 0 && hit(b, e, b.r, e.type === 'boss' ? 92 : 34)) {
  259. e.hp -= b.dmg;
  260. if (state.weapon === 'flame') e.burn = 1.4;
  261. if (!b.pierce) b.life = 0;
  262. }
  263. }
  264. } else if (hit(b, p, b.r, 36)) {
  265. damagePlayer();
  266. b.life = 0;
  267. }
  268. }
  269. state.enemies = state.enemies.filter((e) => {
  270. if (e.hp > 0) return true;
  271. state.score += e.type === 'boss' ? 5000 : 100;
  272. if (e.type === 'boss') {
  273. state.mode = 'win';
  274. state.notice = `MISSION CLEAR SCORE ${state.score}`;
  275. }
  276. return false;
  277. });
  278. state.bullets = state.bullets.filter(b => b.life > 0 && Math.abs(b.x - p.x) < 900);
  279. state.camera = Math.max(0, Math.min(END_X - 600, p.x - 360));
  280. }
  281. function drawImage(name, x, y, w, h, flip = false) {
  282. const img = assets[name];
  283. if (!img) return;
  284. if (flip) {
  285. ctx.save();
  286. ctx.scale(-1, 1);
  287. ctx.drawImage(img, -x - w / 2, y - h / 2, w, h);
  288. ctx.restore();
  289. } else {
  290. ctx.drawImage(img, x - w / 2, y - h / 2, w, h);
  291. }
  292. }
  293. function drawWorld(metrics) {
  294. const cam = state.camera;
  295. const parallax = (cam * 0.18) % 2048;
  296. if (assets.bg) {
  297. for (let x = metrics.left - 2048 - parallax; x < metrics.right + 2048; x += 2048) ctx.drawImage(assets.bg, x, metrics.top, 2048, metrics.height);
  298. } else {
  299. const gradient = ctx.createLinearGradient(0, metrics.top, 0, metrics.bottom);
  300. gradient.addColorStop(0, '#123656');
  301. gradient.addColorStop(0.58, '#2b6f85');
  302. gradient.addColorStop(1, '#111823');
  303. ctx.fillStyle = gradient;
  304. ctx.fillRect(metrics.left, metrics.top, metrics.width, metrics.height);
  305. }
  306. if (assets.ground) {
  307. for (let x = metrics.left - 512 - (cam % 512); x < metrics.right + 512; x += 512) ctx.drawImage(assets.ground, x, GROUND - 18, 512, 96);
  308. }
  309. for (let i = 0; i < 18; i++) drawImage('crystal', i * 235 - cam + 70, GROUND - 55 + (i % 3) * 16, 72, 90);
  310. for (const e of state.enemies) {
  311. if (e.type === 'boss') {
  312. drawImage('boss', e.x - cam, e.y, 210, 205);
  313. ctx.fillStyle = '#ff4fae';
  314. ctx.fillRect(W / 2 - 140, metrics.top + 50, 280 * Math.max(0, e.hp / e.maxHp), 10);
  315. ctx.strokeStyle = '#dffcff';
  316. ctx.strokeRect(W / 2 - 140, metrics.top + 50, 280, 10);
  317. } else {
  318. drawImage(e.type, e.x - cam, e.y, e.type === 'turret' ? 74 : 62, e.type === 'wing' ? 58 : 66);
  319. }
  320. }
  321. for (const b of state.bullets) drawImage(b.sprite, b.x - cam, b.y, b.sprite === 'rail' ? 82 : 32, b.sprite === 'rail' ? 18 : 24);
  322. drawImage('player', state.player.x - cam, state.player.y, 70, 86, state.player.face < 0);
  323. }
  324. function drawHud(metrics) {
  325. if (state.mode === 'win' || state.mode === 'lose') {
  326. const panel = state.mode === 'win' ? assets.outcomeWin : assets.outcomeFail;
  327. const panelW = 560;
  328. const panelH = 332;
  329. const panelX = W / 2 - panelW / 2;
  330. const panelY = (metrics.top + metrics.bottom) / 2 - panelH / 2 - 4;
  331. if (panel && panel.complete && panel.naturalWidth) {
  332. ctx.drawImage(panel, panelX, panelY, panelW, panelH);
  333. } else {
  334. ctx.fillStyle = 'rgba(4, 12, 25, 0.82)';
  335. ctx.fillRect(panelX, panelY, panelW, panelH);
  336. ctx.strokeStyle = state.mode === 'win' ? '#73f7ff' : '#ff725f';
  337. ctx.lineWidth = 4;
  338. ctx.strokeRect(panelX, panelY, panelW, panelH);
  339. }
  340. ctx.textAlign = 'center';
  341. ctx.font = '34px Microsoft YaHei, Arial';
  342. ctx.fillStyle = state.mode === 'win' ? '#fff0a6' : '#ffad98';
  343. ctx.fillText(state.mode === 'win' ? 'MISSION CLEAR' : 'MISSION FAILED', W / 2, panelY + 146);
  344. ctx.font = '22px Microsoft YaHei, Arial';
  345. ctx.fillStyle = '#dffcff';
  346. ctx.fillText(`SCORE ${state.score} HP ${Math.max(0, state.player.hp)}`, W / 2, panelY + 188);
  347. ctx.font = '24px Microsoft YaHei, Arial';
  348. ctx.fillStyle = '#ffffff';
  349. ctx.fillText('TAP TO RESTART', W / 2, panelY + 242);
  350. ctx.textAlign = 'left';
  351. return;
  352. }
  353. ctx.font = '22px Microsoft YaHei, Arial';
  354. ctx.fillStyle = '#fff3d0';
  355. ctx.fillText(`HP ${Math.max(0, state.player.hp)}`, metrics.left + 28, metrics.top + 38);
  356. ctx.fillStyle = '#78f0ff';
  357. ctx.fillText(weapons[state.weapon].name, metrics.left + 148, metrics.top + 38);
  358. ctx.fillStyle = '#ffd96e';
  359. ctx.fillText(`SCORE ${state.score}`, metrics.right - 168, metrics.top + 38);
  360. if (state.boss && state.boss.hp > 0) {
  361. ctx.fillStyle = '#ff73b6';
  362. ctx.fillText(`星巢主脑 ${Math.ceil(state.boss.hp)}/180`, W / 2 - 78, metrics.top + 38);
  363. }
  364. drawButton('mobileLeft', metrics.left + 72, metrics.bottom - 88, 72);
  365. drawButton('mobileRight', metrics.left + 196, metrics.bottom - 88, 72);
  366. drawButton('mobileJump', metrics.left + 134, metrics.bottom - 204, 72);
  367. drawButton('mobileSwitch', metrics.right - 222, metrics.bottom - 88, 70);
  368. drawButton('mobileFire', metrics.right - 84, metrics.bottom - 150, 82);
  369. }
  370. function drawButton(key, x, y, size) {
  371. const img = assets[key] || assets.button;
  372. ctx.drawImage(img, x - size / 2, y - size / 2, size, size);
  373. }
  374. function draw() {
  375. const metrics = fit();
  376. ctx.clearRect(metrics.left, metrics.top, metrics.width, metrics.height);
  377. drawWorld(metrics);
  378. drawHud(metrics);
  379. document.body.dataset.playerX = state.player.x.toFixed(2);
  380. document.body.dataset.playerY = state.player.y.toFixed(2);
  381. document.body.dataset.weapon = state.weapon;
  382. document.body.dataset.bullets = String(state.bullets.length);
  383. document.body.dataset.grounded = String(state.player.grounded);
  384. }
  385. function eventToGame(ev) {
  386. const t = ev.touches ? (ev.touches[0] || ev.changedTouches[0]) : ev;
  387. const metrics = fitMetrics();
  388. return { x: (t.clientX - metrics.ox) / metrics.scale, y: (t.clientY - metrics.oy) / metrics.scale };
  389. }
  390. function isInsideCircle(p, x, y, radius) {
  391. const dx = p.x - x;
  392. const dy = p.y - y;
  393. return dx * dx + dy * dy <= radius * radius;
  394. }
  395. function outcomeHit(p, metrics) {
  396. const cx = W / 2;
  397. const cy = (metrics.top + metrics.bottom) / 2 - 4;
  398. return Math.abs(p.x - cx) <= 300 && Math.abs(p.y - cy) <= 185;
  399. }
  400. function handlePointer(ev, down) {
  401. ev.preventDefault();
  402. const p = eventToGame(ev);
  403. const metrics = fitMetrics();
  404. if (!down) {
  405. state.touchMove = 0;
  406. state.touchFire = false;
  407. state.touchJump = false;
  408. state.pointerDown = false;
  409. state.switchLock = false;
  410. return;
  411. }
  412. if ((state.mode === 'win' || state.mode === 'lose') && outcomeHit(p, metrics)) {
  413. reset();
  414. return;
  415. }
  416. state.pointerDown = true;
  417. state.touchMove = 0;
  418. state.touchFire = false;
  419. state.touchJump = false;
  420. const leftX = metrics.left + 72;
  421. const rightX = metrics.left + 196;
  422. const jumpX = metrics.left + 134;
  423. const switchX = metrics.right - 222;
  424. const fireX = metrics.right - 84;
  425. const lowerY = metrics.bottom - 88;
  426. const jumpY = metrics.bottom - 204;
  427. const fireY = metrics.bottom - 150;
  428. if (isInsideCircle(p, jumpX, jumpY, 58)) {
  429. state.touchJump = true;
  430. tryJump();
  431. } else if (isInsideCircle(p, leftX, lowerY, 58)) state.touchMove = -1;
  432. else if (isInsideCircle(p, rightX, lowerY, 58)) state.touchMove = 1;
  433. else if (isInsideCircle(p, fireX, fireY, 64)) {
  434. state.touchFire = true;
  435. shoot();
  436. }
  437. else if (isInsideCircle(p, switchX, lowerY, 56) && !state.switchLock) {
  438. state.weapon = state.weapon === 'rifle' ? 'flame' : state.weapon === 'flame' ? 'rail' : 'rifle';
  439. state.switchLock = true;
  440. }
  441. }
  442. addEventListener('keydown', (e) => {
  443. const k = e.key.toLowerCase();
  444. if (k === '1') state.weapon = 'rifle';
  445. if (k === '2') state.weapon = 'flame';
  446. if (k === '3') state.weapon = 'rail';
  447. if (k === 'q') state.weapon = state.weapon === 'rifle' ? 'flame' : state.weapon === 'flame' ? 'rail' : 'rifle';
  448. if (k === 'r' && (state.mode === 'win' || state.mode === 'lose')) reset();
  449. state.keys.add(k);
  450. });
  451. addEventListener('keyup', (e) => state.keys.delete(e.key.toLowerCase()));
  452. canvas.addEventListener('touchstart', (e) => handlePointer(e, true), { passive: false });
  453. canvas.addEventListener('touchmove', (e) => handlePointer(e, true), { passive: false });
  454. canvas.addEventListener('touchend', (e) => handlePointer(e, false), { passive: false });
  455. canvas.addEventListener('touchcancel', (e) => handlePointer(e, false), { passive: false });
  456. canvas.addEventListener('mousedown', (e) => handlePointer(e, true));
  457. canvas.addEventListener('mousemove', (e) => { if (state.pointerDown) handlePointer(e, true); });
  458. addEventListener('mouseup', (e) => handlePointer(e, false));
  459. addEventListener('resize', resize);
  460. let last = performance.now();
  461. function loop(now) {
  462. const dt = Math.min(0.033, (now - last) / 1000);
  463. last = now;
  464. update(dt);
  465. draw();
  466. requestAnimationFrame(loop);
  467. }
  468. function applyPreviewState() {
  469. const params = new URLSearchParams(location.search);
  470. if (params.get('preview') !== 'win' && params.get('preview') !== 'lose') return;
  471. state.mode = params.get('preview') === 'lose' ? 'lose' : 'win';
  472. state.score = 6800;
  473. state.player.hp = state.mode === 'lose' ? 0 : 5;
  474. state.player.x = END_X - 220;
  475. state.notice = state.mode === 'lose' ? `MISSION FAILED SCORE ${state.score}` : `MISSION CLEAR SCORE ${state.score}`;
  476. }
  477. resize();
  478. load().then(() => {
  479. applyPreviewState();
  480. requestAnimationFrame(loop);
  481. });
  482. })();
  483. </script>
  484. </body>
  485. </html>