HumanBreak/packages-user/client-base/src/material/autotile.ts

455 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
IRect,
ITextureRenderable,
SizedCanvasImageSource
} from '@motajs/render-assets';
import {
AutotileConnection,
AutotileType,
BlockCls,
IAutotileConnection,
IAutotileProcessor,
IMaterialFramedData,
IMaterialManager
} from './types';
import { isNil } from 'lodash-es';
interface ConnectedAutotile {
readonly lt: Readonly<IRect>;
readonly rt: Readonly<IRect>;
readonly rb: Readonly<IRect>;
readonly lb: Readonly<IRect>;
}
export interface IAutotileData {
/** 图像源 */
readonly source: SizedCanvasImageSource;
/** 自动元件帧数 */
readonly frames: number;
}
/** 3x4 自动元件的连接映射,元组表示将对应大小的自动元件按照格子 1/4 大小切分后对应的索引位置 */
const connectionMap3x4 = new Map<number, [number, number, number, number]>();
/** 2x3 自动元件的连接映射,元组表示将对应大小的自动元件按照格子 1/4 大小切分后对应的索引位置 */
const connectionMap2x3 = new Map<number, [number, number, number, number]>();
/** 3x4 自动元件各方向连接的矩形映射 */
const rectMap3x4 = new Map<number, ConnectedAutotile>();
/** 2x3 自动元件各方向连接的矩形映射 */
const rectMap2x3 = new Map<number, ConnectedAutotile>();
/** 不重复连接映射,用于平铺自动元件,一共 48 种 */
const distinctConnectionMap = new Map<number, number>();
export class AutotileProcessor implements IAutotileProcessor {
/** 自动元件父子关系映射,子元件 -> 父元件 */
readonly parentMap: Map<number, number> = new Map();
/** 自动元件父子关系映射,父元件 -> 子元件列表 */
readonly childMap: Map<number, Set<number>> = new Map();
constructor(readonly manager: IMaterialManager) {}
private ensureChildSet(num: number) {
const set = this.childMap.get(num);
if (set) return set;
const ensure = new Set<number>();
this.childMap.set(num, ensure);
return ensure;
}
setConnection(autotile: number, parent: number): void {
this.parentMap.set(autotile, parent);
const child = this.ensureChildSet(parent);
child.add(autotile);
}
private connectEdge(length: number, index: number, width: number): number {
// 最高位表示左上,低位依次顺时针旋转
// 如果地图大小只有 1
if (length === 1) {
return 0b1111_1111;
}
// 如果地图高度只有 1
if (length === width) {
if (index === 0) {
return 0b1110_1111;
} else if (index === length - 1) {
return 0b1111_1110;
} else {
return 0b1110_1110;
}
}
// 如果地图宽度只有 1
if (width === 1) {
if (index === 0) {
return 0b1111_1011;
} else if (index === length - 1) {
return 0b1011_1111;
} else {
return 0b1011_1011;
}
}
// 正常地图
const lastLine = length - width;
const x = index % width;
// 四个角,左上,右上,右下,左下
if (index === 0) {
return 0b1110_0011;
} else if (index === width - 1) {
return 0b1111_1000;
} else if (index === length - 1) {
return 0b0011_1110;
} else if (index === lastLine) {
return 0b1000_1111;
}
// 四条边,上,右,下,左
else if (index < width) {
return 0b1110_0000;
} else if (x === width - 1) {
return 0b0011_1000;
} else if (index > lastLine) {
return 0b0000_1110;
} else if (x === 0) {
return 0b1000_0011;
}
// 不在边缘
else {
return 0b0000_0000;
}
}
connect(
array: Uint32Array,
index: number,
width: number
): IAutotileConnection {
const block = array[index];
if (block === 0) {
return {
connection: 0,
center: 0
};
}
let res: number = this.connectEdge(array.length, index, width);
const childList = this.childMap.get(block);
// 最高位表示左上,低位依次顺时针旋转
const a7 = array[index - width - 1] ?? 0;
const a6 = array[index - width] ?? 0;
const a5 = array[index - width + 1] ?? 0;
const a4 = array[index + 1] ?? 0;
const a3 = array[index + width + 1] ?? 0;
const a2 = array[index + width] ?? 0;
const a1 = array[index + width - 1] ?? 0;
const a0 = array[index - 1] ?? 0;
// Benchmark https://www.measurethat.net/Benchmarks/Show/35271/0/convert-boolean-to-number
if (!childList || childList.size === 0) {
// 不包含子元件,那么直接跟相同的连接
res |=
+(a0 === block) |
(+(a1 === block) << 1) |
(+(a2 === block) << 2) |
(+(a3 === block) << 3) |
(+(a4 === block) << 4) |
(+(a5 === block) << 5) |
(+(a6 === block) << 6) |
(+(a7 === block) << 7);
} else {
res |=
+childList.has(a0) |
(+childList.has(a1) << 1) |
(+childList.has(a2) << 2) |
(+childList.has(a3) << 3) |
(+childList.has(a4) << 4) |
(+childList.has(a5) << 5) |
(+childList.has(a6) << 6) |
(+childList.has(a7) << 7);
}
return {
connection: res,
center: block
};
}
updateConnectionFor(
connection: number,
center: number,
target: number,
direction: AutotileConnection
): number {
const childList = this.childMap.get(center);
if (!childList || !childList.has(target)) {
return connection & ~direction;
} else {
return connection | direction;
}
}
/**
* 检查贴图是否是一个自动元件
* @param tile 贴图数据
*/
private checkAutotile(tile: IMaterialFramedData) {
if (tile.cls !== BlockCls.Autotile) return false;
const { texture, frames } = tile;
if (texture.width !== 96 * frames) return false;
if (texture.height === 128 || texture.height === 144) return true;
else return false;
}
render(autotile: number, connection: number): ITextureRenderable | null {
const tile = this.manager.getTile(autotile);
if (!tile) return null;
if (!this.checkAutotile(tile)) return null;
return this.renderWithoutCheck(tile, connection);
}
renderWith(
tile: IMaterialFramedData,
connection: number
): ITextureRenderable | null {
if (!this.checkAutotile(tile)) return null;
return this.renderWithoutCheck(tile, connection);
}
renderWithoutCheck(
tile: IMaterialFramedData,
connection: number
): ITextureRenderable | null {
const { texture } = tile;
const size = texture.height === 32 * 48 ? 32 : 48;
const index = distinctConnectionMap.get(connection);
if (isNil(index)) return null;
const { rect } = texture.render();
return {
source: texture.source,
rect: { x: rect.x, y: rect.y + size * index, w: size, h: size }
};
}
*renderAnimated(
autotile: number,
connection: number
): Generator<ITextureRenderable, void> {
const tile = this.manager.getTile(autotile);
if (!tile) return;
yield* this.renderAnimatedWith(tile, connection);
}
*renderAnimatedWith(
tile: IMaterialFramedData,
connection: number
): Generator<ITextureRenderable, void> {
if (!this.checkAutotile(tile)) return;
const { texture, frames } = tile;
const size = texture.height === 128 ? 32 : 48;
const index = distinctConnectionMap.get(connection);
if (isNil(index)) return;
for (let i = 0; i < frames; i++) {
yield {
source: texture.source,
rect: { x: i * size, y: size * index, w: size, h: size }
};
}
}
/**
* 将自动元件图片展平,平铺存储 48 种样式,此时可以只通过一次绘制来绘制出自动元件,不需要四次绘制
* @param image 原始自动元件图片
*/
static flatten(image: IAutotileData): SizedCanvasImageSource | null {
const { source, frames } = image;
if (source.width !== frames * 96) return null;
if (source.height !== 128 && source.height !== 144) return null;
const type =
source.height === 128 ? AutotileType.Big3x4 : AutotileType.Small2x3;
const size = type === AutotileType.Big3x4 ? 32 : 48;
const width = frames * size;
const height = 48 * size;
// 画到画布上
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) return null;
const half = size / 2;
const map = type === AutotileType.Big3x4 ? rectMap3x4 : rectMap2x3;
const used = new Set<number>();
// 遍历每个组合
distinctConnectionMap.forEach((index, conn) => {
if (used.has(conn)) return;
used.add(conn);
const { lt, rt, rb, lb } = map.get(conn)!;
const y = index * size;
for (let i = 0; i < frames; i++) {
const x = i * size;
// prettier-ignore
ctx.drawImage(source, lt.x + i * 96, lt.y, lt.w, lt.h, x, y, half, half);
// prettier-ignore
ctx.drawImage(source, rt.x + i * 96, rt.y, rt.w, rt.h, x + half, y, half, half);
// prettier-ignore
ctx.drawImage(source, rb.x + i * 96, rb.y, rb.w, rb.h, x + half, y + half, half, half);
// prettier-ignore
ctx.drawImage(source, lb.x + i * 96, lb.y, lb.w, lb.h, x, y + half, half, half);
}
});
return canvas;
}
}
/**
* 映射自动元件连接
* @param target 输出映射对象
* @param mode 自动元件类型1 表示 3x42 表示 2x3
*/
function mapAutotile(
target: Map<number, [number, number, number, number]>,
mode: 1 | 2
) {
const h = mode === 1 ? 2 : 1; // 横向偏移因子
const v = mode === 1 ? 12 : 4; // 纵向偏移因子
const luo = mode === 1 ? 12 : 8; // leftup origin
const ruo = mode === 1 ? 17 : 11; // rightup origin
const ldo = mode === 1 ? 42 : 20; // leftdown origin
const rdo = mode === 1 ? 47 : 23; // rightdown origin
const luc = mode === 1 ? 4 : 2; // leftup corner
const ruc = mode === 1 ? 5 : 3; // rightup corner
const rdc = mode === 1 ? 11 : 7; // rightdown corner
const ldc = mode === 1 ? 10 : 6; // leftdown corner
for (let i = 0; i <= 0b1111_1111; i++) {
// 自动元件由四个更小的矩形组合而成
// 初始状态下,四个矩形分别处在四个角的位置
// 而且对应角落的矩形只可能出现在每个大区块的对应角落
let lu = luo; // leftup
let ru = ruo; // rightup
let ld = ldo; // leftdown
let rd = rdo; // rightdown
// 先看四个方向,最后看斜角方向
if (i & 0b0000_0001) {
// 左侧有连接,左侧两个矩形向右偏移两个因子
lu += h * 2;
ld += h * 2;
// 如果右侧还有连接,那么右侧矩形和左侧矩形需要移动至中间
// 但是由于后面还处理了先右侧再左侧的情况,因此需要先向右偏移一个因子
// 结果就是先向右移动了一个因子,在后面又向左移动了两个因子,因此相当于向左移动了一个因子
if (i & 0b0001_0000) {
ru += h;
rd += h;
}
}
if (i & 0b0000_0100) {
// 下侧有连接,下侧两个矩形向上偏移两个因子
ld -= v * 2;
rd -= v * 2;
if (i & 0b0100_0000) {
lu -= v;
ru -= v;
}
}
if (i & 0b0001_0000) {
// 右侧有连接,右侧矩形向左移动两个因子
ru -= h * 2;
rd -= h * 2;
if (i & 0b0000_0001) {
lu -= h;
ld -= h;
}
}
if (i & 0b0100_0000) {
// 上侧有链接,上侧矩形向下移动两个因子
lu += v * 2;
ru += v * 2;
if (i & 0b0000_0100) {
ld += v;
rd += v;
}
}
// 斜角
// 如果左上仅与上和左连接
if ((i & 0b1100_0001) === 0b0100_0001) {
lu = luc;
}
// 如果右上仅与上和右连接
if ((i & 0b0111_0000) === 0b0101_0000) {
ru = ruc;
}
// 如果右下仅与右和下连接
if ((i & 0b0001_1100) === 0b0001_0100) {
rd = rdc;
}
// 如果左下仅与左和下连接
if ((i & 0b0000_0111) === 0b0000_0101) {
ld = ldc;
}
target.set(i, [lu, ru, rd, ld]);
}
}
export function createAutotile() {
mapAutotile(connectionMap3x4, 1);
mapAutotile(connectionMap2x3, 2);
connectionMap3x4.forEach((data, connection) => {
const [ltd, rtd, rbd, lbd] = data;
const ltx = (ltd % 6) * 16;
const lty = Math.floor(ltd / 6) * 16;
const rtx = (rtd % 6) * 16;
const rty = Math.floor(rtd / 6) * 16;
const rbx = (rbd % 6) * 16;
const rby = Math.floor(rbd / 6) * 16;
const lbx = (lbd % 6) * 16;
const lby = Math.floor(lbd / 6) * 16;
rectMap3x4.set(connection, {
lt: { x: ltx, y: lty, w: 16, h: 16 },
rt: { x: rtx, y: rty, w: 16, h: 16 },
rb: { x: rbx, y: rby, w: 16, h: 16 },
lb: { x: lbx, y: lby, w: 16, h: 16 }
});
});
connectionMap2x3.forEach((data, connection) => {
const [ltd, rtd, rbd, lbd] = data;
const ltx = (ltd % 4) * 24;
const lty = Math.floor(ltd / 4) * 24;
const rtx = (rtd % 4) * 24;
const rty = Math.floor(rtd / 4) * 24;
const rbx = (rbd % 4) * 24;
const rby = Math.floor(rbd / 4) * 24;
const lbx = (lbd % 4) * 24;
const lby = Math.floor(lbd / 4) * 24;
rectMap2x3.set(connection, {
lt: { x: ltx, y: lty, w: 24, h: 24 },
rt: { x: rtx, y: rty, w: 24, h: 24 },
rb: { x: rbx, y: rby, w: 24, h: 24 },
lb: { x: lbx, y: lby, w: 24, h: 24 }
});
});
const usedRect: [number, number, number, number][] = [];
let flag = 0;
// 2x3 和 3x4 的自动元件连接方式一样,因此没必要映射两次
connectionMap2x3.forEach((conn, num) => {
const index = usedRect.findIndex(
used =>
used[0] === conn[0] &&
used[1] === conn[1] &&
used[2] === conn[2] &&
used[3] === conn[3]
);
if (index === -1) {
distinctConnectionMap.set(num, flag);
usedRect.push(conn.slice() as [number, number, number, number]);
flag++;
} else {
distinctConnectionMap.set(num, index);
}
});
}