-
-
-
请稍候...
-
-
-
-
资源即将开始加载
-
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/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..d08e771
--- /dev/null
+++ b/packages-user/client-base/src/material/fallback.ts
@@ -0,0 +1,108 @@
+import { materials } from './manager';
+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..39ede5d
--- /dev/null
+++ b/packages-user/client-base/src/material/index.ts
@@ -0,0 +1,10 @@
+import { loading } from '@user/data-base';
+import { fallbackLoad } from './fallback';
+
+export function createMaterial() {
+ loading.once('loaded', () => {
+ fallbackLoad();
+ });
+}
+
+export * from './manager';
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..b2b97c6
--- /dev/null
+++ b/packages-user/client-base/src/material/manager.ts
@@ -0,0 +1,405 @@
+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
+} from './types';
+import { logger } from '@motajs/common';
+import { getClsByString } from './utils';
+import { isNil } from 'lodash-es';
+import { AssetBuilder } from './builder';
+
+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;
+
+ 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,
+ store: ITextureStore,
+ splitter: ITextureSplitter,
+ splitterData: T,
+ processTexture?: (tex: ITexture) => void
+ ): Iterable {
+ const tex = new Texture(source);
+ const textures = [...splitter.split(tex, splitterData)];
+ if (textures.length !== map.length) {
+ logger.warn(75, textures.length.toString(), map.length.toString());
+ }
+ const res: IMaterialData[] = textures.map((v, i) => {
+ const { id, num, cls } = map[i];
+ store.addTexture(num, v);
+ store.alias(num, id);
+ this.clsMap.set(num, getClsByString(cls));
+ processTexture?.(v);
+ const data: IMaterialData = {
+ store,
+ texture: v,
+ identifier: num,
+ alias: id
+ };
+ return data;
+ });
+ return res;
+ }
+
+ addGrid(
+ source: SizedCanvasImageSource,
+ map: ArrayLike
+ ): Iterable {
+ return this.addMappedSource(
+ source,
+ map,
+ this.tileStore,
+ this.gridSplitter,
+ [32, 32]
+ );
+ }
+
+ addRowAnimate(
+ source: SizedCanvasImageSource,
+ map: ArrayLike,
+ frames: number,
+ height: number
+ ): Iterable {
+ return this.addMappedSource(
+ source,
+ map,
+ this.tileStore,
+ this.rowSplitter,
+ height,
+ (tex: ITexture) => {
+ tex.animated(new TextureColumnAnimater(), frames);
+ }
+ );
+ }
+
+ addAutotile(
+ source: SizedCanvasImageSource,
+ identifier: IBlockIdentifier
+ ): IMaterialData {
+ const texture = new Texture(source);
+ this.tileStore.addTexture(identifier.num, texture);
+ this.tileStore.alias(identifier.num, identifier.id);
+ this.clsMap.set(identifier.num, BlockCls.Autotile);
+ const data: IMaterialData = {
+ store: this.tileStore,
+ texture,
+ identifier: identifier.num,
+ alias: identifier.id
+ };
+ return data;
+ }
+
+ addTileset(
+ source: SizedCanvasImageSource,
+ identifier: IIndexedIdentifier
+ ): IMaterialData | null {
+ const tex = new Texture(source);
+ this.tilesetStore.addTexture(identifier.index, tex);
+ this.tilesetStore.alias(identifier.index, identifier.alias);
+ const width = Math.floor(source.width / 32);
+ const height = Math.floor(source.height / 32);
+ const count = width * height;
+ const offset = Math.ceil(count / 10000);
+ if (identifier.index === 0) {
+ this.tilesetOffsetMap.set(0, 0);
+ this.nowTilesetIndex = 0;
+ this.nowTilesetOffset = offset;
+ } else {
+ if (identifier.index - 1 !== this.nowTilesetIndex) {
+ logger.warn(78);
+ return null;
+ }
+ const width = Math.floor(source.width / 32);
+ const height = Math.floor(source.height / 32);
+ const count = width * height;
+ const offset = Math.ceil(count / 10000);
+ const end = this.nowTilesetOffset + offset;
+ for (let i = this.nowTilesetOffset; i < end; i++) {
+ this.tilesetOffsetMap.set(i, identifier.index);
+ }
+ this.nowTilesetOffset = end;
+ this.nowTilesetIndex = identifier.index;
+ }
+ const data: IMaterialData = {
+ store: this.tilesetStore,
+ texture: tex,
+ identifier: identifier.index,
+ alias: identifier.alias
+ };
+ return data;
+ }
+
+ addImage(
+ source: SizedCanvasImageSource,
+ identifier: IIndexedIdentifier
+ ): IMaterialData {
+ const texture = new Texture(source);
+ this.imageStore.addTexture(identifier.index, texture);
+ this.imageStore.alias(identifier.index, identifier.alias);
+ const data: IMaterialData = {
+ store: this.imageStore,
+ texture,
+ identifier: identifier.index,
+ alias: identifier.alias
+ };
+ return data;
+ }
+
+ getTile(identifier: number): ITexture | null {
+ if (identifier < 10000) {
+ return this.tileStore.getTexture(identifier);
+ } else {
+ return this.cacheTileset(identifier);
+ }
+ }
+
+ getTileset(identifier: number): ITexture | null {
+ return this.tilesetStore.getTexture(identifier);
+ }
+
+ getImage(identifier: number): ITexture | null {
+ return this.imageStore.getTexture(identifier);
+ }
+
+ getTileByAlias(alias: string): ITexture | null {
+ if (/X\d{5,}/.test(alias)) {
+ return this.cacheTileset(parseInt(alias.slice(1)));
+ } else {
+ return this.tileStore.fromAlias(alias);
+ }
+ }
+
+ getTilesetByAlias(alias: string): ITexture | null {
+ return this.tilesetStore.fromAlias(alias);
+ }
+
+ getImageByAlias(alias: string): ITexture | null {
+ return this.imageStore.fromAlias(alias);
+ }
+
+ private getTilesetOwnTexture(identifier: number) {
+ const texture = this.tileStore.getTexture(identifier);
+ if (texture) return texture;
+ // 如果 tileset 不存在,那么执行缓存操作
+ const offset = Math.floor(identifier / 10000);
+ const index = this.tilesetOffsetMap.get(offset - 1);
+ if (isNil(index)) return null;
+ // 获取对应的 tileset 贴图
+ const tileset = this.tilesetStore.getTexture(index);
+ if (!tileset) return null;
+ // 计算图块位置
+ const rest = identifier - offset * 10000;
+ const { width, height } = tileset;
+ const tileWidth = Math.floor(width / 32);
+ const tileHeight = Math.floor(height / 32);
+ // 如果图块位置超出了贴图范围
+ if (rest > tileWidth * tileHeight) return null;
+ // 裁剪 tileset,生成贴图
+ const x = rest % tileWidth;
+ const y = Math.floor(rest / tileWidth);
+ const newTexture = new Texture(tileset.source);
+ newTexture.clip(x * 32, y * 32, 32, 32);
+ return newTexture;
+ }
+
+ cacheTileset(identifier: number): ITexture | null {
+ const newTexture = this.getTilesetOwnTexture(identifier);
+ if (!newTexture) return null;
+ // 缓存贴图
+ this.tileStore.addTexture(identifier, newTexture);
+ this.idNumMap.set(`X${identifier}`, identifier);
+ this.numIdMap.set(identifier, `X${identifier}`);
+ const data = this.assetBuilder.addTexture(newTexture);
+ newTexture.toAsset(data);
+ return newTexture;
+ }
+
+ cacheTilesetList(
+ identifierList: Iterable
+ ): Iterable {
+ const arr = [...identifierList];
+ const toAdd: ITexture[] = [];
+
+ arr.forEach(v => {
+ const newTexture = this.getTilesetOwnTexture(v);
+ if (!newTexture) return;
+ toAdd.push(newTexture);
+ this.tileStore.addTexture(v, newTexture);
+ this.idNumMap.set(`X${v}`, v);
+ this.numIdMap.set(v, `X${v}`);
+ });
+
+ const set = new Set(toAdd);
+
+ const data = this.assetBuilder.addTextureList(toAdd);
+ const res = [...data];
+ res.forEach(v => {
+ v.assetMap.keys().forEach(tex => {
+ if (set.has(tex)) tex.toAsset(v);
+ });
+ });
+
+ return toAdd;
+ }
+
+ buildAssets(): Iterable {
+ this.assetBuilder.pipe(this.assetStore);
+ const data = this.assetBuilder.addTextureList(this.tileStore.values());
+ const arr = [...data];
+ const res: IMaterialAssetData[] = [];
+ arr.forEach((v, i) => {
+ const alias = `asset-${i}`;
+ this.assetStore.alias(i, alias);
+ this.assetDataStore.set(i, v);
+ const data: IMaterialAssetData = {
+ data: v,
+ identifier: i,
+ alias,
+ store: this.assetStore
+ };
+ for (const tex of v.assetMap.keys()) {
+ tex.toAsset(v);
+ }
+ res.push(data);
+ });
+ return res;
+ }
+
+ getAsset(identifier: number): ITextureComposedData | null {
+ return this.assetDataStore.get(identifier) ?? null;
+ }
+
+ getAssetByAlias(alias: string): ITextureComposedData | null {
+ const id = this.assetStore.identifierOf(alias);
+ if (isNil(id)) return null;
+ return this.assetDataStore.get(id) ?? null;
+ }
+
+ private getTextureOf(identifier: number, cls: BlockCls): ITexture | null {
+ if (cls === BlockCls.Unknown) return null;
+ if (cls !== BlockCls.Tileset) {
+ return this.tileStore.getTexture(identifier);
+ }
+ if (identifier < 10000) return null;
+ return this.cacheTileset(identifier);
+ }
+
+ getRenderable(identifier: number): ITextureRenderable | null {
+ const cls = this.clsMap.get(identifier);
+ if (isNil(cls)) return null;
+ const texture = this.getTextureOf(identifier, cls);
+ if (!texture) return null;
+ return texture.static();
+ }
+
+ getRenderableByAlias(alias: string): ITextureRenderable | null {
+ const identifier = this.idNumMap.get(alias);
+ if (isNil(identifier)) return null;
+ return this.getRenderable(identifier);
+ }
+
+ getBlockCls(identifier: number): BlockCls {
+ return this.clsMap.get(identifier) ?? BlockCls.Unknown;
+ }
+
+ getBlockClsByAlias(alias: string): BlockCls {
+ const id = this.idNumMap.get(alias);
+ if (isNil(id)) return BlockCls.Unknown;
+ return this.clsMap.get(id) ?? BlockCls.Unknown;
+ }
+
+ getIdentifierByAlias(alias: string): number | undefined {
+ return this.idNumMap.get(alias);
+ }
+
+ getAliasByIdentifier(identifier: number): string | undefined {
+ return this.numIdMap.get(identifier);
+ }
+
+ setBigImage(identifier: number, image: ITexture): IBigImageData {
+ const bigImageId = this.bigImageId++;
+ this.bigImageStore.addTexture(bigImageId, image);
+ this.bigImageData.set(identifier, image);
+ const data: IBigImageData = {
+ identifier: bigImageId,
+ store: this.bigImageStore
+ };
+ return data;
+ }
+
+ isBigImage(identifier: number): boolean {
+ return this.bigImageData.has(identifier);
+ }
+
+ getBigImage(identifier: number): ITexture | null {
+ return this.bigImageData.get(identifier) ?? null;
+ }
+
+ getBigImageByAlias(alias: string): ITexture | null {
+ const identifier = this.idNumMap.get(alias);
+ if (isNil(identifier)) return null;
+ return this.bigImageData.get(identifier) ?? null;
+ }
+}
+
+export const materials = new MaterialManager();
diff --git a/packages-user/client-base/src/material/types.ts b/packages-user/client-base/src/material/types.ts
new file mode 100644
index 0000000..9c90f52
--- /dev/null
+++ b/packages-user/client-base/src/material/types.ts
@@ -0,0 +1,346 @@
+import {
+ IRect,
+ ITexture,
+ ITextureComposedData,
+ ITextureRenderable,
+ ITextureStore,
+ SizedCanvasImageSource
+} from '@motajs/render-assets';
+
+export const enum BlockCls {
+ Unknown,
+ Terrains,
+ Animates,
+ Enemys,
+ Npcs,
+ Items,
+ Enemy48,
+ Npc48,
+ Tileset,
+ Autotile
+}
+
+export interface IMaterialData {
+ /** 此素材的贴图对象存入了哪个贴图存储对象 */
+ readonly store: ITextureStore;
+ /** 贴图对象 */
+ readonly texture: ITexture;
+ /** 此素材的贴图对象的数字 id,一般对应到图块数字 */
+ readonly identifier: number;
+ /** 此素材的贴图对象的字符串别名,一般对应到图块 id */
+ readonly alias?: string;
+}
+
+export interface IBlockIdentifier {
+ /** 图块 id */
+ readonly id: string;
+ /** 图块数字 */
+ readonly num: number;
+ /** 图块类型 */
+ readonly cls: Cls;
+}
+
+export interface IIndexedIdentifier {
+ /** 标识符索引 */
+ readonly index: number;
+ /** 标识符别名 */
+ readonly alias: string;
+}
+
+export interface IMaterialAssetData {
+ /** 图集数据 */
+ readonly data: ITextureComposedData;
+ /** 贴图的标识符 */
+ readonly identifier: number;
+ /** 贴图的别名 */
+ readonly alias: string;
+ /** 贴图所属的存储对象 */
+ readonly store: ITextureStore;
+}
+
+export interface IAutotileConnection {
+ /** 连接方式,上方连接是第一位,顺时针旋转位次依次升高 */
+ readonly connection: number;
+ /** 中心自动元件对应的图块数字 */
+ readonly center: number;
+}
+
+export interface IAutotileRenderable {
+ /** 自动元件的图像源 */
+ readonly source: SizedCanvasImageSource;
+ /** 渲染的矩形范围 */
+ readonly rects: Readonly[];
+}
+
+export interface IBigImageData {
+ /** 大怪物贴图在 store 中的标识符 */
+ readonly identifier: number;
+ /** 存储大怪物贴图的存储对象 */
+ readonly store: ITextureStore;
+}
+
+export interface IAutotileProcessor {
+ /** 该自动元件处理器使用的素材管理器 */
+ readonly manager: IMaterialManager;
+
+ /**
+ * 设置一个自动元件的父元件,一个自动元件可以有多个父元件
+ * @param autotile 自动元件
+ * @param parent 自动元件的父元件
+ */
+ setParent(autotile: number, parent: number): void;
+
+ /**
+ * 获取自动元件的链接情况
+ * @param array 地图图块数组
+ * @param index 自动元件图块所在的索引
+ * @param edge 当前图块的边缘连接情况
+ */
+ connect(
+ array: Float32Array,
+ index: number,
+ edge: number
+ ): IAutotileConnection;
+
+ /**
+ * 获取指定自动元件的可渲染对象
+ * @param autotile 自动元件的图块数字
+ * @param connection 连接方式,上方连接是第一位,顺时针旋转位次依次升高
+ */
+ render(autotile: number, connection: number): IAutotileRenderable;
+
+ /**
+ * 通过可渲染对象输出自动元件经过连接的可渲染对象
+ * @param renderable 自动元件的原始可渲染对象
+ * @param connection 自动元件的链接方式
+ */
+ fromRenderable(
+ renderable: ITextureRenderable,
+ connection: number
+ ): IAutotileRenderable;
+}
+
+export interface IMaterialManager {
+ /** 贴图存储,把 terrains 等内容单独分开存储 */
+ readonly tileStore: ITextureStore;
+ /** tilesets 贴图存储,每个 tileset 是一个贴图对象 */
+ readonly tilesetStore: ITextureStore;
+ /** 存储注册的图像的存储对象 */
+ readonly imageStore: ITextureStore;
+ /** 图集存储,将常用贴图存入其中 */
+ readonly assetStore: ITextureStore;
+ /** bigImage 存储,存储大怪物数据 */
+ readonly bigImageStore: ITextureStore;
+
+ /** 图块类型映射 */
+ readonly clsMap: Map;
+
+ /**
+ * 添加网格类型的贴图,包括 terrains 和 items 类型
+ * @param source 图像源
+ * @param map 贴图字符串 id 与图块数字映射,按照先从左到右,再从上到下的顺序映射
+ */
+ addGrid(
+ source: SizedCanvasImageSource,
+ map: ArrayLike
+ ): Iterable;
+
+ /**
+ * 添加行动画的贴图,包括 animates enemys npcs enemy48 npc48 类型
+ * @param source 图像源
+ * @param map 贴图字符串 id 与图块数字映射,按从上到下的顺序映射
+ * @param frames 每一行的帧数
+ * @param height 每一行的高度
+ */
+ addRowAnimate(
+ source: SizedCanvasImageSource,
+ map: ArrayLike,
+ frames: number,
+ height: number
+ ): Iterable;
+
+ /**
+ * 添加自动元件
+ * @param source 图像源
+ * @param identifier 自动元件的字符串 id 及图块数字
+ */
+ addAutotile(
+ source: SizedCanvasImageSource,
+ identifier: IBlockIdentifier
+ ): IMaterialData;
+
+ /**
+ * 添加一个 tileset 类型的素材
+ * @param source 图像源
+ * @param alias tileset 的标识符,包含其在 tilesets 列表中的索引和图片名称
+ */
+ addTileset(
+ source: SizedCanvasImageSource,
+ identifier: IIndexedIdentifier
+ ): IMaterialData | null;
+
+ /**
+ * 添加一个图片
+ * @param source 图像源
+ * @param identifier 图片的标识符,包含其在 images 列表中的索引和图片名称
+ */
+ addImage(
+ source: SizedCanvasImageSource,
+ identifier: IIndexedIdentifier
+ ): IMaterialData;
+
+ /**
+ * 根据图块数字获取图块,可以获取额外素材,会自动将未缓存的额外素材缓存
+ * @param identifier 图块的图块数字
+ */
+ getTile(identifier: number): ITexture | null;
+
+ /**
+ * 根据额外素材索引获取额外素材
+ * @param identifier 额外素材的索引
+ */
+ getTileset(identifier: number): ITexture | null;
+
+ /**
+ * 根据图片的索引获取图片
+ * @param identifier 图片的索引
+ */
+ getImage(identifier: number): ITexture | null;
+
+ /**
+ * 根据图块 id 获取图块,可以获取额外素材,会自动将未缓存的额外素材缓存
+ * @param alias 图块 id
+ */
+ getTileByAlias(alias: string): ITexture | null;
+
+ /**
+ * 根据额外素材名称获取额外素材
+ * @param alias 额外素材名称
+ */
+ getTilesetByAlias(alias: string): ITexture | null;
+
+ /**
+ * 根据图片名称获取图片
+ * @param alias 图片名称
+ */
+ getImageByAlias(alias: string): ITexture | null;
+
+ /**
+ * 缓存某个 tileset
+ * @param identifier tileset 的标识符,即图块数字
+ */
+ cacheTileset(identifier: number): ITexture | null;
+
+ /**
+ * 缓存一系列 tileset
+ * @param identifierList 标识符列表,即图块数字列表
+ */
+ cacheTilesetList(
+ identifierList: Iterable
+ ): Iterable