fea: 添加设置功能
This commit is contained in:
1 parent
22157d4baa
commit
ada64fe4be
8 files changed
+405
-4
No files matched your search
@@ -9,6 +9,7 @@ import { Save } from './save.js';
|
||||
import { Touch } from './touch.js';
|
||||
import { sfx, unlockAudio, Audio2 } from './audio.js';
|
||||
import { Editor } from './editor.js';
|
||||
import { Settings } from './settings.js';
|
||||
import { clamp } from './util.js';
|
||||
|
||||
const W = 800, H = 608;
|
||||
@@ -298,6 +299,9 @@ UI.init({
|
||||
manage: () => UI.setManage(!UI.manage),
|
||||
help: () => UI.show('scr-help'),
|
||||
editor: toEditor,
|
||||
settings: () => { Settings.enter(); UI.show('scr-settings'); },
|
||||
'set-ok': () => Settings.confirm(),
|
||||
'set-reset': () => Settings.reset(),
|
||||
back: toTitle,
|
||||
resume,
|
||||
restart: () => { restart(); },
|
||||
@@ -321,6 +325,7 @@ addEventListener('keydown', unlock);
|
||||
|
||||
// 移动端虚拟手柄
|
||||
Touch.init({ pause, suicide, shoot });
|
||||
Settings.init();
|
||||
|
||||
UI.show('scr-title');
|
||||
requestAnimationFrame(loop);
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
// 设置:用户名 + 手机虚拟手柄按钮的自定义(位置/大小,拖动调整)
|
||||
import { Save } from './save.js';
|
||||
|
||||
// 每个可自定义的手柄按钮:key 对应 touch.js 里的 data-touch,
|
||||
// def 为默认布局(cx/cy 为相对舞台中心的百分比,size 为按钮边长 px)
|
||||
const BUTTONS = [
|
||||
{ key: 'left', label: '◀', shape: 'round', def: { cx: 12, cy: 80, size: 62 } },
|
||||
{ key: 'right', label: '▶', shape: 'round', def: { cx: 24, cy: 80, size: 62 } },
|
||||
{ key: 'jump', label: '跳', shape: 'jump', def: { cx: 88, cy: 84, size: 94 } },
|
||||
{ key: 'shoot', label: '🔫', shape: 'item', label2: '射击', def: { cx: 79, cy: 72, size: 54 } },
|
||||
{ key: 'down', label: '▼', shape: 'item', label2: '下蹲', def: { cx: 90, cy: 72, size: 54 } },
|
||||
{ key: 'pause', label: '❚❚', shape: 'sm', def: { cx: 81, cy: 58, size: 42 } },
|
||||
{ key: 'suicide', label: '↺', shape: 'sm', def: { cx: 90, cy: 58, size: 42 } },
|
||||
];
|
||||
|
||||
const clone = (o) => JSON.parse(JSON.stringify(o));
|
||||
const DEFAULT_TOUCH = {};
|
||||
BUTTONS.forEach(b => { DEFAULT_TOUCH[b.key] = clone(b.def); });
|
||||
const makeDefaults = () => ({ username: '', touch: clone(DEFAULT_TOUCH) });
|
||||
|
||||
// 工作副本(未点「确定」前不落盘,点「返回」即丢弃)
|
||||
let work = makeDefaults();
|
||||
let wrap = null;
|
||||
const els = {};
|
||||
|
||||
/** 读取/补全存档里的设置对象(保证 touch 字段齐全) */
|
||||
function meta() {
|
||||
if (!Save.data.settings) Save.data.settings = { username: '' };
|
||||
if (!Save.data.settings.touch) Save.data.settings.touch = clone(DEFAULT_TOUCH);
|
||||
return Save.data.settings;
|
||||
}
|
||||
|
||||
/** 把某个按钮按 p={cx,cy,size} 绝对定位(百分比定位 + 像素边长) */
|
||||
function place(el, p) {
|
||||
el.style.left = p.cx + '%';
|
||||
el.style.top = p.cy + '%';
|
||||
el.style.width = p.size + 'px';
|
||||
el.style.height = p.size + 'px';
|
||||
}
|
||||
|
||||
/** 把当前存档布局应用到真正的 #touch 手柄按钮(游戏内生效) */
|
||||
function apply() {
|
||||
const t = meta().touch;
|
||||
for (const b of BUTTONS) {
|
||||
const el = document.querySelector(`#touch [data-touch="${b.key}"]`);
|
||||
if (!el) continue;
|
||||
const p = t[b.key];
|
||||
if (!p) continue;
|
||||
el.style.left = p.cx + '%';
|
||||
el.style.top = p.cy + '%';
|
||||
el.style.width = p.size + 'px';
|
||||
el.style.height = p.size + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
/** 标题界面显示当前用户名 */
|
||||
function showUsername() {
|
||||
const el = document.getElementById('title-user');
|
||||
if (el) el.textContent = '玩家:' + (meta().username || '无名氏');
|
||||
}
|
||||
|
||||
export const Settings = {
|
||||
init() {
|
||||
wrap = document.getElementById('touch-preview');
|
||||
work = clone(meta());
|
||||
this.build();
|
||||
showUsername();
|
||||
apply();
|
||||
},
|
||||
|
||||
/** 构建设置界面里的可拖拽手柄预览(克隆 #touch 的按钮形状) */
|
||||
build() {
|
||||
if (!wrap) return;
|
||||
wrap.querySelectorAll('.tc-btn').forEach(n => n.remove());
|
||||
for (const b of BUTTONS) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'tc-btn tc-' + b.shape;
|
||||
el.innerHTML = `<span>${b.label}</span>` + (b.label2 ? `<span class="lbl">${b.label2}</span>` : '');
|
||||
const h = document.createElement('div');
|
||||
h.className = 'tc-rsz';
|
||||
el.appendChild(h);
|
||||
wrap.appendChild(el);
|
||||
els[b.key] = el;
|
||||
place(el, work.touch[b.key]);
|
||||
wire(el, h, b.key);
|
||||
}
|
||||
},
|
||||
|
||||
/** 打开设置界面:从存档同步工作副本与输入框 */
|
||||
enter() {
|
||||
const s = meta();
|
||||
work = { username: s.username || '', touch: clone(s.touch) };
|
||||
const inp = document.getElementById('set-username');
|
||||
if (inp) inp.value = work.username;
|
||||
this.build();
|
||||
},
|
||||
|
||||
/** 确定:保存用户名与手柄布局到存档 */
|
||||
confirm() {
|
||||
const s = meta();
|
||||
const inp = document.getElementById('set-username');
|
||||
work.username = (inp ? inp.value : '').trim().slice(0, 16);
|
||||
s.username = work.username;
|
||||
s.touch = clone(work.touch);
|
||||
Save.flush();
|
||||
showUsername();
|
||||
apply();
|
||||
},
|
||||
|
||||
/** 恢复默认:布局与用户名全部回到出厂值(仅影响工作副本,需点确定才落盘) */
|
||||
reset() {
|
||||
work = makeDefaults();
|
||||
const inp = document.getElementById('set-username');
|
||||
if (inp) inp.value = '';
|
||||
this.build();
|
||||
},
|
||||
|
||||
/** 供调试 / 测试使用 */
|
||||
_state() { return work; },
|
||||
_els() { return els; },
|
||||
};
|
||||
|
||||
/** 给单个预览按钮绑定「拖动移动」与「右下角缩放」 */
|
||||
function wire(el, handle, key) {
|
||||
let mode = null; // 'move' | 'resize'
|
||||
let rect = null; // 预览区矩形(按下时记录)
|
||||
let lastX = 0, lastY = 0; // 上一帧指针(相对位移用)
|
||||
let startSize = 0, startDist = 1, cx0 = 0, cy0 = 0; // 缩放用:按下时按钮中心与指针距离
|
||||
|
||||
// 按下:只记录基准,不重算绝对坐标 —— 避免任何「按下即跳角」
|
||||
const begin = (e, m) => {
|
||||
e.preventDefault();
|
||||
try { (m === 'resize' ? handle : el).setPointerCapture(e.pointerId); } catch (_) { }
|
||||
mode = m;
|
||||
rect = wrap.getBoundingClientRect();
|
||||
const W = rect.width || 1, H = rect.height || 1;
|
||||
const p = work.touch[key];
|
||||
lastX = e.clientX; lastY = e.clientY;
|
||||
if (m === 'resize') {
|
||||
cx0 = rect.left + p.cx / 100 * W;
|
||||
cy0 = rect.top + p.cy / 100 * H;
|
||||
startSize = p.size;
|
||||
startDist = Math.max(1, Math.hypot(e.clientX - cx0, e.clientY - cy0));
|
||||
}
|
||||
};
|
||||
|
||||
// 移动:用「相对上一帧的位移」累加,第一帧必然停在正确位置
|
||||
const drag = (e) => {
|
||||
if (!mode || !rect) return;
|
||||
const p = work.touch[key];
|
||||
if (mode === 'move') {
|
||||
const W = rect.width || 1, H = rect.height || 1;
|
||||
const dx = (e.clientX - lastX) / W * 100; // 像素位移 → 百分比
|
||||
const dy = (e.clientY - lastY) / H * 100;
|
||||
lastX = e.clientX; lastY = e.clientY;
|
||||
p.cx = Math.max(3, Math.min(97, p.cx + dx));
|
||||
p.cy = Math.max(3, Math.min(97, p.cy + dy));
|
||||
} else {
|
||||
const d = Math.max(1, Math.hypot(e.clientX - cx0, e.clientY - cy0));
|
||||
p.size = Math.max(24, Math.min(160, startSize * (d / startDist)));
|
||||
}
|
||||
place(el, p);
|
||||
};
|
||||
|
||||
const end = (e) => {
|
||||
if (mode) { try { (mode === 'resize' ? handle : el).releasePointerCapture(e.pointerId); } catch (_) { } }
|
||||
mode = null;
|
||||
};
|
||||
|
||||
el.addEventListener('pointerdown', (e) => { if (e.target === handle) return; begin(e, 'move'); });
|
||||
handle.addEventListener('pointerdown', (e) => { e.stopPropagation(); begin(e, 'resize'); });
|
||||
el.addEventListener('pointermove', drag);
|
||||
handle.addEventListener('pointermove', drag);
|
||||
el.addEventListener('pointerup', end);
|
||||
handle.addEventListener('pointerup', end);
|
||||
el.addEventListener('pointercancel', end);
|
||||
handle.addEventListener('pointercancel', end);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// 移动端虚拟手柄:左手方向、右手跳跃 + 道具(参考 IWBT Creator 的触屏布局)
|
||||
// 左下方:← → 移动;右下方:大跳键 + 射击/下蹲 道具键 + 暂停/重生
|
||||
import { setTouch, clearTouch } from './input.js';
|
||||
import { Settings } from './settings.js';
|
||||
|
||||
// 持续按住型动作(通过 action 状态机驱动)
|
||||
const HELD = ['left', 'right', 'jump', 'down'];
|
||||
@@ -77,6 +78,7 @@ export const Touch = {
|
||||
if (!root) root = q('#touch');
|
||||
if (!root) return;
|
||||
visible = true;
|
||||
Settings.apply(); // 应用用户自定义的手柄布局
|
||||
root.classList.remove('hidden');
|
||||
},
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { fmtTime } from './util.js';
|
||||
const $ = s => document.querySelector(s);
|
||||
const $$ = s => Array.from(document.querySelectorAll(s));
|
||||
|
||||
const SCREENS = ['scr-title', 'scr-levels', 'scr-help', 'scr-pause', 'scr-result', 'scr-editor'];
|
||||
const SCREENS = ['scr-title', 'scr-levels', 'scr-help', 'scr-settings', 'scr-pause', 'scr-result', 'scr-editor'];
|
||||
const TABS = [
|
||||
['all', '全部'], ['jump', '跳刺'], ['endur', '耐久'], ['boss', 'BOSS'], ['troll', '坑向'], ['custom', '自制'],
|
||||
];
|
||||
|
||||
Reference in new issue
Block a user