-
-
-
请稍候...
-
-
-
-
资源即将开始加载
-
HTML5魔塔游戏平台,享受更多魔塔游戏:
https://h5mota.com/
-
-
-
-
-
-
![]()
-
-
-
-
![]()
-
-
-
-
![]()
-
-
-
-
![]()
-
-
-
-
![]()
-
-
-
-
![]()
-
-
-
-
![]()
-
-
-
-
![]()
-
-
-
-
![]()
-
-
-
-
![]()
-
-
-
-
![]()
-
-
-
-
![]()
-
-
-
-
![]()
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
diff --git a/mota.config.ts b/mota.config.ts
deleted file mode 100644
index fdfa1a1..0000000
--- a/mota.config.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-interface MotaConfig {
- name: string;
- /** 资源分组打包信息 */
- resourceZip?: string[][];
- resourceName?: string;
-}
-
-function defineConfig(config: MotaConfig): MotaConfig {
- return config;
-}
-
-export default defineConfig({
- // 这里修改塔的name,请保持与全塔属性的完全相同,否则发布之后可能无法进行游玩
- name: 'HumanBreak',
- resourceName: 'HumanBreakRes'
-});
diff --git a/package.json b/package.json
index 000963d..ba976b3 100644
--- a/package.json
+++ b/package.json
@@ -1,63 +1,94 @@
{
- "name": "mota-ts",
- "private": true,
- "version": "1.0.0",
- "type": "module",
- "scripts": {
- "dev": "ts-node-esm script/dev.ts",
- "build": "vue-tsc && vite build && ts-node-esm script/build.ts 0 1 1",
- "build-gh": "vue-tsc && vite build --base=/HumanBreak/ && ts-node-esm script/build.ts 1",
- "build-local": "vue-tsc && vite build --base=/ && ts-node-esm script/build.ts 1",
- "preview": "vite preview",
- "update": "ts-node-esm script/update.ts",
- "declare": "ts-node-esm script/declare.ts",
- "type": "vue-tsc --noEmit",
- "lines": "ts-node-esm script/lines.ts"
- },
- "dependencies": {
- "@ant-design/icons-vue": "^6.1.0",
- "ant-design-vue": "^3.2.20",
- "axios": "^1.4.0",
- "chart.js": "^4.3.0",
- "jszip": "^3.10.1",
- "lodash-es": "^4.17.21",
- "lz-string": "^1.5.0",
- "mutate-animate": "^1.1.1",
- "three": "^0.149.0",
- "vue": "^3.3.4"
- },
- "devDependencies": {
- "@babel/cli": "^7.21.5",
- "@babel/core": "^7.21.8",
- "@babel/preset-env": "^7.21.5",
- "@rollup/plugin-babel": "^6.0.3",
- "@rollup/plugin-commonjs": "^25.0.0",
- "@rollup/plugin-node-resolve": "^15.0.2",
- "@rollup/plugin-replace": "^5.0.2",
- "@rollup/plugin-terser": "^0.4.3",
- "@rollup/plugin-typescript": "^11.1.1",
- "@types/babel__core": "^7.20.0",
- "@types/fontmin": "^0.9.0",
- "@types/fs-extra": "^9.0.13",
- "@types/lodash-es": "^4.17.7",
- "@types/node": "^18.16.14",
- "@types/ws": "^8.5.4",
- "@vitejs/plugin-legacy": "^4.0.3",
- "@vitejs/plugin-vue": "^4.2.3",
- "@vitejs/plugin-vue-jsx": "^3.0.1",
- "chokidar": "^3.5.3",
- "compressing": "^1.9.0",
- "fontmin": "^0.9.9",
- "form-data": "^4.0.0",
- "fs-extra": "^10.1.0",
- "less": "^4.1.3",
- "rollup": "^3.23.0",
- "terser": "^5.17.6",
- "ts-node": "^10.9.1",
- "typescript": "^4.9.5",
- "unplugin-vue-components": "^0.22.12",
- "vite": "^4.3.8",
- "vue-tsc": "^1.6.5",
- "ws": "^8.13.0"
- }
-}
\ No newline at end of file
+ "name": "mota-ts",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "tsx script/dev.ts",
+ "preview": "vite preview",
+ "declare": "tsx script/declare.ts",
+ "type": "vue-tsc --noEmit",
+ "lines": "tsx script/lines.ts packages packages-user",
+ "build:packages": "vue-tsc --noEmit && tsx script/build-packages.ts",
+ "build:game": "tsx script/declare.ts && vue-tsc --noEmit && tsx script/build-game.ts",
+ "build:lib": "vue-tsc --noEmit && tsx script/build-lib.ts",
+ "docs:dev": "concurrently -k -n SIDEBAR,VITEPRESS -c blue,green \"tsx docs/.vitepress/api.ts\" \"vitepress dev docs\"",
+ "docs:build": "vitepress build docs",
+ "docs:preview": "vitepress preview docs"
+ },
+ "dependencies": {
+ "@ant-design/icons-vue": "^6.1.0",
+ "@wasm-audio-decoders/ogg-vorbis": "^0.1.16",
+ "anon-tokyo": "0.0.0-alpha.0",
+ "ant-design-vue": "^3.2.20",
+ "axios": "^1.8.4",
+ "chart.js": "^4.4.8",
+ "codec-parser": "^2.5.0",
+ "eventemitter3": "^5.0.1",
+ "gl-matrix": "^3.4.3",
+ "jszip": "^3.10.1",
+ "lodash-es": "^4.17.21",
+ "lz-string": "^1.5.0",
+ "maxrects-packer": "^2.7.3",
+ "mutate-animate": "^1.4.2",
+ "ogg-opus-decoder": "^1.6.14",
+ "opus-decoder": "^0.7.7",
+ "vue": "^3.5.20"
+ },
+ "devDependencies": {
+ "@babel/cli": "^7.26.4",
+ "@babel/core": "^7.26.10",
+ "@babel/preset-env": "^7.26.9",
+ "@eslint/js": "^9.24.0",
+ "@rollup/plugin-babel": "^6.0.4",
+ "@rollup/plugin-commonjs": "^25.0.8",
+ "@rollup/plugin-json": "^6.1.0",
+ "@rollup/plugin-node-resolve": "^15.3.1",
+ "@rollup/plugin-replace": "^5.0.7",
+ "@rollup/plugin-terser": "^0.4.4",
+ "@rollup/plugin-typescript": "^11.1.6",
+ "@types/archiver": "^6.0.3",
+ "@types/babel__core": "^7.20.5",
+ "@types/express": "^5.0.3",
+ "@types/fontmin": "^0.9.5",
+ "@types/fs-extra": "^11.0.4",
+ "@types/lodash-es": "^4.17.12",
+ "@types/node": "^22.18.0",
+ "@types/ws": "^8.18.0",
+ "@vitejs/plugin-legacy": "^7.2.1",
+ "@vitejs/plugin-vue": "^6.0.1",
+ "@vitejs/plugin-vue-jsx": "^5.1.1",
+ "archiver": "^7.0.1",
+ "chokidar": "^3.6.0",
+ "compressing": "^1.10.1",
+ "concurrently": "^9.1.2",
+ "eslint": "^9.22.0",
+ "eslint-config-prettier": "^10.1.8",
+ "eslint-plugin-prettier": "^5.5.4",
+ "eslint-plugin-react": "^7.37.5",
+ "eslint-plugin-vue": "^9.33.0",
+ "express": "^5.1.0",
+ "fontmin": "^2.0.3",
+ "form-data": "^4.0.2",
+ "fs-extra": "^11.3.1",
+ "glob": "^11.0.1",
+ "globals": "^15.15.0",
+ "less": "^4.2.2",
+ "madge": "^8.0.0",
+ "markdown-it-mathjax3": "^4.3.2",
+ "mermaid": "^11.5.0",
+ "postcss-preset-env": "^9.6.0",
+ "prettier": "^3.6.2",
+ "rollup": "^4.49.0",
+ "terser": "^5.39.0",
+ "tsx": "^4.20.5",
+ "typescript": "^5.9.2",
+ "typescript-eslint": "^8.27.0",
+ "vite": "^7.0.0",
+ "vite-plugin-dts": "^4.5.4",
+ "vitepress": "^1.6.3",
+ "vitepress-plugin-mermaid": "^2.0.17",
+ "vue-tsc": "^2.2.8",
+ "ws": "^8.18.1"
+ }
+}
diff --git a/packages-user/client-base/package.json b/packages-user/client-base/package.json
new file mode 100644
index 0000000..44b79b9
--- /dev/null
+++ b/packages-user/client-base/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@user/client-base",
+ "dependencies": {
+ "@motajs/render-asset": "workspace:*",
+ "@motajs/client-base": "workspace:*"
+ }
+}
diff --git a/packages-user/client-base/src/index.ts b/packages-user/client-base/src/index.ts
new file mode 100644
index 0000000..ddf0bf0
--- /dev/null
+++ b/packages-user/client-base/src/index.ts
@@ -0,0 +1,7 @@
+import { createMaterial } from './material';
+
+export function create() {
+ createMaterial();
+}
+
+export * from './material';
diff --git a/packages-user/client-base/src/material/autotile.ts b/packages-user/client-base/src/material/autotile.ts
new file mode 100644
index 0000000..7d691f9
--- /dev/null
+++ b/packages-user/client-base/src/material/autotile.ts
@@ -0,0 +1,454 @@
+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
;
+ readonly rt: Readonly;
+ readonly rb: Readonly;
+ readonly lb: Readonly;
+}
+
+export interface IAutotileData {
+ /** 图像源 */
+ readonly source: SizedCanvasImageSource;
+ /** 自动元件帧数 */
+ readonly frames: number;
+}
+
+/** 3x4 自动元件的连接映射,元组表示将对应大小的自动元件按照格子 1/4 大小切分后对应的索引位置 */
+const connectionMap3x4 = new Map();
+/** 2x3 自动元件的连接映射,元组表示将对应大小的自动元件按照格子 1/4 大小切分后对应的索引位置 */
+const connectionMap2x3 = new Map();
+/** 3x4 自动元件各方向连接的矩形映射 */
+const rectMap3x4 = new Map();
+/** 2x3 自动元件各方向连接的矩形映射 */
+const rectMap2x3 = new Map();
+/** 不重复连接映射,用于平铺自动元件,一共 48 种 */
+const distinctConnectionMap = new Map();
+
+export class AutotileProcessor implements IAutotileProcessor {
+ /** 自动元件父子关系映射,子元件 -> 父元件 */
+ readonly parentMap: Map = new Map();
+ /** 自动元件父子关系映射,父元件 -> 子元件列表 */
+ readonly childMap: Map> = new Map();
+
+ constructor(readonly manager: IMaterialManager) {}
+
+ private ensureChildSet(num: number) {
+ const set = this.childMap.get(num);
+ if (set) return set;
+ const ensure = new Set();
+ 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 {
+ const tile = this.manager.getTile(autotile);
+ if (!tile) return;
+ yield* this.renderAnimatedWith(tile, connection);
+ }
+
+ *renderAnimatedWith(
+ tile: IMaterialFramedData,
+ connection: number
+ ): Generator {
+ 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();
+ // 遍历每个组合
+ 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 表示 3x4,2 表示 2x3
+ */
+function mapAutotile(
+ target: Map,
+ 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);
+ }
+ });
+}
diff --git a/packages-user/client-base/src/material/builder.ts b/packages-user/client-base/src/material/builder.ts
new file mode 100644
index 0000000..29b4335
--- /dev/null
+++ b/packages-user/client-base/src/material/builder.ts
@@ -0,0 +1,159 @@
+import {
+ ITextureStore,
+ ITexture,
+ ITextureComposedData,
+ ITextureStreamComposer,
+ TextureMaxRectsStreamComposer,
+ SizedCanvasImageSource
+} from '@motajs/render-assets';
+import { IAssetBuilder, IMaterialGetter, ITrackedAssetData } from './types';
+import { logger, PrivateListDirtyTracker } from '@motajs/common';
+
+export class AssetBuilder implements IAssetBuilder {
+ readonly composer: ITextureStreamComposer =
+ new TextureMaxRectsStreamComposer(4096, 4096, 0);
+
+ private output: ITextureStore | null = null;
+ private started: boolean = false;
+
+ private readonly trackedData: TrackedAssetData;
+
+ /** 当前的索引 */
+ private index: number = -1;
+
+ /** 贴图更新的 promise */
+ private pending: Promise = Promise.resolve();
+
+ constructor(readonly materials: IMaterialGetter) {
+ this.trackedData = new TrackedAssetData(materials, this);
+ }
+
+ pipe(store: ITextureStore): void {
+ if (this.started) {
+ logger.warn(76);
+ return;
+ }
+ this.output = store;
+ }
+
+ addTexture(texture: ITexture): ITextureComposedData {
+ this.started = true;
+ const res = [...this.composer.add([texture])];
+ const data = res[0];
+
+ if (this.output) {
+ if (data.index > this.index) {
+ this.output.addTexture(data.index, data.texture);
+ this.index = data.index;
+ }
+ }
+
+ this.pending = this.pending.then(() =>
+ this.trackedData.updateSource(data.index, data.texture.source)
+ );
+
+ return data;
+ }
+
+ private async updateSourceList(source: Set) {
+ for (const data of source) {
+ await this.trackedData.updateSource(
+ data.index,
+ data.texture.source
+ );
+ }
+ }
+
+ addTextureList(
+ texture: Iterable
+ ): Iterable {
+ this.started = true;
+ const res = [...this.composer.add(texture)];
+ const toUpdate = new Set();
+ if (this.output) {
+ res.forEach(data => {
+ if (data.index > this.index) {
+ this.output!.addTexture(data.index, data.texture);
+ this.index = data.index;
+ toUpdate.add(data);
+ } else {
+ toUpdate.add(data);
+ }
+ });
+ }
+
+ this.pending = this.pending.then(() => this.updateSourceList(toUpdate));
+
+ return res;
+ }
+
+ tracked(): ITrackedAssetData {
+ return this.trackedData;
+ }
+
+ close(): void {
+ this.composer.close();
+ }
+}
+
+class TrackedAssetData
+ extends PrivateListDirtyTracker
+ implements ITrackedAssetData
+{
+ readonly sourceList: Map = new Map();
+ readonly skipRef: Map = new Map();
+
+ private originSourceMap: Map = new Map();
+
+ private promises: Set> = new Set();
+
+ constructor(
+ readonly materials: IMaterialGetter,
+ readonly builder: AssetBuilder
+ ) {
+ super(0);
+ }
+
+ markDirty(index: number) {
+ if (index >= this.length) {
+ this.updateLength(index + 1);
+ }
+ this.dirty(index);
+ }
+
+ async updateSource(index: number, source: SizedCanvasImageSource) {
+ if (index >= this.length) {
+ this.updateLength(this.length + 1);
+ }
+ const origin = this.originSourceMap.get(index);
+ const prev = this.sourceList.get(index);
+ if (origin && origin !== source) {
+ this.skipRef.delete(origin);
+ }
+ if (prev) {
+ this.skipRef.delete(prev);
+ }
+ this.originSourceMap.set(index, source);
+ if (source instanceof ImageBitmap) {
+ if (this.skipRef.has(source)) return;
+ this.sourceList.set(index, source);
+ this.skipRef.set(source, index);
+ } else {
+ const promise = createImageBitmap(source);
+ this.promises.add(promise);
+ const bitmap = await promise;
+ this.promises.delete(promise);
+ this.sourceList.set(index, bitmap);
+ this.skipRef.set(bitmap, index);
+ // 要把源也加到映射中,因为这里的 bitmap 与外部源并不同引用
+ this.skipRef.set(source, index);
+ }
+ this.dirty(index);
+ }
+
+ async then(): Promise {
+ await Promise.all([...this.promises]);
+ }
+
+ close(): void {}
+}
diff --git a/packages-user/client-base/src/material/fallback.ts b/packages-user/client-base/src/material/fallback.ts
new file mode 100644
index 0000000..e6ceac6
--- /dev/null
+++ b/packages-user/client-base/src/material/fallback.ts
@@ -0,0 +1,140 @@
+import { ITexture } from '@motajs/render-assets';
+import { materials } from './ins';
+import { IBlockIdentifier, IIndexedIdentifier } from './types';
+import { isNil } from 'lodash-es';
+
+function extractClsBlocks>(
+ cls: C,
+ map: Record,
+ icons: Record
+): IBlockIdentifier[] {
+ const max = Math.max(...Object.values(icons));
+ const arr = Array(max).fill(void 0);
+ for (const [key, value] of Object.entries(icons)) {
+ // 样板编辑器 bug 可能会导致多个 id 使用一个偏移,因此要判断下
+ if (!(key in map) || !isNil(arr[value])) continue;
+ const id = key as AllIdsOf;
+ const num = map[id] as keyof NumberToId;
+ const identifier: IBlockIdentifier = {
+ id: id as string,
+ cls,
+ num
+ };
+ arr[value] = identifier;
+ }
+ return arr;
+}
+
+function addTileset(set: Set, map?: readonly (readonly number[])[]) {
+ if (!map) return;
+ map.forEach(line => {
+ line.forEach(v => {
+ if (v >= 10000) set.add(v);
+ });
+ });
+}
+
+function addAutotile(set: Set, map?: readonly (readonly number[])[]) {
+ if (!map) return;
+ map.forEach(line => {
+ line.forEach(v => {
+ const id = core.maps.blocksInfo[v as keyof NumberToId];
+ if (id?.cls === 'autotile') set.add(v);
+ });
+ });
+}
+
+/**
+ * 兼容旧版加载
+ */
+export function fallbackLoad() {
+ // 基本素材
+ const icons = core.icons.icons;
+ const images = core.material.images;
+ const idNumMap: Record = {};
+
+ for (const [key, value] of Object.entries(core.maps.blocksInfo)) {
+ const num = Number(key);
+ idNumMap[value.id] = Number(num);
+ if (!isNil(value.animate)) {
+ materials.setDefaultFrame(num, value.animate - 1);
+ }
+ }
+
+ const terrains = extractClsBlocks('terrains', idNumMap, icons.terrains);
+ const animates = extractClsBlocks('animates', idNumMap, icons.animates);
+ const items = extractClsBlocks('items', idNumMap, icons.items);
+ const enemys = extractClsBlocks('enemys', idNumMap, icons.enemys);
+ const npcs = extractClsBlocks('npcs', idNumMap, icons.npcs);
+ const enemy48 = extractClsBlocks('enemy48', idNumMap, icons.enemy48);
+ const npc48 = extractClsBlocks('npc48', idNumMap, icons.npc48);
+
+ // Grid
+ materials.addGrid(images.terrains, terrains);
+ materials.addGrid(images.items, items);
+
+ // Row Animates
+ materials.addRowAnimate(images.animates, animates, 32);
+ materials.addRowAnimate(images.enemys, enemys, 32);
+ materials.addRowAnimate(images.npcs, npcs, 32);
+ materials.addRowAnimate(images.enemy48, enemy48, 48);
+ materials.addRowAnimate(images.npc48, npc48, 48);
+
+ // Autotile
+ for (const key of Object.keys(icons.autotile)) {
+ const id = key as AllIdsOf<'autotile'>;
+ const img = images.autotile[id];
+ const identifier: IBlockIdentifier = {
+ id,
+ num: idNumMap[id],
+ cls: 'autotile'
+ };
+ materials.addAutotile(img, identifier);
+ }
+
+ // Tilesets
+ core.tilesets.forEach((v, i) => {
+ const img = images.tilesets[v];
+ const identifier: IIndexedIdentifier = {
+ index: i,
+ alias: v
+ };
+ materials.addTileset(img, identifier);
+ });
+
+ // Images
+ core.images.forEach((v, i) => {
+ const img = core.material.images.images[v];
+ materials.addImage(img, { index: i, alias: v });
+ });
+
+ // 地图上出现过的 tileset
+ const tilesetSet = new Set