fix: 修复了冰块惯性问题和死亡效果
This commit is contained in:
1 parent
47a462be01
commit
4b3dfd635b
15 files changed
+633
-38
No files matched your search
+15
@@ -15,6 +15,7 @@ const BIND = {
|
||||
const down = new Set(); // 当前按住的原始 code
|
||||
const now = {}; // action 是否按下
|
||||
const prev = {}; // 上一帧
|
||||
const touch = {}; // action -> bool(来自触屏虚拟按键)
|
||||
const blocked = new Set(['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab']);
|
||||
|
||||
export const Input = {
|
||||
@@ -44,10 +45,24 @@ function refresh() {
|
||||
for (const act in BIND) {
|
||||
let v = false;
|
||||
for (const c of BIND[act]) if (down.has(c)) { v = true; break; }
|
||||
if (touch[act]) v = true; // 触屏虚拟按键
|
||||
now[act] = Input.enabled ? v : false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- 触屏虚拟按键接口 ----------------
|
||||
export function setTouch(action, on) {
|
||||
if (!(action in touch)) touch[action] = false;
|
||||
if (touch[action] === on) return;
|
||||
touch[action] = on;
|
||||
refresh();
|
||||
}
|
||||
export function clearTouch() {
|
||||
let any = false;
|
||||
for (const k in touch) if (touch[k]) { touch[k] = false; any = true; }
|
||||
if (any) refresh();
|
||||
}
|
||||
|
||||
addEventListener('keydown', e => {
|
||||
if (blocked.has(e.code)) e.preventDefault();
|
||||
if (e.repeat) return;
|
||||
|
||||
+32
@@ -87,6 +87,7 @@ export class Level {
|
||||
/** 完整重置(死亡 / 重开) */
|
||||
reset(full = false) {
|
||||
// tiles 状态(vanish 等)
|
||||
if (full) this.deathMarks = []; // 死亡血迹只在整关重开时清空,复活后保留
|
||||
this.vanish = new Map();
|
||||
this.bullets = [];
|
||||
this.ebullets = [];
|
||||
@@ -241,6 +242,32 @@ export class Level {
|
||||
}
|
||||
}
|
||||
|
||||
// 死亡标记:在死亡地点留下一个常驻「红叉」(IWBTG 风格死亡指示)。
|
||||
// 只保留上一次死亡的红叉:新增前先清空,多次死亡不会累积。
|
||||
addDeathMark(x, y) {
|
||||
this.deathMarks.length = 0;
|
||||
this.deathMarks.push({ x, y });
|
||||
}
|
||||
|
||||
drawDeathMark(ctx, dm) {
|
||||
const s = 4; // 红叉半边长(< kid 的 11×21,整体约 8px)
|
||||
ctx.lineCap = 'round';
|
||||
// 深色描边,保证在任意背景下都清晰
|
||||
ctx.lineWidth = 3;
|
||||
ctx.strokeStyle = 'rgba(0,0,0,0.55)';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(dm.x - s, dm.y - s); ctx.lineTo(dm.x + s, dm.y + s);
|
||||
ctx.moveTo(dm.x + s, dm.y - s); ctx.lineTo(dm.x - s, dm.y + s);
|
||||
ctx.stroke();
|
||||
// 红色主体
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeStyle = '#ff2535';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(dm.x - s, dm.y - s); ctx.lineTo(dm.x + s, dm.y + s);
|
||||
ctx.moveTo(dm.x + s, dm.y - s); ctx.lineTo(dm.x - s, dm.y + s);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// ---------------- 查询 ----------------
|
||||
tileAt(tx, ty) {
|
||||
if (tx < 0 || ty < 0 || tx >= this.w || ty >= this.h) return ty < 0 ? T.EMPTY : T.BLOCK;
|
||||
@@ -458,6 +485,8 @@ export class Level {
|
||||
if (this.shake > 0) this.shake *= 0.86;
|
||||
if (this.toastT > 0 && --this.toastT === 0) this.toast = null;
|
||||
this.particles.update();
|
||||
|
||||
// 死亡红叉为常驻静态标记,无需逐帧演化,复活后仍保留
|
||||
}
|
||||
|
||||
fireEvent(ev, player) {
|
||||
@@ -587,6 +616,9 @@ export class Level {
|
||||
for (const b of this.bullets) S.drawBullet(ctx, b);
|
||||
for (const b of this.ebullets) S.drawEBullet(ctx, b, t);
|
||||
|
||||
// 死亡血迹(IWBTG 风格,常驻于死亡地点)
|
||||
for (const dm of this.deathMarks) this.drawDeathMark(ctx, dm);
|
||||
|
||||
this.particles.draw(ctx);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
+32
-12
@@ -6,6 +6,7 @@ import { Camera } from './camera.js';
|
||||
import { Input } from './input.js';
|
||||
import { UI } from './ui.js';
|
||||
import { Save } from './save.js';
|
||||
import { Touch } from './touch.js';
|
||||
import { sfx, unlockAudio, Audio2 } from './audio.js';
|
||||
import { Editor } from './editor.js';
|
||||
import { clamp } from './util.js';
|
||||
@@ -13,6 +14,16 @@ import { clamp } from './util.js';
|
||||
const W = 800, H = 608;
|
||||
const STEP = 1000 / 60;
|
||||
|
||||
// 移动端检测:触屏设备自动启用虚拟手柄;?touch=1 强制开启,?touch=0 强制关闭(桌面调试用)
|
||||
// 判定条件:① 是移动端(存在 ontouchstart)/ 支持触屏(maxTouchPoints>0)/ 粗指针(触屏笔或手指)
|
||||
// 只有命中才给 <html> 加 .touch 类;CSS 里 html:not(.touch) #touch 会强制隐藏,桌面端永远不显示按钮
|
||||
const _qp = new URLSearchParams(location.search);
|
||||
const _force = _qp.get('touch');
|
||||
const _isTouch = ('ontouchstart' in window) || navigator.maxTouchPoints > 0 || matchMedia('(pointer: coarse)').matches;
|
||||
const useTouch = _force === '1' || (_force !== '0' && _isTouch);
|
||||
if (useTouch) document.documentElement.classList.add('touch');
|
||||
function showTouch() { if (useTouch) Touch.show(); }
|
||||
|
||||
const canvas = document.getElementById('game');
|
||||
const ctx = canvas.getContext('2d', { alpha: false });
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
@@ -40,6 +51,7 @@ function startLevel(data, idx = -1) {
|
||||
G.deaths = 0; G.ms = 0; G.hudTick = 0;
|
||||
G.state = 'playing';
|
||||
UI.showGame();
|
||||
showTouch();
|
||||
UI.toast('');
|
||||
if (data.hint) UI.toast(data.hint);
|
||||
}
|
||||
@@ -58,9 +70,17 @@ function suicide() {
|
||||
G.player.kill();
|
||||
}
|
||||
|
||||
/** 触屏射击:仅在持枪、未死亡且游戏进行中时发射(受玩家自身冷却限制) */
|
||||
function shoot() {
|
||||
const P = G.player;
|
||||
if (G.state !== 'playing' || !P || !P.alive || !P.hasGun || P.shootCool !== 0) return;
|
||||
P.shoot();
|
||||
}
|
||||
|
||||
function finish() {
|
||||
if (G.state !== 'playing') return;
|
||||
G.state = 'result';
|
||||
Touch.hide();
|
||||
sfx.goal();
|
||||
const apples = G.player.apples || 0;
|
||||
const total = G.level.totalApples || 0;
|
||||
@@ -76,10 +96,10 @@ function nextLevel() {
|
||||
if (G.idx >= 0 && G.idx + 1 < LEVELS.length) startLevel(LEVELS[G.idx + 1], G.idx + 1);
|
||||
else toLevels();
|
||||
}
|
||||
function toLevels() { G.state = 'menu'; UI.show('scr-levels'); }
|
||||
function toTitle() { G.state = 'menu'; UI.show('scr-title'); }
|
||||
function pause() { if (G.state === 'playing') { G.state = 'paused'; UI.show('scr-pause'); } }
|
||||
function resume() { if (G.state === 'paused') { G.state = 'playing'; UI.showGame(); } }
|
||||
function toLevels() { G.state = 'menu'; UI.show('scr-levels'); Touch.hide(); }
|
||||
function toTitle() { G.state = 'menu'; UI.show('scr-title'); Touch.hide(); }
|
||||
function pause() { if (G.state === 'playing') { G.state = 'paused'; UI.show('scr-pause'); Touch.hide(); } }
|
||||
function resume() { if (G.state === 'paused') { G.state = 'playing'; UI.showGame(); showTouch(); } }
|
||||
|
||||
// ---------------- 更新 ----------------
|
||||
function update() {
|
||||
@@ -94,7 +114,8 @@ function update() {
|
||||
for (const b of L.bosses) b.update(1, P);
|
||||
P.update(1);
|
||||
// 刚从死亡中复活:镜头立刻对准重生点,别慢慢飘过去
|
||||
if (wasDead && !P.dead) G.cam.snapTo(P, L);
|
||||
if (P.justRespawned || (wasDead && !P.dead)) G.cam.snapTo(P, L);
|
||||
P.justRespawned = false;
|
||||
G.cam.update(L, P, 1);
|
||||
G.ms += STEP;
|
||||
G.time++;
|
||||
@@ -105,6 +126,7 @@ function update() {
|
||||
apples: P.apples, totalApples: L.totalApples, hasGun: P.hasGun,
|
||||
save: P.saveIdx || 0, totalSaves: L.totalSaves || 0,
|
||||
});
|
||||
Touch.setGun(P.hasGun);
|
||||
const boss = L.bosses.find(b => !b.dead) || L.bosses[0];
|
||||
UI.bossBar(L.bosses.length ? boss : null);
|
||||
}
|
||||
@@ -126,11 +148,6 @@ function render() {
|
||||
ctx.restore();
|
||||
L.drawOverlay(ctx, C, t);
|
||||
UI.toast(L.toast || '');
|
||||
// 死亡时的血色闪烁
|
||||
if (P.dead) {
|
||||
ctx.fillStyle = 'rgba(200,0,40,.18)';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
}
|
||||
} else {
|
||||
drawTitleBg(t);
|
||||
}
|
||||
@@ -188,7 +205,7 @@ UI.init({
|
||||
play: (lv) => startLevel(lv, lv.builtin === false ? -1 : (lv.index ?? -1)),
|
||||
levels: toLevels,
|
||||
help: () => UI.show('scr-help'),
|
||||
editor: () => { G.state = 'editor'; UI.show('scr-editor'); Editor.open(); },
|
||||
editor: () => { G.state = 'editor'; UI.show('scr-editor'); Touch.hide(); Editor.open(); },
|
||||
back: toTitle,
|
||||
resume,
|
||||
restart: () => { restart(); },
|
||||
@@ -210,8 +227,11 @@ const unlock = () => { unlockAudio(); removeEventListener('pointerdown', unlock)
|
||||
addEventListener('pointerdown', unlock);
|
||||
addEventListener('keydown', unlock);
|
||||
|
||||
// 移动端虚拟手柄
|
||||
Touch.init({ pause, suicide, shoot });
|
||||
|
||||
UI.show('scr-title');
|
||||
requestAnimationFrame(loop);
|
||||
|
||||
// 调试钩子
|
||||
window.__IW = { G, LEVELS, Save, startLevel };
|
||||
window.__IW = { G, LEVELS, Save, startLevel, pause, resume, restart, suicide };
|
||||
+70
-21
@@ -16,6 +16,13 @@ export const PHY = {
|
||||
WATER_RISE: 0.42,
|
||||
ICE_ACC: 0.18,
|
||||
ICE_FRIC: 0.07,
|
||||
// 滑出冰块后的惯性:空中几乎不损失,地面缓慢收住
|
||||
SLIDE_FRIC: 0.12, // 非冰地面上收住惯性的摩擦
|
||||
SLIDE_AIR: 0.02, // 空中惯性衰减(≈0,滑出冰块边缘能一路飞出去)
|
||||
SLIDE_EASE: 0.14, // 超速滑行时按住同向键,收敛回正常走速的速度
|
||||
SLIDE_BRAKE: 0.45, // 滑行中按反向键的刹车
|
||||
SLIDE_MIN: 0.35, // 冰面上攒到多大速度才算进入滑行状态
|
||||
SLIDE_STOP: 0.05, // 低于此速度惯性结束
|
||||
};
|
||||
|
||||
export class Player {
|
||||
@@ -41,6 +48,8 @@ export class Player {
|
||||
this.animT = 0;
|
||||
this.shootCool = 0;
|
||||
this.mover = null;
|
||||
this.sliding = false; // 冰面滑行惯性状态:离开冰块后仍保留
|
||||
this.justRespawned = false; // 本帧刚复活(供镜头瞬移)
|
||||
if (full) {
|
||||
this.hasGun = !!this.level.data.startGun;
|
||||
this.apples = 0;
|
||||
@@ -52,16 +61,14 @@ export class Player {
|
||||
get cx() { return this.x + this.w / 2; }
|
||||
get cy() { return this.y + this.h / 2; }
|
||||
|
||||
// 死亡:不播任何死亡动画(无等待、无镜头演出),只留音效、死亡计数,
|
||||
// 以及在死亡地点落一处 IWBTG 风格的血迹(由 Level 常驻渲染,复活后仍在)。
|
||||
kill() {
|
||||
if (this.dead || !this.alive) return;
|
||||
this.dead = true; this.alive = false; this.deadT = 0;
|
||||
this.vx = 0; this.vy = 0;
|
||||
this.vx = 0; this.vy = 0; this.sliding = false;
|
||||
sfx.death();
|
||||
this.level.particles.burst(this.cx, this.cy, {
|
||||
n: 26, colors: ['#e03050', '#fff', '#ffc63a', '#8e2530'],
|
||||
speed: 4.5, life: 44, size: 4, grav: 0.18,
|
||||
});
|
||||
this.level.shake = 9;
|
||||
this.level.addDeathMark(this.cx, this.cy);
|
||||
if (this.onDeath) this.onDeath();
|
||||
}
|
||||
|
||||
@@ -71,11 +78,13 @@ export class Player {
|
||||
const sp = this.savePoint || L.spawn;
|
||||
this.x = sp.x; this.y = sp.y;
|
||||
this.vx = 0; this.vy = 0; this.jumps = 0;
|
||||
this.squash = 1; this.shootCool = 0;
|
||||
this.squash = 1; this.shootCool = 0; this.sliding = false;
|
||||
this.deadT = 0;
|
||||
// 安全兜底:万一重生点被地形/消失块堵住,向上顶出,别一复活就卡死
|
||||
let guard = 0;
|
||||
while (L.solidRect(this.x, this.y, this.w, this.h) && guard++ < 48) this.y -= 2;
|
||||
L.softReset();
|
||||
this.justRespawned = true; // 供 main.js 把镜头直接吸附到重生点
|
||||
}
|
||||
|
||||
// ---------------- 每帧更新 ----------------
|
||||
@@ -84,32 +93,60 @@ export class Player {
|
||||
this.animT++;
|
||||
if (this.shootCool > 0) this.shootCool--;
|
||||
|
||||
if (this.dead) {
|
||||
if (++this.deadT > 34) this.respawn();
|
||||
return;
|
||||
}
|
||||
// 上一帧/外部(R 键自杀、假存档)触发的死亡:立刻复活,不留死亡动画
|
||||
if (this.dead) this.respawn();
|
||||
|
||||
const inWater = L.waterRect(this.x + 2, this.y + 4, this.w - 4, this.h - 4);
|
||||
if (inWater && !this.inWater) sfx.splash();
|
||||
this.inWater = inWater;
|
||||
|
||||
// ---- 水平输入(IWBT:瞬时,无惯性)----
|
||||
// ---- 水平输入(IWBT:普通地面瞬时、无惯性;冰面打滑并保留惯性)----
|
||||
const moveL = Input.is('left'), moveR = Input.is('right');
|
||||
const onIce = this.grounded && L.iceAtBottom(this.x, this.y, this.w, this.h);
|
||||
let target = 0;
|
||||
if (moveL && !moveR) target = -PHY.HSPD;
|
||||
else if (moveR && !moveL) target = PHY.HSPD;
|
||||
|
||||
const ICE_MAX = PHY.HSPD * 1.35;
|
||||
const decel = f => {
|
||||
if (Math.abs(this.vx) <= f) this.vx = 0;
|
||||
else this.vx -= sgn(this.vx) * f;
|
||||
};
|
||||
|
||||
if (onIce) {
|
||||
// 冰面:打滑
|
||||
// 冰面:加速/减速都很慢,速度上限也更高
|
||||
if (target !== 0) this.vx += sgn(target) * PHY.ICE_ACC;
|
||||
else {
|
||||
if (Math.abs(this.vx) < PHY.ICE_FRIC) this.vx = 0;
|
||||
else this.vx -= sgn(this.vx) * PHY.ICE_FRIC;
|
||||
else decel(PHY.ICE_FRIC);
|
||||
this.vx = clamp(this.vx, -ICE_MAX, ICE_MAX);
|
||||
// 攒出一定速度即进入「滑行」状态——这是惯性得以保留的关键
|
||||
if (Math.abs(this.vx) > PHY.SLIDE_MIN) this.sliding = true;
|
||||
} else if (this.sliding && Math.abs(this.vx) > PHY.SLIDE_STOP) {
|
||||
// 滑出冰块后:惯性依然保留 —— 空中几乎不衰减,地面才慢慢收住
|
||||
if (target !== 0) {
|
||||
const dir = sgn(this.vx);
|
||||
if (dir === 0 || sgn(target) === dir) {
|
||||
if (Math.abs(this.vx) >= PHY.HSPD) {
|
||||
// 还在超速滑行:同向按键只让它缓慢收敛回正常走速,不再继续加速
|
||||
this.vx -= dir * PHY.SLIDE_EASE;
|
||||
if (Math.abs(this.vx) < PHY.HSPD) { this.vx = target; this.sliding = false; }
|
||||
} else {
|
||||
this.vx = target; // 已低于走速:交还玩家控制
|
||||
this.sliding = false;
|
||||
}
|
||||
} else {
|
||||
this.vx -= dir * PHY.SLIDE_BRAKE; // 反向按键:刹车,刹住后立刻反向
|
||||
if (sgn(this.vx) === sgn(target)) { this.vx = target; this.sliding = false; }
|
||||
}
|
||||
this.vx = clamp(this.vx, -ICE_MAX, ICE_MAX);
|
||||
} else {
|
||||
decel(this.grounded ? PHY.SLIDE_FRIC : PHY.SLIDE_AIR);
|
||||
if (this.vx === 0) this.sliding = false;
|
||||
}
|
||||
this.vx = clamp(this.vx, -PHY.HSPD * 1.35, PHY.HSPD * 1.35);
|
||||
if (this.vx !== 0 && Math.abs(this.vx) < PHY.SLIDE_STOP) { this.vx = 0; this.sliding = false; }
|
||||
} else {
|
||||
// 常规地面/空中(非滑行):IWBT 标准手感,松手即停、无惯性
|
||||
this.vx = target;
|
||||
this.sliding = false;
|
||||
}
|
||||
if (target !== 0) this.facing = sgn(target);
|
||||
|
||||
@@ -194,13 +231,21 @@ export class Player {
|
||||
}
|
||||
this.squash += (1 - this.squash) * 0.22;
|
||||
|
||||
// ---- 滑行反馈:超速滑行(冰上,或刚滑出冰块仍带惯性)时脚下溅起冰屑 ----
|
||||
if (this.sliding && this.grounded && Math.abs(this.vx) > PHY.HSPD && this.animT % 5 === 0) {
|
||||
L.particles.spawn(this.cx - sgn(this.vx) * 5, this.y + this.h - 2, 2, {
|
||||
colors: ['#dff4ff', '#8fd8f0', '#fff'], speed: 1.1, life: 16, size: 2,
|
||||
dir: -Math.PI / 2, spread: 1.7, grav: 0.1,
|
||||
});
|
||||
}
|
||||
|
||||
// ---- 边界 ----
|
||||
this.x = clamp(this.x, 0, L.pxW - this.w);
|
||||
if (this.y > L.pxH + 200) this.kill();
|
||||
if (this.y > L.pxH + 200) { this.kill(); if (this.dead) { this.respawn(); return; } }
|
||||
if (this.y < -300) { this.y = -300; this.vy = 0; }
|
||||
|
||||
// ---- 致死 ----
|
||||
if (L.deadlyRect(this.x, this.y, this.w, this.h)) this.kill();
|
||||
// ---- 致死:同帧立刻复活,画面上不留任何死亡状态 ----
|
||||
if (L.deadlyRect(this.x, this.y, this.w, this.h)) { this.kill(); if (this.dead) { this.respawn(); return; } }
|
||||
|
||||
// ---- 射击 ----
|
||||
if (this.hasGun && Input.hit('shoot') && this.shootCool === 0 && L.bullets.length < 5) {
|
||||
@@ -221,6 +266,9 @@ export class Player {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 兜底:本帧内任何环节致死(如假存档)都立刻复活
|
||||
if (this.dead) this.respawn();
|
||||
}
|
||||
|
||||
shoot() {
|
||||
@@ -254,7 +302,8 @@ export class Player {
|
||||
sfx.save();
|
||||
L.particles.spawn(e.x + e.w / 2, e.y + e.h / 2, 18,
|
||||
{ colors: ['#b06bff', '#fff', '#e5d0ff'], speed: 2.6, life: 30 });
|
||||
if (e.fake) { L.showToast('假的。', 90); this.kill(); }
|
||||
// 假存档:立刻复活回上一个存档点,不播死亡动画
|
||||
if (e.fake) { L.showToast('假的。', 90); this.kill(); if (this.dead) { this.respawn(); return; } }
|
||||
}
|
||||
break;
|
||||
case 'goal':
|
||||
|
||||
@@ -175,8 +175,6 @@ export function drawKid(ctx, p, t) {
|
||||
// 眼睛
|
||||
px(ctx, face > 0 ? w - 6 : 3, 4, 2, 2, '#101018');
|
||||
px(ctx, face > 0 ? w - 3 : 0, 4, 1, 2, '#101018');
|
||||
// 嘴(死亡张嘴)
|
||||
if (p.dead) { px(ctx, face > 0 ? w - 6 : 3, 6, 3, 2, '#5a1a1a'); }
|
||||
ctx.restore();
|
||||
|
||||
// 持枪
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
// 移动端虚拟手柄:左手方向、右手跳跃 + 道具(参考 IWBT Creator 的触屏布局)
|
||||
// 左下方:← → 移动;右下方:大跳键 + 射击/下蹲 道具键 + 暂停/重生
|
||||
import { setTouch, clearTouch } from './input.js';
|
||||
|
||||
// 持续按住型动作(通过 action 状态机驱动)
|
||||
const HELD = ['left', 'right', 'jump', 'down'];
|
||||
|
||||
const api = {
|
||||
pause() {},
|
||||
suicide() {},
|
||||
shoot() {},
|
||||
};
|
||||
|
||||
let visible = false;
|
||||
let root = null;
|
||||
|
||||
function q(sel) { return document.querySelector(sel); }
|
||||
|
||||
function bind(btn) {
|
||||
const action = btn.dataset.touch;
|
||||
if (!action) return;
|
||||
let timer = null;
|
||||
|
||||
const press = (e) => {
|
||||
e.preventDefault();
|
||||
if (btn._pid != null) return; // 已在按住(多指防重入)
|
||||
btn._pid = e.pointerId;
|
||||
try { btn.setPointerCapture(e.pointerId); } catch (_) {}
|
||||
btn.classList.add('on');
|
||||
|
||||
if (action === 'shoot') {
|
||||
// 长按连发(受玩家冷却与是否持枪限制)
|
||||
api.shoot();
|
||||
timer = setInterval(() => api.shoot(), 140);
|
||||
} else if (HELD.includes(action)) {
|
||||
setTouch(action, true);
|
||||
} else if (action === 'pause') {
|
||||
api.pause();
|
||||
} else if (action === 'suicide') {
|
||||
api.suicide();
|
||||
}
|
||||
};
|
||||
|
||||
const release = (e) => {
|
||||
if (btn._pid != null && e.pointerId !== btn._pid) return;
|
||||
btn._pid = null;
|
||||
try { btn.releasePointerCapture(e.pointerId); } catch (_) {}
|
||||
btn.classList.remove('on');
|
||||
if (action === 'shoot') {
|
||||
if (timer) { clearInterval(timer); timer = null; }
|
||||
} else if (HELD.includes(action)) {
|
||||
setTouch(action, false);
|
||||
}
|
||||
};
|
||||
|
||||
btn.addEventListener('pointerdown', press);
|
||||
btn.addEventListener('pointerup', release);
|
||||
btn.addEventListener('pointercancel', release);
|
||||
btn.addEventListener('lostpointercapture', release);
|
||||
btn.addEventListener('contextmenu', (e) => e.preventDefault());
|
||||
}
|
||||
|
||||
export const Touch = {
|
||||
init(handlers) {
|
||||
Object.assign(api, handlers || {});
|
||||
root = q('#touch');
|
||||
root.querySelectorAll('[data-touch]').forEach(bind);
|
||||
// 游戏进行中禁止页面滚动/回弹
|
||||
const stage = q('#stage');
|
||||
if (stage) {
|
||||
stage.addEventListener('touchmove', (e) => { if (visible) e.preventDefault(); }, { passive: false });
|
||||
stage.addEventListener('contextmenu', (e) => { if (visible) e.preventDefault(); });
|
||||
}
|
||||
},
|
||||
|
||||
show() {
|
||||
if (!root) root = q('#touch');
|
||||
if (!root) return;
|
||||
visible = true;
|
||||
root.classList.remove('hidden');
|
||||
},
|
||||
|
||||
hide() {
|
||||
if (!root) root = q('#touch');
|
||||
if (!root) return;
|
||||
visible = false;
|
||||
root.classList.add('hidden');
|
||||
clearTouch(); // 防止按键卡住
|
||||
},
|
||||
|
||||
/** 反映是否持有手枪:无枪时射击键置灰 */
|
||||
setGun(has) {
|
||||
const g = q('#tc-gun');
|
||||
if (g) g.classList.toggle('off', !has);
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user