mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-11-28 22:42:59 +08:00
548 lines
18 KiB
TypeScript
548 lines
18 KiB
TypeScript
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<number, SizedCanvasImageSource> = new Map();
|
||
|
||
/** 图集信息存储 */
|
||
readonly assetDataStore: Map<number, IMaterialAsset> = new Map();
|
||
/** 贴图到图集索引的映射 */
|
||
readonly assetMap: Map<ITexture, number> = new Map();
|
||
|
||
/** 大怪物数据 */
|
||
readonly bigImageData: Map<number, IMaterialFramedData> = new Map();
|
||
/** tileset 中 `Math.floor(id / 10000) + 1` 映射到 tileset 对应索引的映射,用于处理图块超出 10000 的 tileset */
|
||
readonly tilesetOffsetMap: Map<number, number> = new Map();
|
||
/** 图集打包器 */
|
||
readonly assetBuilder: IAssetBuilder = new AssetBuilder();
|
||
|
||
/** 图块 id 到图块数字的映射 */
|
||
readonly idNumMap: Map<string, number> = new Map();
|
||
/** 图块数字到图块 id 的映射 */
|
||
readonly numIdMap: Map<number, string> = new Map();
|
||
/** 图块数字到图块类型的映射 */
|
||
readonly clsMap: Map<number, BlockCls> = 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<T>(
|
||
source: SizedCanvasImageSource,
|
||
map: ArrayLike<IBlockIdentifier>,
|
||
store: ITextureStore,
|
||
splitter: ITextureSplitter<T>,
|
||
splitterData: T,
|
||
processTexture?: (tex: ITexture) => void
|
||
): Iterable<IMaterialData> {
|
||
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<IBlockIdentifier>
|
||
): Iterable<IMaterialData> {
|
||
return this.addMappedSource(
|
||
source,
|
||
map,
|
||
this.tileStore,
|
||
this.gridSplitter,
|
||
[32, 32]
|
||
);
|
||
}
|
||
|
||
addRowAnimate(
|
||
source: SizedCanvasImageSource,
|
||
map: ArrayLike<IBlockIdentifier>,
|
||
height: number
|
||
): Iterable<IMaterialData> {
|
||
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<number>
|
||
): Iterable<ITexture | null> {
|
||
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<number>
|
||
): Iterable<ITexture | null> {
|
||
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<IMaterialAssetData> {
|
||
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);
|
||
}
|
||
}
|