fix: 优化跳跃和下落手感
This commit is contained in:
1 parent
ada64fe4be
commit
afe2f64b9b
25 files changed
+1197
-188
No files matched your search
+76
-32
@@ -2,12 +2,13 @@
|
||||
import { TILE } from './util.js';
|
||||
import { Save } from './save.js';
|
||||
import { makeCustomLevel } from './levels/index.js';
|
||||
import * as S from './sprites.js';
|
||||
|
||||
const $ = s => document.querySelector(s);
|
||||
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
|
||||
|
||||
const PALETTE = [
|
||||
['?', '查看属性', '#8a8a9a'], ['#', '方块', '#6b6b7e'], ['@', '草地', '#4f9e46'], ['=', '砖块', '#a8543c'], ['M', '金属', '#5a6478'],
|
||||
export const PALETTE = [
|
||||
['#', '方块', '#6b6b7e'], ['@', '草地', '#4f9e46'], ['=', '砖块', '#a8543c'], ['M', '金属', '#5a6478'],
|
||||
['%', '冰面', '#8fd8f0'], ['~', '水', '#2a6fd8'], ['-', '单向台', '#9aa6bd'], ['V', '消失块', '#7a7a8c'],
|
||||
['^', '刺', '#c8c8d8'],
|
||||
['*', '美味水果', '#e03050'], ['S', '出生点', '#3ddc84'], ['G', '存档点', '#b06bff'], ['X', '终点', '#ffc63a'],
|
||||
@@ -42,10 +43,58 @@ const SCHEMA = {
|
||||
|
||||
function charLabel(ch) { const it = PALETTE.find(p => p[0] === ch); return it ? it[1] : ch; }
|
||||
|
||||
// 把某个方块按「游戏内真实外观」画到 ctx 的 (px,py) 处 —— 编辑器所见即所得
|
||||
function drawTileReal(ctx, ch, px, py, props, t) {
|
||||
const P = props || {};
|
||||
switch (ch) {
|
||||
case '#': S.drawBlock(ctx, px, py, 0); break; // 方块
|
||||
case '@': S.drawBlock(ctx, px, py, 1); break; // 草地
|
||||
case '=': S.drawBlock(ctx, px, py, 2); break; // 砖块
|
||||
case 'M': S.drawBlock(ctx, px, py, 3); break; // 金属
|
||||
case '%': S.drawIce(ctx, px, py); break; // 冰面
|
||||
case '~': S.drawWater(ctx, px, py, t); break; // 水
|
||||
case '-': S.drawOneWay(ctx, px, py); break; // 单向台
|
||||
case 'V': S.drawVanish(ctx, px, py, 0, t); break; // 消失块
|
||||
case '^': { // 刺:真实三角形,按道具方向
|
||||
const di = { up: 0, down: 1, left: 2, right: 3 }[P.dir || 'up'] || 0;
|
||||
S.drawSpike(ctx, px, py, di, t);
|
||||
break;
|
||||
}
|
||||
case '*': S.drawKiller(ctx, px, py, t); break; // 美味水果
|
||||
case 'D': S.drawDropSpike(ctx, { x: px, y: py, falling: false }, t); break; // 掉落刺
|
||||
case 'X': S.drawGoal(ctx, { x: px, y: py, w: TILE, h: TILE }, t); break; // 终点
|
||||
case 'G': S.drawSave(ctx, { x: px, y: py, w: TILE, h: TILE, active: false }, t); break; // 存档点
|
||||
case 'P': S.drawGun(ctx, { x: px, y: py }, t); break; // 手枪
|
||||
case 'F': S.drawApple(ctx, { x: px, y: py, w: TILE, h: TILE }, t); break; // 苹果
|
||||
case 'J': S.drawJumpRef(ctx, px, py, t); break; // 跳刷新
|
||||
case 'W': S.drawWarp(ctx, { x: px, y: py, w: TILE, h: TILE }, t); break; // 传送门
|
||||
case 'T': S.drawSign(ctx, { x: px, y: py, w: TILE, h: TILE }, t); break; // 提示牌
|
||||
case 'O': S.drawTurret(ctx, { x: px, y: py, dir: P.dir || 'left' }, t); break; // 炮台
|
||||
case 'C': S.drawCamera(ctx, { x: px, y: py, w: TILE, h: TILE }, t); break; // 摄像头
|
||||
case 'S': S.drawKid(ctx, { // 出生点:画一个小人
|
||||
x: px + (TILE - 11) / 2, y: py + (TILE - 21), w: 11, h: 21,
|
||||
facing: 1, grounded: true, hasGun: false, dead: false,
|
||||
}, t); break;
|
||||
default: drawMarker(ctx, px, py, ch); break; // BOSS / 触发器等无独立美术:色块+字母兜底
|
||||
}
|
||||
}
|
||||
|
||||
// 没有独立 sprite 的方块(BOSS / 触发器等):保留「色块 + 字母」保证可见
|
||||
function drawMarker(ctx, px, py, ch) {
|
||||
const item = PALETTE.find(p => p[0] === ch);
|
||||
const col = item ? item[2] : '#555';
|
||||
ctx.fillStyle = col; ctx.globalAlpha = 0.85;
|
||||
ctx.fillRect(px + 1, py + 1, TILE - 2, TILE - 2);
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = 'bold 14px monospace';
|
||||
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||||
ctx.fillText(ch, px + TILE / 2, py + TILE / 2);
|
||||
}
|
||||
|
||||
// 编辑器中刺统一用 '^',方向存在 props.dir;导出时翻译成引擎的四个字符
|
||||
const SPIKE_CHAR = { up: '^', down: 'v', left: '<', right: '>' };
|
||||
const SPIKE_DIR = { '^': 'up', 'v': 'down', '<': 'left', '>': 'right' };
|
||||
const SPIKE_ARROW = { up: '↑', down: '↓', left: '←', right: '→' };
|
||||
|
||||
export const Editor = {
|
||||
api: {},
|
||||
@@ -70,10 +119,17 @@ export const Editor = {
|
||||
buildPalette() {
|
||||
const p = $('#palette');
|
||||
p.innerHTML = '';
|
||||
PALETTE.forEach(([ch, label, col]) => {
|
||||
PALETTE.forEach(([ch, label]) => {
|
||||
const d = document.createElement('div');
|
||||
d.className = 'pal' + (ch === this.sel ? ' on' : '');
|
||||
d.innerHTML = `<span class="sw" style="background:${col}"></span><span class="ch">${ch === '?' ? '⌖' : ch}</span><span>${label}</span>`;
|
||||
// 用真实绘制函数渲染缩略图,所见即所得
|
||||
const cv = document.createElement('canvas');
|
||||
cv.className = 'pal-cv'; cv.width = TILE; cv.height = TILE;
|
||||
drawTileReal(cv.getContext('2d'), ch, 0, 0, null, 0);
|
||||
d.appendChild(cv);
|
||||
const sp = document.createElement('span');
|
||||
sp.textContent = label;
|
||||
d.appendChild(sp);
|
||||
d.onclick = () => {
|
||||
this.sel = ch;
|
||||
Array.from(p.children).forEach(c => c.classList.remove('on'));
|
||||
@@ -95,13 +151,11 @@ export const Editor = {
|
||||
const f = this.curFace();
|
||||
const { tx, ty } = pos(e);
|
||||
if (tx < 0 || ty < 0 || tx >= f.w || ty >= f.h) return;
|
||||
// 选中该格(属性面板依据它渲染)
|
||||
// 无论手上拿的是什么方块,点击格子都选中它,属性面板立即显示该方块的属性
|
||||
this.selTile = { tx, ty };
|
||||
// 「查看属性」工具:只选中,不修改地图
|
||||
if (this.sel === '?') { this.renderProps(); return; }
|
||||
const prev = f.grid[ty][tx];
|
||||
const ch = erase ? '.' : (this.sel === '.' ? '.' : this.sel);
|
||||
// 放置不覆盖:放实方块时若该格已有方块则跳过(仍选中以便查看属性)
|
||||
// 放置不覆盖:放实方块时若该格已有方块则跳过,仅选中查看属性
|
||||
if (!erase && ch !== '.' && prev !== '.') { this.renderProps(); return; }
|
||||
// 门的增删:放上传送门自动登记一扇门;擦掉或覆盖传送门则移除对应门
|
||||
if (prev === 'W' && ch !== 'W') this.removeDoor(f, tx, ty);
|
||||
@@ -240,34 +294,24 @@ export const Editor = {
|
||||
ctx.fillStyle = '#101018'; ctx.fillRect(0, 0, c.width, c.height);
|
||||
ctx.save();
|
||||
ctx.translate(-this.scroll, 0);
|
||||
const t = 0; // 编辑器为静态预览,不跑动画
|
||||
for (let y = 0; y < f.h; y++) {
|
||||
for (let x = 0; x < f.w; x++) {
|
||||
const ch = f.grid[y][x];
|
||||
const px = x * TILE, py = y * TILE;
|
||||
if (ch !== '.') {
|
||||
let col = '#555';
|
||||
const item = PALETTE.find(p => p[0] === ch);
|
||||
if (item) col = item[2];
|
||||
if (ch === 'W') { const d = f.doors.find(o => o.tx === x && o.ty === y); col = (d && d.to) ? '#3ddc84' : '#8a8a9a'; }
|
||||
ctx.fillStyle = col;
|
||||
ctx.globalAlpha = 0.85;
|
||||
ctx.fillRect(px + 1, py + 1, TILE - 2, TILE - 2);
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
ctx.strokeStyle = 'rgba(255,255,255,.07)';
|
||||
// 真实外观绘制(刺→三角形,方块→纹理,实体→对应精灵)
|
||||
if (ch !== '.') drawTileReal(ctx, ch, px, py, f.props[y][x] || null, t);
|
||||
// 淡网格线(编辑对位用)
|
||||
ctx.strokeStyle = 'rgba(255,255,255,.06)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(px + .5, py + .5, TILE - 1, TILE - 1);
|
||||
if (ch !== '.') {
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = 'bold 14px monospace';
|
||||
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||||
if (ch === 'W') {
|
||||
const d = f.doors.find(o => o.tx === x && o.ty === y);
|
||||
ctx.fillText('W' + (d ? d.id : ''), px + TILE / 2, py + TILE / 2);
|
||||
} else if (ch === '^') {
|
||||
// 刺:用箭头直观显示方向
|
||||
const dir = ((f.props[y][x] || {}).dir) || 'up';
|
||||
ctx.fillText(SPIKE_ARROW[dir] || '↑', px + TILE / 2, py + TILE / 2);
|
||||
} else ctx.fillText(ch, px + TILE / 2, py + TILE / 2);
|
||||
// 传送门:叠加门编号与连接状态(编辑器元数据,游戏内无此显示)
|
||||
if (ch === 'W') {
|
||||
const d = f.doors.find(o => o.tx === x && o.ty === y);
|
||||
ctx.fillStyle = (d && d.to) ? '#bfffd0' : '#ffe6a0';
|
||||
ctx.font = 'bold 9px monospace';
|
||||
ctx.textAlign = 'left'; ctx.textBaseline = 'top';
|
||||
ctx.fillText('W' + (d ? d.id : ''), px + 2, py + 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user