毁灭战士Doom

一个伪3D毁灭战士Doom

适配设备:Pro

作者:rikka

下载量:49 · 点赞:1

可信源码风险提示:保存或下载前请阅读源码和权限声明。
SHA-256c7dbd9dcfac89cca8f62c3bd1e5be622d2cd38dda9682fbf1dcbaba485a7c1f1
const PW = typeof ui.getWidth === 'function' ? (ui.getWidth() || 324) : 324;
const CFG = { ph: 440, q: 32, rot: 'cw', px: 'btn', anim: true, hud: true };
const QS = [24, 32, 48, 64];
const FOV = 0.8, TURN = Math.PI / 18, STEP = 0.45, STRAFE = 0.35, RAD = 0.25;
const GAP = 16;
let LW, LH, HUD_H, CTRL_H, VY, VHL, FOCAL;
function layout() {
  LW = CFG.ph; LH = PW; HUD_H = 22; CTRL_H = 84; VY = HUD_H + 2;
  VHL = LH - VY - CTRL_H - 4; FOCAL = LW / (2 * FOV);
}
layout();
const MAP = [
  '################',
  '#p....#........#',
  '#.....#........#',
  '#.....G........#',
  '#.....#....BBB.#',
  '###.####...B...#',
  '#.....#....B...#',
  '#.....#........#',
  '#.....B.BBB.####',
  '#.....#.......E#',
  '#.....#........#',
  '#.....G........#',
  '#..............#',
  '################',
];
const WALL = { 1: '#b04030', 2: '#3a63b8', 3: '#3f9a50', 4: '#2ee06a' };
const WCODE = { '#': 1, B: 2, G: 3, E: 4 };
const tick = ui.signal(0);
const touch = () => tick.update(n => n + 1);
const buzz = m => { try { system.vibration.vibrate({ mode: m }); } catch (e) {} };
const noop = () => {};
const S = { g: [], px: 0, py: 0, pa: 0, steps: 0, over: null, banner: null, fps: 0, busy: false, report: null };
let A = null, timer = null, B = null;
function newGame() {
  S.g = [];
  MAP.forEach((row, y) => {
    S.g[y] = [];
    for (let x = 0; x < row.length; x++) {
      S.g[y][x] = WCODE[row[x]] || 0;
      if (row[x] === 'p') { S.px = x + 0.5; S.py = y + 0.5; }
    }
  });
  S.pa = 0; S.steps = 0; S.over = null; S.banner = null; S.report = null; A = null;
}
const solid = (x, y) => { const r = S.g[Math.floor(y)]; return !r || r[Math.floor(x)] === undefined || r[Math.floor(x)] > 0; };
const free = (x, y, r) => !solid(x - r, y - r) && !solid(x + r, y - r) && !solid(x - r, y + r) && !solid(x + r, y + r);
function shade(hex, f) {
  const n = parseInt(hex.slice(1), 16);
  const c = k => Math.max(0, Math.min(255, Math.round(((n >> k) & 255) * f)));
  return '#' + ((1 << 24) | (c(16) << 16) | (c(8) << 8) | c(0)).toString(16).slice(1);
}
const FONT = {
  A: '010101111101101', B: '110101110101110', C: '011100100100011', D: '110101101101110', E: '111100110100111',
  F: '111100110100100', G: '011100101101011', H: '101101111101101', I: '111010010010111', J: '001001001101010',
  K: '101101110101101', L: '100100100100111', M: '101111111101101', N: '110101101101101', O: '010101101101010',
  P: '110101110100100', Q: '010101101110011', R: '110101110101101', S: '011100010001110', T: '111010010010010',
  U: '101101101101111', V: '101101101101010', W: '101101111111101', X: '101101010101101', Y: '101101010010010',
  Z: '111001010100111', 0: '111101101101111', 1: '010110010010111', 2: '111001111100111', 3: '111001111001111',
  4: '101101111001001', 5: '111100111001111', 6: '111100111101111', 7: '111001010010010', 8: '111101111101111',
  9: '111101111001111', '!': '010010010000010', '-': '000000111000000',
};
const GL = {};
Object.keys(FONT).forEach(ch => {
  const g = FONT[ch], used = [], out = [];
  for (let r = 0; r < 5; r++) for (let c = 0; c < 3; c++) {
    if (g[r * 3 + c] !== '1' || used[r * 3 + c]) continue;
    let w = 1, h = 1;
    while (c + w < 3 && g[r * 3 + c + w] === '1' && !used[r * 3 + c + w]) w++;
    grow: while (r + h < 5) { for (let k = 0; k < w; k++) if (g[(r + h) * 3 + c + k] !== '1' || used[(r + h) * 3 + c + k]) break grow; h++; }
    for (let a = 0; a < h; a++) for (let b = 0; b < w; b++) used[(r + a) * 3 + c + b] = true;
    out.push([c, r, w, h]);
  }
  GL[ch] = out;
});
function text(add, str, x, y, s, col) {
  String(str).toUpperCase().split('').forEach((ch, i) => {
    (GL[ch] || []).forEach(q => add(x + i * 4 * s + q[0] * s, y + q[1] * s, q[2] * s, q[3] * s, col));
  });
}
const textW = (str, s) => String(str).length * 4 * s - s;
function pose() {
  if (!A) return { px: S.px, py: S.py, pa: S.pa };
  const t = Math.min(1, (Date.now() - A.t0) / A.dur), e = t * t * (3 - 2 * t), f = A.f;
  return { px: f.px + (S.px - f.px) * e, py: f.py + (S.py - f.py) * e, pa: f.pa + (S.pa - f.pa) * e };
}
const begin = () => { A = null; return { px: S.px, py: S.py, pa: S.pa }; };
function go(from, dur) {
  if (!CFG.anim) { touch(); return; }
  A = { f: from, t0: Date.now(), dur, frames: 0 };
  if (timer) return;
  timer = setTimeout(function step() {
    timer = null;
    if (A && Date.now() - A.t0 < A.dur) { touch(); timer = setTimeout(step, GAP); }
    else {
      if (A && A.frames >= 2 && Date.now() - A.t0 >= 150) S.fps = Math.round(A.frames * 1000 / (Date.now() - A.t0));
      A = null; touch();
    }
  }, 0);
}
function frame() {
  const out = [], p = pose(), N = CFG.q, cw = LW / N, hz = VHL / 2;
  const add = id => {
    let k = 0;
    return (x, y, w, h, c) => {
      const x0 = Math.max(0, x), y0 = Math.max(0, y), x1 = Math.min(LW, x + w), y1 = Math.min(LH, y + h);
      out.push({ id: id + '-' + (k++), x: Math.round(x0), y: Math.round(y0), w: Math.max(1, Math.round(x1 - x0)), h: Math.max(1, Math.round(y1 - y0)), c });
    };
  };
  ['#13131a', '#1d1d28', '#292937'].forEach((c, i) => add('c' + i)(0, VY + i * hz / 3, LW, hz / 3 + 1, c));
  ['#2b231a', '#382d21', '#4a3b2c'].forEach((c, i) => add('f' + i)(0, VY + hz + i * (VHL - hz) / 3, LW, (VHL - hz) / 3 + 1, c));
  const dx = Math.cos(p.pa), dy = Math.sin(p.pa), plx = -dy * FOV, ply = dx * FOV;
  for (let i = 0; i < N; i++) {
    const cam = 2 * (i + 0.5) / N - 1, rdx = dx + plx * cam, rdy = dy + ply * cam;
    let mx = Math.floor(p.px), my = Math.floor(p.py);
    const ddx = rdx === 0 ? 1e9 : Math.abs(1 / rdx), ddy = rdy === 0 ? 1e9 : Math.abs(1 / rdy);
    let sx, sy, sdx, sdy;
    if (rdx < 0) { sx = -1; sdx = (p.px - mx) * ddx; } else { sx = 1; sdx = (mx + 1 - p.px) * ddx; }
    if (rdy < 0) { sy = -1; sdy = (p.py - my) * ddy; } else { sy = 1; sdy = (my + 1 - p.py) * ddy; }
    let side = 0, type = 0, guard = 0;
    while (!type && guard++ < 40) {
      if (sdx < sdy) { sdx += ddx; mx += sx; side = 0; } else { sdy += ddy; my += sy; side = 1; }
      type = S.g[my] && S.g[my][mx] !== undefined ? S.g[my][mx] : 1;
    }
    const dist = Math.max(0.1, side === 0 ? sdx - ddx : sdy - ddy);
    const f = Math.max(0.2, 1 - dist / 11) * (side ? 0.72 : 1);
    const lh = FOCAL / dist, top = Math.max(0, hz - lh / 2), bot = Math.min(VHL, hz + lh / 2);
    add('w' + i)(Math.round(i * cw), VY + top, Math.ceil(cw) + 1, Math.max(2, bot - top), shade(WALL[type], f));
  }
  if (CFG.hud) {
    add('hb')(0, 0, LW, HUD_H, '#101014');
    text(add('ha'), '列' + CFG.q, 8, 6, 2, '#8fa0b8');
    text(add('hf'), '帧' + S.fps, 56, 6, 2, S.fps >= 12 ? '#52c41a' : S.fps >= 8 ? '#ffd75e' : '#ff4d4f');
  }
  const b = S.banner && (!S.banner.until || S.banner.until > Date.now()) ? S.banner.text : null;
  if (b) {
    const s = b.length > 9 ? 5 : 7, tw = textW(b, s), bx = Math.round((LW - tw) / 2), by = VY + Math.round(VHL / 2 - 2.5 * s);
    add('ob')(bx - 14, by - 12, tw + 28, 5 * s + 24, '#000000'); add('oc')(bx - 12, by - 10, tw + 24, 5 * s + 20, '#22222c');
    text(add('ot'), b, bx, by, s, '#5cff95');
  }
  if (S.report) {
    const s = 4, h = S.report.length * 7 * s + 16, w = textW('列64 99', s) + 40;
    add('rb')((LW - w) / 2, VY + (VHL - h) / 2, w, h, '#000000');
    S.report.forEach((line, i) => text(add('r' + i), line, (LW - w) / 2 + 20, VY + (VHL - h) / 2 + 8 + i * 7 * s, s, line.charAt(0) === 'B' ? '#5cff95' : '#f4f4f4'));
  }
  return out;
}
function place(x, y, w, h) {
  let x0, x1, y0, y1;
  if (CFG.rot === 'cw') { x0 = LH - (y + h); x1 = LH - y; y0 = x; y1 = x + w; }
  else { x0 = y; x1 = y + h; y0 = LW - (x + w); y1 = LW - x; }
  const a = Math.round(x0), b = Math.round(x1), c = Math.round(y0), d = Math.round(y1);
  return { x: a, y: c, width: Math.max(1, b - a), height: Math.max(1, d - c) };
}
function banner(t, ms) { S.banner = { text: t, until: ms ? Date.now() + ms : 0 }; if (ms) setTimeout(touch, ms + 20); }
function turn(dir) {
  if (S.over || S.busy) return;
  S.report = null; const from = begin(); S.pa += dir * TURN; go(from, 150);
}
function move(f, s) {
  if (S.over || S.busy) return;
  S.report = null;
  const from = begin(), dx = Math.cos(S.pa), dy = Math.sin(S.pa);
  const nx = S.px + dx * f * STEP + -dy * s * STRAFE, ny = S.py + dy * f * STEP + dx * s * STRAFE;
  const at = (x, y) => (S.g[Math.floor(y)] || [])[Math.floor(x)];
  if (f > 0 && at(S.px + dx * (STEP + RAD), S.py + dy * (STEP + RAD)) === 4) {
    S.over = 'win'; banner('关卡通关'); buzz('long'); setTimeout(menu, 1300);
  } else if (free(nx, ny, RAD)) { S.px = nx; S.py = ny; S.steps++; }
  else if (free(nx, S.py, RAD)) { S.px = nx; S.steps++; }
  else if (free(S.px, ny, RAD)) { S.py = ny; S.steps++; }
  go(from, 220);
}
function spin(q, ms) {
  return new Promise(res => {
    CFG.q = q; const t0 = Date.now(); let last = t0, counting = false, c0 = 0;
    B = { frames: 0 };
    (function step() {
      const now = Date.now();
      if (!counting && now - t0 > 400) { counting = true; B.frames = 0; c0 = now; }
      if (counting && now - c0 >= ms) { const fps = B.frames * 1000 / (now - c0); B = null; res(fps); return; }
      S.pa += 0.0035 * (now - last); last = now; touch(); setTimeout(step, GAP);
    })();
  });
}
async function benchmark() {
  if (S.busy) return;
  S.busy = true; S.report = null; A = null;
  const keepQ = CFG.q, keepPa = S.pa, res = [];
  for (const q of QS) res.push({ q, fps: Math.round(await spin(q, 1800)) });
  const ok = res.filter(r => r.fps >= 12), best = ok.length ? ok[ok.length - 1] : res[0];
  CFG.q = best.q; S.pa = keepPa; S.fps = best.fps; saveCfg();
  S.report = res.map(r => '列' + r.q + ' ' + r.fps).concat(['最佳 列' + best.q]);
  S.busy = false; touch();
}
async function menu() {
  const items = [
    { label: '重新开始', value: 'restart' },
    { label: '性能测试(自动选择画质)', value: 'bench' },
    { label: '画质: ' + CFG.q + '列', value: 'q' },
    { label: '渲染单元: ' + (CFG.px === 'btn' ? '按钮' : '文本') + '(可切换)', value: 'px' },
    { label: '动画: ' + (CFG.anim ? '开启' : '关闭'), value: 'anim' },
    { label: '顶部状态栏: ' + (CFG.hud ? '开启' : '关闭'), value: 'hud' },
    { label: '屏幕旋转: ' + (CFG.rot === 'cw' ? '顺时针' : '逆时针'), value: 'rot' },
    { label: '屏幕高度: ' + CFG.ph, value: 'ph' },
    { label: '退出', value: 'quit' }];
  const r = await dialog.select({ title: S.over === 'win' ? '你获胜!' : '菜单', items });
  const v = r && r.action !== 'cancel' ? (Array.isArray(r.value) ? r.value[0] : r.value) : null;
  if (v === 'restart') { newGame(); touch(); }
  else if (v === 'bench') benchmark();
  else if (v === 'q') { CFG.q = QS[(QS.indexOf(CFG.q) + 1) % QS.length]; saveCfg(); touch(); }
  else if (v === 'px') { CFG.px = CFG.px === 'btn' ? 'txt' : 'btn'; saveCfg(); touch(); }
  else if (v === 'anim') { CFG.anim = !CFG.anim; saveCfg(); touch(); }
  else if (v === 'hud') { CFG.hud = !CFG.hud; saveCfg(); touch(); }
  else if (v === 'rot') { CFG.rot = CFG.rot === 'cw' ? 'ccw' : 'cw'; saveCfg(); touch(); }
  else if (v === 'ph') {
    const n = await dialog.number({ title: '屏幕高度(像素)', value: CFG.ph, min: 300, max: 620, decimals: 0 });
    if (n && n.action !== 'cancel') { CFG.ph = Math.round(n.value); layout(); saveCfg(); touch(); }
  } else if (v === 'quit') script.exit();
}
const saveCfg = () => script.data.set('doomLite.cfg', JSON.stringify(CFG));
function node(r) {
  const q = place(r.x, r.y, r.w, r.h), o = { id: r.id, x: q.x, y: q.y, width: q.width, height: q.height, background: r.c, lines: 1 };
  if (CFG.px === 'txt') return ui.text('', o);
  o.radius = 0; o.padding = 0;
  return ui.button('', noop, o);
}
function controls(kids) {
  const bw = Math.floor((LW - 12) / 4), bh = 38, y0 = LH - CTRL_H, X = i => i * (bw + 4);
  const L = CFG.rot === 'cw'
    ? { left: '↑', right: '↓', up: '→', down: '←', sl: '↖', sr: '↙' }
    : { left: '↓', right: '↑', up: '←', down: '→', sl: '↙', sr: '↘' };
  const b = (label, fn, id, col, row, bg, size, rows) => {
    const q = place(X(col), y0 + row * (bh + 4), bw, bh * (rows || 1) + (rows ? 4 : 0));
    kids.push(ui.button(label, fn, { id, x: q.x, y: q.y, width: q.width, height: q.height, lines: 1, size: size || 24, background: bg || '#4a4f58', color: '#f4f4f4', radius: 8 }));
  };
  b(L.left, () => turn(-1), 'tl', 0, 0); b(L.up, () => move(1, 0), 'fw', 1, 0); b(L.right, () => turn(1), 'tr', 2, 0);
  b(L.sl, () => move(0, -1), 'sl', 0, 1, '#363a41'); b(L.down, () => move(-1, 0), 'bk', 1, 1); b(L.sr, () => move(0, 1), 'sr', 2, 1, '#363a41');
  b('=', menu, 'menu', 3, 0, '#34373d', 32, 2);
}
function screen() {
  tick.get();
  if (A) A.frames++;
  if (B) B.frames++;
  const kids = frame().map(node);
  controls(kids);
  return ui.column([ui.stack(kids, { id: 'scene', height: LW })], { id: 'game', gap: 0 });
}
if (typeof __test !== 'undefined') __test.api = { S, CFG, MAP, QS, frame, place, newGame, move, turn, benchmark, layout, screen, pose, LWf: () => LW, LHf: () => LH, VHLf: () => VHL, getB: () => B };
(async function main() {
  newGame();
  ui.showHeader(false);
  ui.setTitle('毁灭战士Doom');
  ui.render(screen);
  try {
    const c = JSON.parse(await script.data.get('doomLite.cfg', '{}'));
    if (c.ph >= 300 && c.ph <= 620) CFG.ph = c.ph;
    if (QS.indexOf(c.q) >= 0) CFG.q = c.q;
    if (c.rot === 'cw' || c.rot === 'ccw') CFG.rot = c.rot;
    if (c.px === 'btn' || c.px === 'txt') CFG.px = c.px;
    if (typeof c.anim === 'boolean') CFG.anim = c.anim;
    if (typeof c.hud === 'boolean') CFG.hud = c.hud;
    layout(); touch();
  } catch (e) {}
})();