| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519 |
- <!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>
- html, body {
- margin: 0;
- width: 100%;
- height: 100%;
- overflow: hidden;
- background: #07101d;
- font-family: "Microsoft YaHei", Arial, sans-serif;
- }
- canvas {
- display: block;
- width: 100vw;
- height: 100vh;
- image-rendering: auto;
- touch-action: none;
- }
- </style>
- </head>
- <body>
- <canvas id="game"></canvas>
- <script>
- (() => {
- const canvas = document.getElementById('game');
- const ctx = canvas.getContext('2d');
- const W = 960;
- const H = 540;
- const GROUND = 375;
- const END_X = 3320;
- const assets = {};
- const sources = {
- ground: '../assets/resources/sprites/ground_strip.png',
- player: '../assets/resources/sprites/player.png',
- spore: '../assets/resources/sprites/enemy_spore.png',
- wing: '../assets/resources/sprites/enemy_wing.png',
- turret: '../assets/resources/sprites/enemy_turret.png',
- boss: '../assets/resources/sprites/imagegen/frames/boss_idle_pulse.png',
- crystal: '../assets/resources/sprites/alien_crystal.png',
- rifle: '../assets/resources/sprites/bullet_rifle.png',
- flame: '../assets/resources/sprites/bullet_flame.png',
- rail: '../assets/resources/sprites/bullet_rail.png',
- acid: '../assets/resources/sprites/bullet_acid.png',
- button: '../assets/resources/sprites/ui_button.png',
- outcomeFail: '../assets/resources/sprites/imagegen/ui_scene/ui/outcome_fail_panel.png',
- outcomeWin: '../assets/resources/sprites/imagegen/ui_scene/ui/outcome_win_panel.png',
- mobileLeft: '../assets/resources/sprites/imagegen/ui_scene/ui/mobile_left.png',
- mobileRight: '../assets/resources/sprites/imagegen/ui_scene/ui/mobile_right.png',
- mobileJump: '../assets/resources/sprites/imagegen/ui_scene/ui/mobile_jump.png',
- mobileFire: '../assets/resources/sprites/imagegen/ui_scene/ui/mobile_fire.png',
- mobileSwitch: '../assets/resources/sprites/imagegen/ui_scene/ui/mobile_switch.png'
- };
- const weapons = {
- rifle: { name: '突击步枪', cd: 0.105, dmg: 4, speed: 880, sprite: 'rifle', pierce: false, life: 0.85 },
- flame: { name: '火焰喷射器', cd: 0.055, dmg: 1.35, speed: 520, sprite: 'flame', pierce: false, life: 0.24 },
- rail: { name: '电磁炮', cd: 0.58, dmg: 18, speed: 1250, sprite: 'rail', pierce: true, life: 0.85 }
- };
- const state = {
- mode: 'playing',
- keys: new Set(),
- touchMove: 0,
- touchFire: false,
- touchJump: false,
- pointerDown: false,
- switchLock: false,
- weapon: 'rifle',
- score: 0,
- camera: 0,
- fireCd: 0,
- mercy: 0,
- player: { x: 120, y: GROUND - 65, vx: 0, vy: 0, hp: 6, face: 1, grounded: true },
- bullets: [],
- enemies: [],
- spawned: new Set(),
- boss: null,
- bossTime: 0,
- notice: ''
- };
- window.__steelDebug = { state, fitMetrics: () => fitMetrics() };
- const waves = [
- { x: 420, type: 'spore', count: 4 },
- { x: 860, type: 'wing', count: 3 },
- { x: 1280, type: 'turret', count: 2 },
- { x: 1680, type: 'spore', count: 5 },
- { x: 2200, type: 'wing', count: 4 },
- { x: 2700, type: 'spore', count: 3 }
- ];
- function resize() {
- const dpr = Math.min(devicePixelRatio || 1, 2);
- canvas.width = Math.floor(innerWidth * dpr);
- canvas.height = Math.floor(innerHeight * dpr);
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
- }
- function fitMetrics() {
- const ratio = innerWidth / Math.max(1, innerHeight);
- const width = Math.max(W, H * ratio);
- const height = Math.max(H, W / Math.max(0.001, ratio));
- const left = (W - width) / 2;
- const top = (H - height) / 2;
- const scale = innerWidth / width;
- const ox = (innerWidth - width * scale) / 2 - left * scale;
- const oy = (innerHeight - height * scale) / 2 - top * scale;
- return { width, height, left, right: left + width, top, bottom: top + height, scale, ox, oy };
- }
- function fit() {
- const metrics = fitMetrics();
- ctx.setTransform(metrics.scale, 0, 0, metrics.scale, metrics.ox, metrics.oy);
- return metrics;
- }
- function load() {
- return Promise.all(Object.entries(sources).map(([key, src]) => new Promise((resolve) => {
- const img = new Image();
- img.onload = () => { assets[key] = img; resolve(); };
- img.onerror = () => { console.warn(`asset missing: ${src}`); resolve(); };
- img.src = src;
- })));
- }
- function reset() {
- state.mode = 'playing';
- state.weapon = 'rifle';
- state.score = 0;
- state.camera = 0;
- state.fireCd = 0;
- state.mercy = 0;
- state.touchMove = 0;
- state.touchFire = false;
- state.touchJump = false;
- state.pointerDown = false;
- state.switchLock = false;
- state.player = { x: 120, y: GROUND - 65, vx: 0, vy: 0, hp: 6, face: 1, grounded: true };
- state.bullets = [];
- state.enemies = [];
- state.spawned = new Set();
- state.boss = null;
- state.bossTime = 0;
- state.notice = '';
- }
- function spawnEnemy(type, x, i = 0) {
- const hp = type === 'turret' ? 24 : type === 'wing' ? 16 : 12;
- 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 });
- }
- function spawnBoss() {
- if (state.boss) return;
- const boss = { type: 'boss', x: 3290, y: GROUND - 122, hp: 180, maxHp: 180, vx: 0, cd: 0.5, burn: 0 };
- state.boss = boss;
- state.enemies.push(boss);
- state.mode = 'boss';
- }
- function shoot() {
- const w = weapons[state.weapon];
- if (state.fireCd > 0) return;
- state.fireCd = w.cd;
- const p = state.player;
- if (state.weapon === 'flame') {
- for (let i = 0; i < 2; i++) {
- 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 });
- }
- return;
- }
- 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 });
- }
- function tryJump() {
- if (state.mode === 'win' || state.mode === 'lose' || !state.player.grounded) return;
- state.player.vy = -620;
- state.player.grounded = false;
- }
- function enemyShot(x, y, dir, speed = 220) {
- state.bullets.push({ x, y, vx: dir * speed, vy: -20, dmg: 1, life: 3, player: false, pierce: false, r: 12, sprite: 'acid' });
- }
- function hit(a, b, ar, br) {
- const dx = a.x - b.x;
- const dy = a.y - b.y;
- return dx * dx + dy * dy < (ar + br) * (ar + br);
- }
- function damagePlayer() {
- if (state.mercy > 0) return;
- state.player.hp -= 1;
- state.mercy = 0.55;
- if (state.player.hp <= 0) {
- state.mode = 'lose';
- state.notice = 'MISSION FAILED 按 R 重试';
- }
- }
- function update(dt) {
- if (state.mode === 'win' || state.mode === 'lose') return;
- const p = state.player;
- const left = state.keys.has('a') || state.keys.has('arrowleft') || state.touchMove < 0;
- const right = state.keys.has('d') || state.keys.has('arrowright') || state.touchMove > 0;
- const jump = state.keys.has(' ') || state.keys.has('w') || state.keys.has('arrowup') || state.touchJump;
- const fire = state.keys.has('j') || state.keys.has('enter') || state.touchFire;
- p.vx = left ? -270 : right ? 270 : 0;
- if (p.vx) p.face = p.vx > 0 ? 1 : -1;
- if (jump) tryJump();
- p.vy += 1450 * dt;
- p.x = Math.max(20, Math.min(END_X - 180, p.x + p.vx * dt));
- p.y += p.vy * dt;
- if (p.y >= GROUND - 65) {
- p.y = GROUND - 65;
- p.vy = 0;
- p.grounded = true;
- }
- state.fireCd -= dt;
- state.mercy -= dt;
- if (fire) shoot();
- waves.forEach((wave, index) => {
- if (p.x > wave.x - 520 && !state.spawned.has(index)) {
- state.spawned.add(index);
- for (let i = 0; i < wave.count; i++) spawnEnemy(wave.type, wave.x + i * 58, i);
- }
- });
- if (p.x > 3090) spawnBoss();
- for (const e of state.enemies) {
- if (e.hp <= 0) continue;
- e.cd -= dt;
- e.burn = Math.max(0, e.burn - dt);
- if (e.burn > 0) e.hp -= 5 * dt;
- if (e.type === 'spore') e.vx = e.x > p.x ? -105 : 105;
- if (e.type === 'wing') {
- e.vx = e.x > p.x ? -82 : 82;
- e.y += Math.sin(performance.now() / 260 + e.x * 0.02) * 34 * dt;
- if (e.cd <= 0) {
- enemyShot(e.x, e.y, p.x > e.x ? 1 : -1);
- e.cd = 1.35;
- }
- }
- if (e.type === 'turret') {
- e.vx = 0;
- if (e.cd <= 0) {
- enemyShot(e.x - 20, e.y + 8, -1);
- e.cd = 1.05;
- }
- }
- if (e.type === 'boss') {
- state.bossTime += dt;
- e.vx = Math.sin(state.bossTime * 0.75) * 28;
- const phase = e.hp > 115 ? 1 : e.hp > 55 ? 2 : 3;
- if (e.cd <= 0) {
- const dir = p.x > e.x ? 1 : -1;
- enemyShot(e.x - 80, e.y + 58, dir, phase >= 3 ? 270 : 220);
- if (phase >= 2) enemyShot(e.x - 80, e.y - 14, dir, 180);
- if (phase >= 3 && state.enemies.filter(v => v.type === 'spore').length < 4) spawnEnemy('spore', e.x - 120);
- e.cd = phase === 1 ? 1.1 : phase === 2 ? 0.85 : 0.62;
- }
- }
- e.x += e.vx * dt;
- if (hit(p, e, 36, e.type === 'boss' ? 92 : 34)) damagePlayer();
- }
- for (const b of state.bullets) {
- b.life -= dt;
- b.x += b.vx * dt;
- b.y += b.vy * dt;
- if (b.player) {
- for (const e of state.enemies) {
- if (e.hp > 0 && hit(b, e, b.r, e.type === 'boss' ? 92 : 34)) {
- e.hp -= b.dmg;
- if (state.weapon === 'flame') e.burn = 1.4;
- if (!b.pierce) b.life = 0;
- }
- }
- } else if (hit(b, p, b.r, 36)) {
- damagePlayer();
- b.life = 0;
- }
- }
- state.enemies = state.enemies.filter((e) => {
- if (e.hp > 0) return true;
- state.score += e.type === 'boss' ? 5000 : 100;
- if (e.type === 'boss') {
- state.mode = 'win';
- state.notice = `MISSION CLEAR SCORE ${state.score}`;
- }
- return false;
- });
- state.bullets = state.bullets.filter(b => b.life > 0 && Math.abs(b.x - p.x) < 900);
- state.camera = Math.max(0, Math.min(END_X - 600, p.x - 360));
- }
- function drawImage(name, x, y, w, h, flip = false) {
- const img = assets[name];
- if (!img) return;
- if (flip) {
- ctx.save();
- ctx.scale(-1, 1);
- ctx.drawImage(img, -x - w / 2, y - h / 2, w, h);
- ctx.restore();
- } else {
- ctx.drawImage(img, x - w / 2, y - h / 2, w, h);
- }
- }
- function drawWorld(metrics) {
- const cam = state.camera;
- const parallax = (cam * 0.18) % 2048;
- if (assets.bg) {
- for (let x = metrics.left - 2048 - parallax; x < metrics.right + 2048; x += 2048) ctx.drawImage(assets.bg, x, metrics.top, 2048, metrics.height);
- } else {
- const gradient = ctx.createLinearGradient(0, metrics.top, 0, metrics.bottom);
- gradient.addColorStop(0, '#123656');
- gradient.addColorStop(0.58, '#2b6f85');
- gradient.addColorStop(1, '#111823');
- ctx.fillStyle = gradient;
- ctx.fillRect(metrics.left, metrics.top, metrics.width, metrics.height);
- }
- if (assets.ground) {
- for (let x = metrics.left - 512 - (cam % 512); x < metrics.right + 512; x += 512) ctx.drawImage(assets.ground, x, GROUND - 18, 512, 96);
- }
- for (let i = 0; i < 18; i++) drawImage('crystal', i * 235 - cam + 70, GROUND - 55 + (i % 3) * 16, 72, 90);
- for (const e of state.enemies) {
- if (e.type === 'boss') {
- drawImage('boss', e.x - cam, e.y, 210, 205);
- ctx.fillStyle = '#ff4fae';
- ctx.fillRect(W / 2 - 140, metrics.top + 50, 280 * Math.max(0, e.hp / e.maxHp), 10);
- ctx.strokeStyle = '#dffcff';
- ctx.strokeRect(W / 2 - 140, metrics.top + 50, 280, 10);
- } else {
- drawImage(e.type, e.x - cam, e.y, e.type === 'turret' ? 74 : 62, e.type === 'wing' ? 58 : 66);
- }
- }
- for (const b of state.bullets) drawImage(b.sprite, b.x - cam, b.y, b.sprite === 'rail' ? 82 : 32, b.sprite === 'rail' ? 18 : 24);
- drawImage('player', state.player.x - cam, state.player.y, 70, 86, state.player.face < 0);
- }
- function drawHud(metrics) {
- if (state.mode === 'win' || state.mode === 'lose') {
- const panel = state.mode === 'win' ? assets.outcomeWin : assets.outcomeFail;
- const panelW = 560;
- const panelH = 332;
- const panelX = W / 2 - panelW / 2;
- const panelY = (metrics.top + metrics.bottom) / 2 - panelH / 2 - 4;
- if (panel && panel.complete && panel.naturalWidth) {
- ctx.drawImage(panel, panelX, panelY, panelW, panelH);
- } else {
- ctx.fillStyle = 'rgba(4, 12, 25, 0.82)';
- ctx.fillRect(panelX, panelY, panelW, panelH);
- ctx.strokeStyle = state.mode === 'win' ? '#73f7ff' : '#ff725f';
- ctx.lineWidth = 4;
- ctx.strokeRect(panelX, panelY, panelW, panelH);
- }
- ctx.textAlign = 'center';
- ctx.font = '34px Microsoft YaHei, Arial';
- ctx.fillStyle = state.mode === 'win' ? '#fff0a6' : '#ffad98';
- ctx.fillText(state.mode === 'win' ? 'MISSION CLEAR' : 'MISSION FAILED', W / 2, panelY + 146);
- ctx.font = '22px Microsoft YaHei, Arial';
- ctx.fillStyle = '#dffcff';
- ctx.fillText(`SCORE ${state.score} HP ${Math.max(0, state.player.hp)}`, W / 2, panelY + 188);
- ctx.font = '24px Microsoft YaHei, Arial';
- ctx.fillStyle = '#ffffff';
- ctx.fillText('TAP TO RESTART', W / 2, panelY + 242);
- ctx.textAlign = 'left';
- return;
- }
- ctx.font = '22px Microsoft YaHei, Arial';
- ctx.fillStyle = '#fff3d0';
- ctx.fillText(`HP ${Math.max(0, state.player.hp)}`, metrics.left + 28, metrics.top + 38);
- ctx.fillStyle = '#78f0ff';
- ctx.fillText(weapons[state.weapon].name, metrics.left + 148, metrics.top + 38);
- ctx.fillStyle = '#ffd96e';
- ctx.fillText(`SCORE ${state.score}`, metrics.right - 168, metrics.top + 38);
- if (state.boss && state.boss.hp > 0) {
- ctx.fillStyle = '#ff73b6';
- ctx.fillText(`星巢主脑 ${Math.ceil(state.boss.hp)}/180`, W / 2 - 78, metrics.top + 38);
- }
- drawButton('mobileLeft', metrics.left + 72, metrics.bottom - 88, 72);
- drawButton('mobileRight', metrics.left + 196, metrics.bottom - 88, 72);
- drawButton('mobileJump', metrics.left + 134, metrics.bottom - 204, 72);
- drawButton('mobileSwitch', metrics.right - 222, metrics.bottom - 88, 70);
- drawButton('mobileFire', metrics.right - 84, metrics.bottom - 150, 82);
- }
- function drawButton(key, x, y, size) {
- const img = assets[key] || assets.button;
- ctx.drawImage(img, x - size / 2, y - size / 2, size, size);
- }
- function draw() {
- const metrics = fit();
- ctx.clearRect(metrics.left, metrics.top, metrics.width, metrics.height);
- drawWorld(metrics);
- drawHud(metrics);
- document.body.dataset.playerX = state.player.x.toFixed(2);
- document.body.dataset.playerY = state.player.y.toFixed(2);
- document.body.dataset.weapon = state.weapon;
- document.body.dataset.bullets = String(state.bullets.length);
- document.body.dataset.grounded = String(state.player.grounded);
- }
- function eventToGame(ev) {
- const t = ev.touches ? (ev.touches[0] || ev.changedTouches[0]) : ev;
- const metrics = fitMetrics();
- return { x: (t.clientX - metrics.ox) / metrics.scale, y: (t.clientY - metrics.oy) / metrics.scale };
- }
- function isInsideCircle(p, x, y, radius) {
- const dx = p.x - x;
- const dy = p.y - y;
- return dx * dx + dy * dy <= radius * radius;
- }
- function outcomeHit(p, metrics) {
- const cx = W / 2;
- const cy = (metrics.top + metrics.bottom) / 2 - 4;
- return Math.abs(p.x - cx) <= 300 && Math.abs(p.y - cy) <= 185;
- }
- function handlePointer(ev, down) {
- ev.preventDefault();
- const p = eventToGame(ev);
- const metrics = fitMetrics();
- if (!down) {
- state.touchMove = 0;
- state.touchFire = false;
- state.touchJump = false;
- state.pointerDown = false;
- state.switchLock = false;
- return;
- }
- if ((state.mode === 'win' || state.mode === 'lose') && outcomeHit(p, metrics)) {
- reset();
- return;
- }
- state.pointerDown = true;
- state.touchMove = 0;
- state.touchFire = false;
- state.touchJump = false;
- const leftX = metrics.left + 72;
- const rightX = metrics.left + 196;
- const jumpX = metrics.left + 134;
- const switchX = metrics.right - 222;
- const fireX = metrics.right - 84;
- const lowerY = metrics.bottom - 88;
- const jumpY = metrics.bottom - 204;
- const fireY = metrics.bottom - 150;
- if (isInsideCircle(p, jumpX, jumpY, 58)) {
- state.touchJump = true;
- tryJump();
- } else if (isInsideCircle(p, leftX, lowerY, 58)) state.touchMove = -1;
- else if (isInsideCircle(p, rightX, lowerY, 58)) state.touchMove = 1;
- else if (isInsideCircle(p, fireX, fireY, 64)) {
- state.touchFire = true;
- shoot();
- }
- else if (isInsideCircle(p, switchX, lowerY, 56) && !state.switchLock) {
- state.weapon = state.weapon === 'rifle' ? 'flame' : state.weapon === 'flame' ? 'rail' : 'rifle';
- state.switchLock = true;
- }
- }
- addEventListener('keydown', (e) => {
- const k = e.key.toLowerCase();
- if (k === '1') state.weapon = 'rifle';
- if (k === '2') state.weapon = 'flame';
- if (k === '3') state.weapon = 'rail';
- if (k === 'q') state.weapon = state.weapon === 'rifle' ? 'flame' : state.weapon === 'flame' ? 'rail' : 'rifle';
- if (k === 'r' && (state.mode === 'win' || state.mode === 'lose')) reset();
- state.keys.add(k);
- });
- addEventListener('keyup', (e) => state.keys.delete(e.key.toLowerCase()));
- canvas.addEventListener('touchstart', (e) => handlePointer(e, true), { passive: false });
- canvas.addEventListener('touchmove', (e) => handlePointer(e, true), { passive: false });
- canvas.addEventListener('touchend', (e) => handlePointer(e, false), { passive: false });
- canvas.addEventListener('touchcancel', (e) => handlePointer(e, false), { passive: false });
- canvas.addEventListener('mousedown', (e) => handlePointer(e, true));
- canvas.addEventListener('mousemove', (e) => { if (state.pointerDown) handlePointer(e, true); });
- addEventListener('mouseup', (e) => handlePointer(e, false));
- addEventListener('resize', resize);
- let last = performance.now();
- function loop(now) {
- const dt = Math.min(0.033, (now - last) / 1000);
- last = now;
- update(dt);
- draw();
- requestAnimationFrame(loop);
- }
- function applyPreviewState() {
- const params = new URLSearchParams(location.search);
- if (params.get('preview') !== 'win' && params.get('preview') !== 'lose') return;
- state.mode = params.get('preview') === 'lose' ? 'lose' : 'win';
- state.score = 6800;
- state.player.hp = state.mode === 'lose' ? 0 : 5;
- state.player.x = END_X - 220;
- state.notice = state.mode === 'lose' ? `MISSION FAILED SCORE ${state.score}` : `MISSION CLEAR SCORE ${state.score}`;
- }
- resize();
- load().then(() => {
- applyPreviewState();
- requestAnimationFrame(loop);
- });
- })();
- </script>
- </body>
- </html>
|