From b3ed39f800eaa49105143a82023fe5d3a4267227 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Sun, 26 Oct 2025 16:21:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9B=BE=E5=9D=97=E6=89=93=E5=8C=85?= =?UTF-8?q?=E8=BF=9B=E5=9B=BE=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client-base/src/material/builder.ts | 1 + .../client-base/src/material/fallback.ts | 108 +++++++++++++ .../client-base/src/material/index.ts | 9 +- .../client-base/src/material/manager.ts | 146 ++++++++++++++---- .../client-base/src/material/types.ts | 20 ++- packages/common/src/logger.json | 1 + packages/render-assets/src/streamComposer.ts | 41 ++--- 7 files changed, 272 insertions(+), 54 deletions(-) create mode 100644 packages-user/client-base/src/material/fallback.ts diff --git a/packages-user/client-base/src/material/builder.ts b/packages-user/client-base/src/material/builder.ts index 917c349..a9473bc 100644 --- a/packages-user/client-base/src/material/builder.ts +++ b/packages-user/client-base/src/material/builder.ts @@ -27,6 +27,7 @@ export class AssetBuilder implements IAssetBuilder { 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); 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 index 756dd11..39ede5d 100644 --- a/packages-user/client-base/src/material/index.ts +++ b/packages-user/client-base/src/material/index.ts @@ -1,3 +1,10 @@ -export function createMaterial() {} +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 index a142b4a..b2b97c6 100644 --- a/packages-user/client-base/src/material/manager.ts +++ b/packages-user/client-base/src/material/manager.ts @@ -53,6 +53,14 @@ export class MaterialManager implements IMaterialManager { /** 大怪物贴图的标识符 */ private bigImageId: number = 0; + /** 当前 tileset 索引 */ + private nowTilesetIndex: number = -1; + /** 当前 tileset 偏移 */ + private nowTilesetOffset: number = 0; + + constructor() { + this.assetBuilder.pipe(this.assetStore); + } /** * 添加由分割器和图块映射组成的图像源贴图 @@ -109,14 +117,15 @@ export class MaterialManager implements IMaterialManager { addRowAnimate( source: SizedCanvasImageSource, map: ArrayLike, - frames: number + frames: number, + height: number ): Iterable { return this.addMappedSource( source, map, this.tileStore, this.rowSplitter, - 32, + height, (tex: ITexture) => { tex.animated(new TextureColumnAnimater(), frames); } @@ -143,10 +152,34 @@ export class MaterialManager implements IMaterialManager { addTileset( source: SizedCanvasImageSource, identifier: IIndexedIdentifier - ): IMaterialData { + ): 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, @@ -173,7 +206,11 @@ export class MaterialManager implements IMaterialManager { } getTile(identifier: number): ITexture | null { - return this.tileStore.getTexture(identifier); + if (identifier < 10000) { + return this.tileStore.getTexture(identifier); + } else { + return this.cacheTileset(identifier); + } } getTileset(identifier: number): ITexture | null { @@ -185,7 +222,11 @@ export class MaterialManager implements IMaterialManager { } getTileByAlias(alias: string): ITexture | null { - return this.tileStore.fromAlias(alias); + if (/X\d{5,}/.test(alias)) { + return this.cacheTileset(parseInt(alias.slice(1))); + } else { + return this.tileStore.fromAlias(alias); + } } getTilesetByAlias(alias: string): ITexture | null { @@ -196,6 +237,71 @@ export class MaterialManager implements IMaterialManager { 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()); @@ -204,6 +310,7 @@ export class MaterialManager implements IMaterialManager { 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, @@ -234,34 +341,7 @@ export class MaterialManager implements IMaterialManager { return this.tileStore.getTexture(identifier); } if (identifier < 10000) return null; - const texture = this.tileStore.getTexture(identifier); - if (texture) return texture; - // 如果 tileset 不存在,那么执行缓存操作 - const offset = Math.floor(identifier / 10000); - const index = this.tilesetOffsetMap.get(offset); - 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); - // 缓存贴图 - 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; + return this.cacheTileset(identifier); } getRenderable(identifier: number): ITextureRenderable | null { diff --git a/packages-user/client-base/src/material/types.ts b/packages-user/client-base/src/material/types.ts index 6af5b18..9c90f52 100644 --- a/packages-user/client-base/src/material/types.ts +++ b/packages-user/client-base/src/material/types.ts @@ -150,11 +150,13 @@ export interface IMaterialManager { * @param source 图像源 * @param map 贴图字符串 id 与图块数字映射,按从上到下的顺序映射 * @param frames 每一行的帧数 + * @param height 每一行的高度 */ addRowAnimate( source: SizedCanvasImageSource, map: ArrayLike, - frames: number + frames: number, + height: number ): Iterable; /** @@ -175,7 +177,7 @@ export interface IMaterialManager { addTileset( source: SizedCanvasImageSource, identifier: IIndexedIdentifier - ): IMaterialData; + ): IMaterialData | null; /** * 添加一个图片 @@ -223,6 +225,20 @@ export interface IMaterialManager { */ getImageByAlias(alias: string): ITexture | null; + /** + * 缓存某个 tileset + * @param identifier tileset 的标识符,即图块数字 + */ + cacheTileset(identifier: number): ITexture | null; + + /** + * 缓存一系列 tileset + * @param identifierList 标识符列表,即图块数字列表 + */ + cacheTilesetList( + identifierList: Iterable + ): Iterable; + /** * 把常用素材打包成为图集形式供后续使用 */ diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index 151f357..b3f6ea2 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -109,6 +109,7 @@ "75": "Material count splitted by grid does not match alias count, some material may have no alias or some alias may not map to a texture. Splitted: $1, alias: $2.", "76": "Cannot pipe texture store when asset builder is started.", "77": "Texture is clipped to an area of 0. Ensure that the texture contains your clip rect and clip rect's area is not zero.", + "78": "Adding tileset source must follow index order.", "1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency.", "1101": "Cannot add new effect to point effect instance, for there's no more reserve space for it. Please increase the max count of the instance." } diff --git a/packages/render-assets/src/streamComposer.ts b/packages/render-assets/src/streamComposer.ts index fad3797..3f489b3 100644 --- a/packages/render-assets/src/streamComposer.ts +++ b/packages/render-assets/src/streamComposer.ts @@ -72,17 +72,23 @@ export class TextureGridStreamComposer implements ITextureStreamComposer { this.nowCtx.drawImage(source, cx, cy, cw, ch, x, y, cw, ch); this.nowMap.set(tex, { x, y, w: cw, h: ch }); - const data: ITextureComposedData = { - index: this.outputIndex, - texture: this.nowTexture, - assetMap: this.nowMap - }; - yield data; - if (++index === max) { + const data: ITextureComposedData = { + index: this.outputIndex, + texture: this.nowTexture, + assetMap: this.nowMap + }; + yield data; this.nextCanvas(); } } + + const data: ITextureComposedData = { + index: this.outputIndex, + texture: this.nowTexture, + assetMap: this.nowMap + }; + yield data; } close(): void { @@ -102,12 +108,12 @@ export class TextureMaxRectsStreamComposer readonly packer: MaxRectsPacker; private outputIndex: number = 0; - private nowTexture: ITexture; + private nowTexture!: ITexture; - private nowCanvas: HTMLCanvasElement; - private nowCtx: CanvasRenderingContext2D; - private nowMap: Map>; - private nowBin: number; + private nowCanvas!: HTMLCanvasElement; + private nowCtx!: CanvasRenderingContext2D; + private nowMap!: Map>; + private nowBin: number = 0; /** * 使用 Max Rects 算法执行贴图整合。输出的纹理的图像源将会是不同的画布。 @@ -129,16 +135,14 @@ export class TextureMaxRectsStreamComposer options ); - this.nowCanvas = document.createElement('canvas'); - this.nowCtx = this.nowCanvas.getContext('2d')!; - this.nowMap = new Map(); - this.nowBin = 0; - this.nowTexture = new Texture(this.nowCanvas); + this.nextCanvas(); } private nextCanvas() { this.nowCanvas = document.createElement('canvas'); this.nowCtx = this.nowCanvas.getContext('2d')!; + this.nowCanvas.width = this.maxWidth; + this.nowCanvas.height = this.maxHeight; this.nowMap = new Map(); this.outputIndex++; this.nowTexture = new Texture(this.nowCanvas); @@ -176,6 +180,7 @@ export class TextureMaxRectsStreamComposer const { x: cx, y: cy, w: cw, h: ch } = rect; this.nowCtx.drawImage(source, cx, cy, cw, ch, v.x, v.y, cw, ch); }); + const data: ITextureComposedData = { index: this.outputIndex, texture: this.nowTexture, @@ -187,7 +192,7 @@ export class TextureMaxRectsStreamComposer } } - this.nowBin = bins.length; + this.nowBin = bins.length - 1; } close(): void {