-
-
-
请稍候...
-
-
-
-
资源即将开始加载
-
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..4dfed65
--- /dev/null
+++ b/packages-user/client-base/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@user/client-base",
+ "dependencies": {
+ "@motajs/render-asset": "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/asset.ts b/packages-user/client-base/src/material/asset.ts
new file mode 100644
index 0000000..f1242be
--- /dev/null
+++ b/packages-user/client-base/src/material/asset.ts
@@ -0,0 +1,30 @@
+import { ITextureComposedData } from '@motajs/render-assets';
+import { IMaterialAsset } from './types';
+
+export class MaterialAsset implements IMaterialAsset {
+ /** 标记列表 */
+ private readonly marks: Map
= new Map();
+ /** 脏标记,所有值小于此标记的都视为需要更新 */
+ private dirtyFlag: number = 0;
+
+ constructor(readonly data: ITextureComposedData) {}
+
+ dirty(): void {
+ this.dirtyFlag++;
+ }
+
+ mark(): symbol {
+ const symbol = Symbol();
+ this.marks.set(symbol, this.dirtyFlag);
+ return symbol;
+ }
+
+ unmark(mark: symbol): void {
+ this.marks.delete(mark);
+ }
+
+ dirtySince(mark: symbol): boolean {
+ const value = this.marks.get(mark) ?? -1;
+ return value < this.dirtyFlag;
+ }
+}
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..0de4e3b
--- /dev/null
+++ b/packages-user/client-base/src/material/autotile.ts
@@ -0,0 +1,447 @@
+import { IRect, ITexture, ITextureRenderable } from '@motajs/render-assets';
+import {
+ AutotileType,
+ BlockCls,
+ IAutotileConnection,
+ IAutotileProcessor,
+ IAutotileRenderable,
+ IMaterialManager
+} from './types';
+import { logger } from '@motajs/common';
+
+interface ConnectedAutotile {
+ readonly lt: Readonly;
+ readonly rt: Readonly;
+ readonly rb: Readonly;
+ readonly lb: Readonly;
+}
+
+/** 3x4 自动元件的连接映射,元组表示将对应大小的自动元件按照格子 1/4 大小切分后对应的索引位置 */
+const connectionMap3x4 = new Map();
+/** 2x3 自动元件的连接映射,元组表示将对应大小的自动元件按照格子 1/4 大小切分后对应的索引位置 */
+const connectionMap2x3 = new Map();
+/** 3x4 自动元件各方向连接矩形映射 */
+const rectMap3x4 = new Map();
+/** 2x3 自动元件各方向连接的矩形映射 */
+const rectMap2x3 = new Map();
+
+interface AutotileFrameList {
+ type: AutotileType;
+ rects: Readonly[];
+}
+
+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;
+ }
+
+ setParent(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 0b1100_0111;
+ } else if (index === length - 1) {
+ return 0b0111_1100;
+ } else {
+ return 0b0100_0100;
+ }
+ }
+ // 如果地图宽度只有 1
+ if (width === 1) {
+ if (index === 0) {
+ return 0b1111_0001;
+ } else if (index === length - 1) {
+ return 0b0001_1111;
+ } else {
+ return 0b0001_0001;
+ }
+ }
+
+ // 正常地图
+
+ const lastLine = length - width;
+ const x = index % width;
+
+ // 四个角,左上,右上,右下,左下
+ if (index === 0) {
+ return 0b1100_0001;
+ } else if (index === width - 1) {
+ return 0b0111_0000;
+ } else if (index === length - 1) {
+ return 0b0001_1100;
+ } else if (index === lastLine) {
+ return 0b0000_0111;
+ }
+ // 四条边,上,右,下,左
+ else if (index < width) {
+ return 0b0100_0000;
+ } else if (x === width - 1) {
+ return 0b0001_0000;
+ } else if (index > lastLine) {
+ return 0b0000_0100;
+ } else if (x === 0) {
+ return 0b0000_0001;
+ }
+ // 不在边缘
+ else {
+ return 0b0000_0000;
+ }
+ }
+
+ connect(
+ array: Uint32Array,
+ index: number,
+ width: number
+ ): IAutotileConnection {
+ let res: number = this.connectEdge(array.length, index, width);
+ const block = array[index];
+ 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) {
+ // 不包含子元件,那么直接跟相同的连接
+ 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
+ };
+ }
+
+ render(
+ autotile: number,
+ connection: number
+ ): Generator | null {
+ const cls = this.manager.getBlockCls(autotile);
+ if (cls !== BlockCls.Autotile) return null;
+ const tile = this.manager.getTile(autotile)!;
+ return this.fromStaticRenderable(tile.static(), connection);
+ }
+
+ /**
+ * 根据静态可渲染对象获取自动元件的帧列表
+ * @param renderable 静态可渲染对象
+ */
+ private getStaticRectList(
+ renderable: ITextureRenderable
+ ): AutotileFrameList {
+ const { x, y, w, h } = renderable.rect;
+ const type = h === 128 ? AutotileType.Big3x4 : AutotileType.Small2x3;
+ if (w === 96) {
+ return {
+ type,
+ rects: [renderable.rect]
+ };
+ } else {
+ return {
+ type,
+ rects: [
+ { x: x + 0, y, w, h },
+ { x: x + 96, y, w, h },
+ { x: x + 192, y, w, h },
+ { x: x + 288, y, w, h }
+ ]
+ };
+ }
+ }
+
+ /**
+ * 对自动元件连接执行偏移操作,偏移至自动元件在图像源中的所在矩形范围
+ * @param ox 横向偏移量
+ * @param oy 纵向偏移量
+ * @param connection 自动元件连接信息
+ */
+ private getConnectedRect(
+ ox: number,
+ oy: number,
+ connection: ConnectedAutotile
+ ): ConnectedAutotile {
+ const { lt, rt, rb, lb } = connection;
+
+ return {
+ lt: { x: ox + lt.x, y: oy + lt.y, w: lt.w, h: lt.h },
+ rt: { x: ox + rt.x, y: oy + rt.y, w: rt.w, h: rt.h },
+ rb: { x: ox + rb.x, y: oy + rb.y, w: rb.w, h: rb.h },
+ lb: { x: ox + lb.x, y: oy + lb.y, w: lb.w, h: lb.h }
+ };
+ }
+
+ *fromStaticRenderable(
+ renderable: ITextureRenderable,
+ connection: number
+ ): Generator | null {
+ const { type, rects } = this.getStaticRectList(renderable);
+ const map = type === AutotileType.Big3x4 ? rectMap3x4 : rectMap2x3;
+ const data = map.get(connection);
+ if (!data) {
+ logger.error(27);
+ return null;
+ }
+ if (rects.length === 1) {
+ const { x, y } = rects[0];
+ const connected = this.getConnectedRect(x, y, data);
+ if (!connected) return null;
+ const res: IAutotileRenderable = {
+ source: renderable.source,
+ lt: connected.lt,
+ rt: connected.rt,
+ rb: connected.rb,
+ lb: connected.lb
+ };
+ yield res;
+ } else {
+ for (const { x, y } of rects) {
+ const connected = this.getConnectedRect(x, y, data);
+ if (!connected) return null;
+ const res: IAutotileRenderable = {
+ source: renderable.source,
+ lt: connected.lt,
+ rt: connected.rt,
+ rb: connected.rb,
+ lb: connected.lb
+ };
+ yield res;
+ }
+ }
+ }
+
+ fromAnimatedRenderable(
+ renderable: ITextureRenderable,
+ connection: number
+ ): IAutotileRenderable | null {
+ const { x, y, h } = renderable.rect;
+ const type = h === 128 ? AutotileType.Big3x4 : AutotileType.Small2x3;
+ const map = type === AutotileType.Big3x4 ? rectMap3x4 : rectMap2x3;
+ const data = map.get(connection);
+ if (!data) {
+ logger.error(27);
+ return null;
+ }
+ const connected = this.getConnectedRect(x, y, data);
+ if (!connected) return null;
+ const res: IAutotileRenderable = {
+ source: renderable.source,
+ lt: connected.lt,
+ rt: connected.rt,
+ rb: connected.rb,
+ lb: connected.lb
+ };
+ return res;
+ }
+
+ *fromAnimatedGenerator(
+ texture: ITexture,
+ generator: Generator | null,
+ connection: number
+ ): Generator | null {
+ if (!generator) return null;
+ const h = texture.height;
+ const type = h === 128 ? AutotileType.Big3x4 : AutotileType.Small2x3;
+ const map = type === AutotileType.Big3x4 ? rectMap3x4 : rectMap2x3;
+ const data = map.get(connection);
+ if (!data) {
+ logger.error(27);
+ return null;
+ }
+ while (true) {
+ const value = generator.next();
+ if (value.done) break;
+ const renderable = value.value;
+ const { x, y } = renderable.rect;
+ const connected = this.getConnectedRect(x, y, data);
+ if (!connected) return null;
+ const res: IAutotileRenderable = {
+ source: renderable.source,
+ lt: connected.lt,
+ rt: connected.rt,
+ rb: connected.rb,
+ lb: connected.lb
+ };
+ yield res;
+ }
+ }
+}
+
+/**
+ * 映射自动元件连接
+ * @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 }
+ });
+ });
+}
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..a9473bc
--- /dev/null
+++ b/packages-user/client-base/src/material/builder.ts
@@ -0,0 +1,57 @@
+import {
+ ITextureStore,
+ ITexture,
+ ITextureComposedData,
+ ITextureStreamComposer,
+ TextureMaxRectsStreamComposer
+} from '@motajs/render-assets';
+import { IAssetBuilder } from './types';
+import { logger } 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;
+
+ 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 (!this.output.getTexture(data.index)) {
+ this.output.addTexture(data.index, data.texture);
+ }
+ }
+ return data;
+ }
+
+ addTextureList(
+ texture: Iterable
+ ): Iterable {
+ this.started = true;
+ const res = [...this.composer.add(texture)];
+ if (this.output) {
+ res.forEach(v => {
+ if (!this.output!.getTexture(v.index)) {
+ this.output!.addTexture(v.index, v.texture);
+ }
+ });
+ }
+ return res;
+ }
+
+ close(): void {
+ this.composer.close();
+ }
+}
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..030c4a8
--- /dev/null
+++ b/packages-user/client-base/src/material/fallback.ts
@@ -0,0 +1,108 @@
+import { materials } from './ins';
+import { IBlockIdentifier, IIndexedIdentifier } from './types';
+
+function extractClsBlocks>(
+ cls: C,
+ map: Record,
+ icons: Record
+): IBlockIdentifier[] {
+ const max = Math.max(...Object.values(icons));
+ const arr = Array(max).fill(0);
+ for (const [key, value] of Object.entries(icons)) {
+ if (!(key in map)) 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);
+ });
+ });
+}
+
+/**
+ * 兼容旧版加载
+ */
+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)) {
+ idNumMap[value.id] = Number(key);
+ }
+
+ 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, 4, 32);
+ materials.addRowAnimate(images.enemys, enemys, 2, 32);
+ materials.addRowAnimate(images.npcs, npcs, 2, 32);
+ materials.addRowAnimate(images.enemy48, enemy48, 4, 48);
+ materials.addRowAnimate(images.npc48, npc48, 4, 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 });
+ });
+
+ materials.buildAssets();
+
+ // 地图上出现过的 tileset
+ const tilesetSet = new Set();
+ core.floorIds.forEach(v => {
+ const floor = core.floors[v];
+ addTileset(tilesetSet, floor.bgmap);
+ addTileset(tilesetSet, floor.bg2map);
+ addTileset(tilesetSet, floor.map);
+ addTileset(tilesetSet, floor.fgmap);
+ addTileset(tilesetSet, floor.fg2map);
+ });
+
+ materials.cacheTilesetList(tilesetSet);
+}
diff --git a/packages-user/client-base/src/material/index.ts b/packages-user/client-base/src/material/index.ts
new file mode 100644
index 0000000..0e8c3b5
--- /dev/null
+++ b/packages-user/client-base/src/material/index.ts
@@ -0,0 +1,16 @@
+import { loading } from '@user/data-base';
+import { fallbackLoad } from './fallback';
+
+export function createMaterial() {
+ loading.once('loaded', () => {
+ fallbackLoad();
+ });
+}
+
+export * from './autotile';
+export * from './builder';
+export * from './fallback';
+export * from './ins';
+export * from './manager';
+export * from './types';
+export * from './utils';
diff --git a/packages-user/client-base/src/material/ins.ts b/packages-user/client-base/src/material/ins.ts
new file mode 100644
index 0000000..a4f3bc1
--- /dev/null
+++ b/packages-user/client-base/src/material/ins.ts
@@ -0,0 +1,5 @@
+import { AutotileProcessor } from './autotile';
+import { MaterialManager } from './manager';
+
+export const materials = new MaterialManager();
+export const autotile = new AutotileProcessor(materials);
diff --git a/packages-user/client-base/src/material/manager.ts b/packages-user/client-base/src/material/manager.ts
new file mode 100644
index 0000000..30fc2f1
--- /dev/null
+++ b/packages-user/client-base/src/material/manager.ts
@@ -0,0 +1,444 @@
+import {
+ ITexture,
+ ITextureComposedData,
+ ITextureRenderable,
+ ITextureSplitter,
+ ITextureStore,
+ SizedCanvasImageSource,
+ Texture,
+ TextureColumnAnimater,
+ TextureGridSplitter,
+ TextureRowSplitter,
+ TextureStore
+} from '@motajs/render-assets';
+import {
+ IBlockIdentifier,
+ IMaterialData,
+ IMaterialManager,
+ IIndexedIdentifier,
+ IMaterialAssetData,
+ BlockCls,
+ IBigImageData,
+ IAssetBuilder,
+ IMaterialAsset
+} from './types';
+import { logger } from '@motajs/common';
+import { getClsByString } from './utils';
+import { isNil } from 'lodash-es';
+import { AssetBuilder } from './builder';
+import { MaterialAsset } from './asset';
+
+export class MaterialManager implements IMaterialManager {
+ readonly tileStore: ITextureStore = new TextureStore();
+ readonly tilesetStore: ITextureStore = new TextureStore();
+ readonly imageStore: ITextureStore = new TextureStore();
+ readonly assetStore: ITextureStore = new TextureStore();
+ readonly bigImageStore: ITextureStore = new TextureStore();
+
+ /** 图集信息存储 */
+ readonly assetDataStore: Map = new Map();
+
+ /** 大怪物数据 */
+ readonly bigImageData: Map = new Map();
+ /** tileset 中 `Math.floor(id / 10000) + 1` 映射到 tileset 对应索引的映射,用于处理图块超出 10000 的 tileset */
+ readonly tilesetOffsetMap: Map = new Map();
+ /** 图集打包器 */
+ readonly assetBuilder: IAssetBuilder = new AssetBuilder();
+
+ readonly idNumMap: Map = new Map();
+ readonly numIdMap: Map = new Map();
+ readonly clsMap: Map = new Map();
+
+ /** 网格切分器 */
+ readonly gridSplitter: TextureGridSplitter = new TextureGridSplitter();
+ /** 行切分器 */
+ readonly rowSplitter: TextureRowSplitter = new TextureRowSplitter();
+
+ /** 大怪物贴图的标识符 */
+ private bigImageId: number = 0;
+ /** 当前 tileset 索引 */
+ private nowTilesetIndex: number = -1;
+ /** 当前 tileset 偏移 */
+ private nowTilesetOffset: number = 0;
+ /** 是否已经构建过素材 */
+ private built: boolean = false;
+
+ /** 标记列表 */
+ private readonly markList: symbol[] = [];
+
+ constructor() {
+ this.assetBuilder.pipe(this.assetStore);
+ }
+
+ /**
+ * 添加由分割器和图块映射组成的图像源贴图
+ * @param source 图像源
+ * @param map 图块 id 与图块数字映射
+ * @param store 要添加至的贴图存储对象
+ * @param splitter 使用的分割器
+ * @param splitterData 传递给分割器的数据
+ * @param processTexture 对每个纹理进行处理
+ */
+ private addMappedSource(
+ source: SizedCanvasImageSource,
+ map: ArrayLike