import { ITexture, ITextureComposedData, ITextureRenderable, ITextureSplitter, ITextureStore, SizedCanvasImageSource, Texture, TextureGridSplitter, TextureRowSplitter, TextureStore } from '@motajs/render-assets'; import { IBlockIdentifier, IMaterialData, IMaterialManager, IIndexedIdentifier, IMaterialAssetData, BlockCls, IBigImageReturn, IAssetBuilder, IMaterialAsset, IMaterialFramedData } from './types'; import { logger } from '@motajs/common'; import { getClsByString, getTextureFrame } from './utils'; import { isNil } from 'lodash-es'; import { AssetBuilder } from './builder'; import { MaterialAsset } from './asset'; import { AutotileProcessor } from './autotile'; 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 autotileSource: Map = new Map(); /** 图集信息存储 */ readonly assetDataStore: Map = new Map(); /** 贴图到图集索引的映射 */ readonly assetMap: 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(); /** 图块 id 到图块数字的映射 */ readonly idNumMap: Map = new Map(); /** 图块数字到图块 id 的映射 */ 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; 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, height: number ): Iterable { return this.addMappedSource( source, map, this.tileStore, this.rowSplitter, height ); } addAutotile( source: SizedCanvasImageSource, identifier: IBlockIdentifier ): IMaterialData | null { const frames = source.width === 96 ? 1 : 4; const flattened = AutotileProcessor.flatten({ source, frames }); if (!flattened) return null; const texture = new Texture(flattened); 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; } // 一个 tileset 可能不止 10000 个图块,需要计算偏移 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): IMaterialFramedData | null { if (identifier < 10000) { const texture = this.tileStore.getTexture(identifier); if (!texture) return null; const cls = this.clsMap.get(identifier) ?? BlockCls.Unknown; return { texture, cls, offset: 32, frames: getTextureFrame(cls, texture) }; } else { const texture = this.cacheTileset(identifier); if (!texture) return null; return { texture, cls: BlockCls.Tileset, offset: 32, frames: 1 }; } } getTileset(identifier: number): ITexture | null { return this.tilesetStore.getTexture(identifier); } getImage(identifier: number): ITexture | null { return this.imageStore.getTexture(identifier); } getTileByAlias(alias: string): IMaterialFramedData | null { if (/X\d{5,}/.test(alias)) { return this.getTile(parseInt(alias.slice(1))); } else { const identifier = this.tileStore.identifierOf(alias); if (isNil(identifier)) return null; return this.getTile(identifier); } } 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; } /** * 检查图集状态,如果已存在图集则标记为脏,否则新增图集 * @param data 图集数据 */ private checkAssetDirty(data: ITextureComposedData) { const asset = this.assetDataStore.get(data.index); if (asset) { // 如果不是新图集,需要标记为脏 asset.dirty(); } else { // 如果有新图集,需要添加 const alias = `asset-${data.index}`; const newAsset = new MaterialAsset(data); newAsset.dirty(); this.assetStore.alias(data.index, alias); this.assetDataStore.set(data.index, newAsset); } } /** * 将指定的贴图列表转换至指定的图集数据中 * @param composedData 组合数据 * @param textures 贴图列表 */ private cacheToAsset( composedData: ITextureComposedData[], textures: ITexture[] ) { textures.forEach(tex => { const assetData = composedData.find(v => v.assetMap.has(tex)); if (!assetData) { logger.error(38); return; } tex.toAsset(assetData); }); composedData.forEach(v => this.checkAssetDirty(v)); } 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); this.checkAssetDirty(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 data = this.assetBuilder.addTextureList(toAdd); const res = [...data]; this.cacheToAsset(res, toAdd); return toAdd; } /** * 获取自动元件展开后的图片,如果图片不存在,或是已经展开并存储至了 `tileStore`,那么返回 `null` * @param identifier 自动元件标识符 */ private getFlattenedAutotile( identifier: number ): SizedCanvasImageSource | null { const cls = this.clsMap.get(identifier); if (cls !== BlockCls.Autotile) return null; if (this.tileStore.getTexture(identifier)) return null; const source = this.autotileSource.get(identifier); if (!source) return null; const frames = source.width === 96 ? 1 : 4; const flattened = AutotileProcessor.flatten({ source, frames }); if (!flattened) return null; return flattened; } cacheAutotile(identifier: number): ITexture | null { const flattened = this.getFlattenedAutotile(identifier); if (!flattened) return null; const tex = new Texture(flattened); this.tileStore.addTexture(identifier, tex); const data = this.assetBuilder.addTexture(tex); tex.toAsset(data); this.checkAssetDirty(data); return tex; } cacheAutotileList( identifierList: Iterable ): Iterable { const arr = [...identifierList]; const toAdd: ITexture[] = []; arr.forEach(v => { const flattened = this.getFlattenedAutotile(v); if (!flattened) return; const tex = new Texture(flattened); this.tileStore.addTexture(v, tex); toAdd.push(tex); }); const data = this.assetBuilder.addTextureList(toAdd); const res = [...data]; this.cacheToAsset(res, toAdd); return toAdd; } buildAssets(): Iterable { if (this.built) { logger.warn(79); return []; } this.built = true; const data = this.assetBuilder.addTextureList(this.tileStore.values()); const arr = [...data]; const res: IMaterialAssetData[] = []; arr.forEach(v => { const alias = `asset-${v.index}`; this.assetStore.alias(v.index, alias); this.assetDataStore.set(v.index, new MaterialAsset(v)); const data: IMaterialAssetData = { data: v, identifier: v.index, alias, store: this.assetStore }; for (const tex of v.assetMap.keys()) { tex.toAsset(v); } res.push(data); }); return res; } getAsset(identifier: number): IMaterialAsset | null { return this.assetDataStore.get(identifier) ?? null; } getAssetByAlias(alias: string): IMaterialAsset | 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.render(); } 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, frames: number ): IBigImageReturn { const bigImageId = this.bigImageId++; this.bigImageStore.addTexture(bigImageId, image); const cls = this.clsMap.get(identifier) ?? BlockCls.Unknown; const store: IMaterialFramedData = { texture: image, cls, offset: image.width / 4, frames }; this.bigImageData.set(identifier, store); const data: IBigImageReturn = { identifier: bigImageId, store: this.bigImageStore }; return data; } isBigImage(identifier: number): boolean { return this.bigImageData.has(identifier); } getBigImage(identifier: number): IMaterialFramedData | null { return this.bigImageData.get(identifier) ?? null; } getBigImageByAlias(alias: string): IMaterialFramedData | null { const identifier = this.idNumMap.get(alias); if (isNil(identifier)) return null; return this.bigImageData.get(identifier) ?? null; } getIfBigImage(identifier: number): IMaterialFramedData | null { const bigImage = this.bigImageData.get(identifier) ?? null; if (bigImage) return bigImage; else return this.getTile(identifier); } assetContainsTexture(texture: ITexture): boolean { return this.assetMap.has(texture); } getTextureAsset(texture: ITexture): number | undefined { return this.assetMap.get(texture); } }