feat: 将 HumanBreak 新内容移植入该样板

This commit is contained in:
AncTe committed 2026-02-28 22:56:59 +08:00
1 parent 6bf32174d8
commit bea2725d6a
128 files changed
+11984 -573

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
import { createMaterial } from './material';
export function create() {
createMaterial();
}
export * from './material';
@@ -0,0 +1,454 @@
import {
IRect,
ITextureRenderable,
SizedCanvasImageSource
} from '@motajs/render-assets';
import {
AutotileConnection,
AutotileType,
BlockCls,
IAutotileConnection,
IAutotileProcessor,
IMaterialFramedData,
IMaterialManager
} from './types';
import { isNil } from 'lodash-es';
interface ConnectedAutotile {
readonly lt: Readonly<IRect>;
readonly rt: Readonly<IRect>;
readonly rb: Readonly<IRect>;
readonly lb: Readonly<IRect>;
}
export interface IAutotileData {
/** 图像源 */
readonly source: SizedCanvasImageSource;
/** 自动元件帧数 */
readonly frames: number;
}
/** 3x4 自动元件的连接映射,元组表示将对应大小的自动元件按照格子 1/4 大小切分后对应的索引位置 */
const connectionMap3x4 = new Map<number, [number, number, number, number]>();
/** 2x3 自动元件的连接映射,元组表示将对应大小的自动元件按照格子 1/4 大小切分后对应的索引位置 */
const connectionMap2x3 = new Map<number, [number, number, number, number]>();
/** 3x4 自动元件各方向连接的矩形映射 */
const rectMap3x4 = new Map<number, ConnectedAutotile>();
/** 2x3 自动元件各方向连接的矩形映射 */
const rectMap2x3 = new Map<number, ConnectedAutotile>();
/** 不重复连接映射,用于平铺自动元件,一共 48 种 */
const distinctConnectionMap = new Map<number, number>();
export class AutotileProcessor implements IAutotileProcessor {
/** 自动元件父子关系映射,子元件 -> 父元件 */
readonly parentMap: Map<number, number> = new Map();
/** 自动元件父子关系映射,父元件 -> 子元件列表 */
readonly childMap: Map<number, Set<number>> = new Map();
constructor(readonly manager: IMaterialManager) {}
private ensureChildSet(num: number) {
const set = this.childMap.get(num);
if (set) return set;
const ensure = new Set<number>();
this.childMap.set(num, ensure);
return ensure;
}
setConnection(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 0b1110_1111;
} else if (index === length - 1) {
return 0b1111_1110;
} else {
return 0b1110_1110;
}
}
// 如果地图宽度只有 1
if (width === 1) {
if (index === 0) {
return 0b1111_1011;
} else if (index === length - 1) {
return 0b1011_1111;
} else {
return 0b1011_1011;
}
}
// 正常地图
const lastLine = length - width;
const x = index % width;
// 四个角,左上,右上,右下,左下
if (index === 0) {
return 0b1110_0011;
} else if (index === width - 1) {
return 0b1111_1000;
} else if (index === length - 1) {
return 0b0011_1110;
} else if (index === lastLine) {
return 0b1000_1111;
}
// 四条边,上,右,下,左
else if (index < width) {
return 0b1110_0000;
} else if (x === width - 1) {
return 0b0011_1000;
} else if (index > lastLine) {
return 0b0000_1110;
} else if (x === 0) {
return 0b1000_0011;
}
// 不在边缘
else {
return 0b0000_0000;
}
}
connect(
array: Uint32Array,
index: number,
width: number
): IAutotileConnection {
const block = array[index];
if (block === 0) {
return {
connection: 0,
center: 0
};
}
let res: number = this.connectEdge(array.length, index, width);
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 || childList.size === 0) {
// 不包含子元件,那么直接跟相同的连接
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
};
}
updateConnectionFor(
connection: number,
center: number,
target: number,
direction: AutotileConnection
): number {
const childList = this.childMap.get(center);
if (!childList || !childList.has(target)) {
return connection & ~direction;
} else {
return connection | direction;
}
}
/**
* 检查贴图是否是一个自动元件
* @param tile 贴图数据
*/
private checkAutotile(tile: IMaterialFramedData) {
if (tile.cls !== BlockCls.Autotile) return false;
const { texture, frames } = tile;
if (texture.width !== 96 * frames) return false;
if (texture.height === 128 || texture.height === 144) return true;
else return false;
}
render(autotile: number, connection: number): ITextureRenderable | null {
const tile = this.manager.getTile(autotile);
if (!tile) return null;
if (!this.checkAutotile(tile)) return null;
return this.renderWithoutCheck(tile, connection);
}
renderWith(
tile: IMaterialFramedData,
connection: number
): ITextureRenderable | null {
if (!this.checkAutotile(tile)) return null;
return this.renderWithoutCheck(tile, connection);
}
renderWithoutCheck(
tile: IMaterialFramedData,
connection: number
): ITextureRenderable | null {
const { texture } = tile;
const size = texture.height === 32 * 48 ? 32 : 48;
const index = distinctConnectionMap.get(connection);
if (isNil(index)) return null;
const { rect } = texture.render();
return {
source: texture.source,
rect: { x: rect.x, y: rect.y + size * index, w: size, h: size }
};
}
*renderAnimated(
autotile: number,
connection: number
): Generator<ITextureRenderable, void> {
const tile = this.manager.getTile(autotile);
if (!tile) return;
yield* this.renderAnimatedWith(tile, connection);
}
*renderAnimatedWith(
tile: IMaterialFramedData,
connection: number
): Generator<ITextureRenderable, void> {
if (!this.checkAutotile(tile)) return;
const { texture, frames } = tile;
const size = texture.height === 128 ? 32 : 48;
const index = distinctConnectionMap.get(connection);
if (isNil(index)) return;
for (let i = 0; i < frames; i++) {
yield {
source: texture.source,
rect: { x: i * size, y: size * index, w: size, h: size }
};
}
}
/**
* 将自动元件图片展平,平铺存储 48 种样式,此时可以只通过一次绘制来绘制出自动元件,不需要四次绘制
* @param image 原始自动元件图片
*/
static flatten(image: IAutotileData): SizedCanvasImageSource | null {
const { source, frames } = image;
if (source.width !== frames * 96) return null;
if (source.height !== 128 && source.height !== 144) return null;
const type =
source.height === 128 ? AutotileType.Big3x4 : AutotileType.Small2x3;
const size = type === AutotileType.Big3x4 ? 32 : 48;
const width = frames * size;
const height = 48 * size;
// 画到画布上
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) return null;
const half = size / 2;
const map = type === AutotileType.Big3x4 ? rectMap3x4 : rectMap2x3;
const used = new Set<number>();
// 遍历每个组合
distinctConnectionMap.forEach((index, conn) => {
if (used.has(conn)) return;
used.add(conn);
const { lt, rt, rb, lb } = map.get(conn)!;
const y = index * size;
for (let i = 0; i < frames; i++) {
const x = i * size;
// prettier-ignore
ctx.drawImage(source, lt.x + i * 96, lt.y, lt.w, lt.h, x, y, half, half);
// prettier-ignore
ctx.drawImage(source, rt.x + i * 96, rt.y, rt.w, rt.h, x + half, y, half, half);
// prettier-ignore
ctx.drawImage(source, rb.x + i * 96, rb.y, rb.w, rb.h, x + half, y + half, half, half);
// prettier-ignore
ctx.drawImage(source, lb.x + i * 96, lb.y, lb.w, lb.h, x, y + half, half, half);
}
});
return canvas;
}
}
/**
* 映射自动元件连接
* @param target 输出映射对象
* @param mode 自动元件类型,1 表示 3x4,2 表示 2x3
*/
function mapAutotile(
target: Map<number, [number, number, number, number]>,
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 }
});
});
const usedRect: [number, number, number, number][] = [];
let flag = 0;
// 2x3 和 3x4 的自动元件连接方式一样,因此没必要映射两次
connectionMap2x3.forEach((conn, num) => {
const index = usedRect.findIndex(
used =>
used[0] === conn[0] &&
used[1] === conn[1] &&
used[2] === conn[2] &&
used[3] === conn[3]
);
if (index === -1) {
distinctConnectionMap.set(num, flag);
usedRect.push(conn.slice() as [number, number, number, number]);
flag++;
} else {
distinctConnectionMap.set(num, index);
}
});
}
@@ -0,0 +1,159 @@
import {
ITextureStore,
ITexture,
ITextureComposedData,
ITextureStreamComposer,
TextureMaxRectsStreamComposer,
SizedCanvasImageSource
} from '@motajs/render-assets';
import { IAssetBuilder, IMaterialGetter, ITrackedAssetData } from './types';
import { logger, PrivateListDirtyTracker } from '@motajs/common';
export class AssetBuilder implements IAssetBuilder {
readonly composer: ITextureStreamComposer<void> =
new TextureMaxRectsStreamComposer(4096, 4096, 0);
private output: ITextureStore | null = null;
private started: boolean = false;
private readonly trackedData: TrackedAssetData;
/** 当前的索引 */
private index: number = -1;
/** 贴图更新的 promise */
private pending: Promise<void> = Promise.resolve();
constructor(readonly materials: IMaterialGetter) {
this.trackedData = new TrackedAssetData(materials, this);
}
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 (data.index > this.index) {
this.output.addTexture(data.index, data.texture);
this.index = data.index;
}
}
this.pending = this.pending.then(() =>
this.trackedData.updateSource(data.index, data.texture.source)
);
return data;
}
private async updateSourceList(source: Set<ITextureComposedData>) {
for (const data of source) {
await this.trackedData.updateSource(
data.index,
data.texture.source
);
}
}
addTextureList(
texture: Iterable<ITexture>
): Iterable<ITextureComposedData> {
this.started = true;
const res = [...this.composer.add(texture)];
const toUpdate = new Set<ITextureComposedData>();
if (this.output) {
res.forEach(data => {
if (data.index > this.index) {
this.output!.addTexture(data.index, data.texture);
this.index = data.index;
toUpdate.add(data);
} else {
toUpdate.add(data);
}
});
}
this.pending = this.pending.then(() => this.updateSourceList(toUpdate));
return res;
}
tracked(): ITrackedAssetData {
return this.trackedData;
}
close(): void {
this.composer.close();
}
}
class TrackedAssetData
extends PrivateListDirtyTracker<number>
implements ITrackedAssetData
{
readonly sourceList: Map<number, ImageBitmap> = new Map();
readonly skipRef: Map<SizedCanvasImageSource, number> = new Map();
private originSourceMap: Map<number, SizedCanvasImageSource> = new Map();
private promises: Set<Promise<ImageBitmap>> = new Set();
constructor(
readonly materials: IMaterialGetter,
readonly builder: AssetBuilder
) {
super(0);
}
markDirty(index: number) {
if (index >= this.length) {
this.updateLength(index + 1);
}
this.dirty(index);
}
async updateSource(index: number, source: SizedCanvasImageSource) {
if (index >= this.length) {
this.updateLength(this.length + 1);
}
const origin = this.originSourceMap.get(index);
const prev = this.sourceList.get(index);
if (origin && origin !== source) {
this.skipRef.delete(origin);
}
if (prev) {
this.skipRef.delete(prev);
}
this.originSourceMap.set(index, source);
if (source instanceof ImageBitmap) {
if (this.skipRef.has(source)) return;
this.sourceList.set(index, source);
this.skipRef.set(source, index);
} else {
const promise = createImageBitmap(source);
this.promises.add(promise);
const bitmap = await promise;
this.promises.delete(promise);
this.sourceList.set(index, bitmap);
this.skipRef.set(bitmap, index);
// 要把源也加到映射中,因为这里的 bitmap 与外部源并不同引用
this.skipRef.set(source, index);
}
this.dirty(index);
}
async then(): Promise<void> {
await Promise.all([...this.promises]);
}
close(): void {}
}
@@ -0,0 +1,140 @@
import { ITexture } from '@motajs/render-assets';
import { materials } from './ins';
import { IBlockIdentifier, IIndexedIdentifier } from './types';
import { isNil } from 'lodash-es';
function extractClsBlocks<C extends Exclude<Cls, 'tileset'>>(
cls: C,
map: Record<string, number>,
icons: Record<string, number>
): IBlockIdentifier[] {
const max = Math.max(...Object.values(icons));
const arr = Array(max).fill(void 0);
for (const [key, value] of Object.entries(icons)) {
// 样板编辑器 bug 可能会导致多个 id 使用一个偏移,因此要判断下
if (!(key in map) || !isNil(arr[value])) continue;
const id = key as AllIdsOf<C>;
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<number>, map?: readonly (readonly number[])[]) {
if (!map) return;
map.forEach(line => {
line.forEach(v => {
if (v >= 10000) set.add(v);
});
});
}
function addAutotile(set: Set<number>, map?: readonly (readonly number[])[]) {
if (!map) return;
map.forEach(line => {
line.forEach(v => {
const id = core.maps.blocksInfo[v as keyof NumberToId];
if (id?.cls === 'autotile') set.add(v);
});
});
}
/**
* 兼容旧版加载
*/
export function fallbackLoad() {
// 基本素材
const icons = core.icons.icons;
const images = core.material.images;
const idNumMap: Record<string, number> = {};
for (const [key, value] of Object.entries(core.maps.blocksInfo)) {
const num = Number(key);
idNumMap[value.id] = Number(num);
if (!isNil(value.animate)) {
materials.setDefaultFrame(num, value.animate - 1);
}
}
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, 32);
materials.addRowAnimate(images.enemys, enemys, 32);
materials.addRowAnimate(images.npcs, npcs, 32);
materials.addRowAnimate(images.enemy48, enemy48, 48);
materials.addRowAnimate(images.npc48, npc48, 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 });
});
// 地图上出现过的 tileset
const tilesetSet = new Set<number>();
const autotileSet = new Set<number>();
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);
addAutotile(autotileSet, floor.bgmap);
addAutotile(autotileSet, floor.bg2map);
addAutotile(autotileSet, floor.map);
addAutotile(autotileSet, floor.fgmap);
addAutotile(autotileSet, floor.fg2map);
});
const heroTextures: ITexture[] = [];
data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d.main.heroImages.forEach(v => {
const tex = materials.getImageByAlias(v);
if (tex) heroTextures.push(tex);
});
materials.buildAssets();
materials.cacheAutotileList(autotileSet);
materials.cacheTilesetList(tilesetSet);
materials.buildListToAsset(heroTextures);
}
@@ -0,0 +1,19 @@
import { loading } from '@user/data-base';
import { fallbackLoad } from './fallback';
import { createAutotile } from './autotile';
export function createMaterial() {
createAutotile();
loading.once('loaded', () => {
fallbackLoad();
loading.emit('assetBuilt');
});
}
export * from './autotile';
export * from './builder';
export * from './fallback';
export * from './ins';
export * from './manager';
export * from './types';
export * from './utils';
@@ -0,0 +1,5 @@
import { AutotileProcessor } from './autotile';
import { MaterialManager } from './manager';
export const materials = new MaterialManager();
export const autotile = new AutotileProcessor(materials);
@@ -0,0 +1,602 @@
import {
ITexture,
ITextureComposedData,
ITextureRenderable,
ITextureSplitter,
ITextureStore,
SizedCanvasImageSource,
Texture,
TextureGridSplitter,
TextureRowSplitter,
TextureStore
} from '@motajs/render-assets';
import {
IBlockIdentifier,
IMaterialData,
IMaterialManager,
IIndexedIdentifier,
IMaterialAssetData,
BlockCls,
IBigImageReturn,
IAssetBuilder,
IMaterialFramedData,
ITrackedAssetData
} from './types';
import { logger } from '@motajs/common';
import { getClsByString, getTextureFrame } from './utils';
import { isNil } from 'lodash-es';
import { AssetBuilder } from './builder';
import { AutotileProcessor } from './autotile';
interface TilesetCache {
/** 是否已经在贴图库中存在 */
readonly existed: boolean;
/** 贴图对象 */
readonly texture: ITexture;
}
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, ITextureComposedData> = new Map();
/** 贴图到图集索引的映射 */
readonly assetMap: Map<ITexture, number> = new Map();
/** 带有脏标记追踪的图集对象 */
readonly trackedAsset: ITrackedAssetData;
/** 大怪物数据 */
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;
/** 图块 id 到图块数字的映射 */
readonly idNumMap: Map<string, number> = new Map();
/** 图块数字到图块 id 的映射 */
readonly numIdMap: Map<number, string> = new Map();
/** 图块数字到图块类型的映射 */
readonly clsMap: Map<number, BlockCls> = new Map();
/** 图块的默认帧数 */
readonly defaultFrames: Map<number, number> = 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 = new AssetBuilder(this);
this.assetBuilder.pipe(this.assetStore);
this.trackedAsset = this.assetBuilder.tracked();
}
/**
* 添加由分割器和图块映射组成的图像源贴图
* @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) => {
if (!map[i]) {
return {
store,
texture: v,
identifier: -1,
alias: '@internal-unknown'
};
}
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
): void {
this.autotileSource.set(identifier.num, source);
this.tileStore.alias(identifier.num, identifier.id);
this.clsMap.set(identifier.num, BlockCls.Autotile);
}
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;
}
setDefaultFrame(identifier: number, defaultFrame: number): void {
this.defaultFrames.set(identifier, defaultFrame);
const bigImageData = this.bigImageData.get(identifier);
if (bigImageData) {
bigImageData.defaultFrame = defaultFrame;
}
}
getDefaultFrame(identifier: number): number {
return this.defaultFrames.get(identifier) ?? -1;
}
getTile(identifier: number): Readonly<IMaterialFramedData> | null {
if (identifier < 10000) {
const cls = this.clsMap.get(identifier) ?? BlockCls.Unknown;
if (
cls === BlockCls.Autotile &&
this.autotileSource.has(identifier)
) {
this.cacheAutotile(identifier);
}
const texture = this.tileStore.getTexture(identifier);
if (!texture) return null;
return {
texture,
cls,
offset: 32,
frames: getTextureFrame(cls, texture),
defaultFrame: this.defaultFrames.get(identifier) ?? -1
};
} else {
const texture = this.cacheTileset(identifier);
if (!texture) return null;
return {
texture,
cls: BlockCls.Tileset,
offset: 32,
frames: 1,
defaultFrame: -1
};
}
}
getTileset(identifier: number): ITexture | null {
return this.tilesetStore.getTexture(identifier);
}
getImage(identifier: number): ITexture | null {
return this.imageStore.getTexture(identifier);
}
getTileByAlias(alias: string): Readonly<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): TilesetCache | null {
const texture = this.tileStore.getTexture(identifier);
if (texture) return { existed: true, 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 { existed: false, texture: newTexture };
}
/**
* 检查图集状态,如果已存在图集则标记为脏,否则新增图集
* @param data 图集数据
*/
private checkAssetDirty(data: ITextureComposedData) {
if (!this.built) return;
const asset = this.assetDataStore.get(data.index);
if (!asset) {
// 如果有新图集,需要添加
const alias = `asset-${data.index}`;
this.assetStore.alias(data.index, alias);
this.assetDataStore.set(data.index, data);
}
}
/**
* 将指定的贴图列表转换至指定的图集数据中
* @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;
const { existed, texture } = newTexture;
if (existed) return texture;
// 缓存贴图
this.tileStore.addTexture(identifier, texture);
this.idNumMap.set(`X${identifier}`, identifier);
this.numIdMap.set(identifier, `X${identifier}`);
const data = this.assetBuilder.addTexture(texture);
texture.toAsset(data);
this.checkAssetDirty(data);
return texture;
}
cacheTilesetList(
identifierList: Iterable<number>
): Iterable<ITexture | null> {
const arr = [...identifierList];
const toAdd: ITexture[] = [];
arr.forEach(v => {
const newTexture = this.getTilesetOwnTexture(v);
if (!newTexture) return;
const { existed, texture } = newTexture;
if (existed) return;
toAdd.push(texture);
this.tileStore.addTexture(v, texture);
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.autotileSource.delete(identifier);
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);
this.autotileSource.delete(v);
});
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;
return this.buildListToAsset(this.tileStore.values());
}
buildToAsset(texture: ITexture): IMaterialAssetData {
const data = this.assetBuilder.addTexture(texture);
const assetData: IMaterialAssetData = {
data: data,
identifier: data.index,
alias: `asset-${data.index}`,
store: this.assetStore
};
this.checkAssetDirty(data);
texture.toAsset(data);
return assetData;
}
buildListToAsset(
texture: Iterable<ITexture>
): Iterable<IMaterialAssetData> {
const data = this.assetBuilder.addTextureList(texture);
const arr = [...data];
const res: IMaterialAssetData[] = [];
arr.forEach(v => {
const alias = `asset-${v.index}`;
if (!this.assetDataStore.has(v.index)) {
this.assetDataStore.set(v.index, 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);
});
arr.forEach(v => {
this.checkAssetDirty(v);
});
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.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,
defaultFrame: this.defaultFrames.get(identifier) ?? -1
};
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): Readonly<IMaterialFramedData> | null {
return this.bigImageData.get(identifier) ?? null;
}
getBigImageByAlias(alias: string): Readonly<IMaterialFramedData> | null {
const identifier = this.idNumMap.get(alias);
if (isNil(identifier)) return null;
return this.bigImageData.get(identifier) ?? null;
}
getIfBigImage(identifier: number): Readonly<IMaterialFramedData> | null {
const bigImage = this.bigImageData.get(identifier);
if (bigImage) return bigImage;
else return this.getTile(identifier);
}
assetContainsTexture(texture: ITexture): boolean {
return this.trackedAsset.skipRef.has(texture.source);
}
getTextureAsset(texture: ITexture): number | undefined {
return this.trackedAsset.skipRef.get(texture.source);
}
}
@@ -0,0 +1,528 @@
import { IDirtyTracker, IDirtyMarker } from '@motajs/common';
import {
ITexture,
ITextureComposedData,
ITextureRenderable,
ITextureStore,
SizedCanvasImageSource
} from '@motajs/render-assets';
export const enum BlockCls {
Unknown,
Terrains,
Animates,
Enemys,
Npcs,
Items,
Enemy48,
Npc48,
Tileset,
Autotile
}
export const enum AutotileType {
Small2x3,
Big3x4
}
export const enum AutotileConnection {
LeftUp = 0b1000_0000,
Up = 0b0100_0000,
RightUp = 0b0010_0000,
Right = 0b0001_0000,
RightDown = 0b0000_1000,
Down = 0b0000_0100,
LeftDown = 0b0000_0010,
Left = 0b0000_0001
}
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 IBigImageReturn {
/** 大怪物贴图在 store 中的标识符 */
readonly identifier: number;
/** 存储大怪物贴图的存储对象 */
readonly store: ITextureStore;
}
export interface IMaterialFramedData {
/** 贴图对象 */
texture: ITexture;
/** 图块类型 */
cls: BlockCls;
/** 贴图总帧数 */
frames: number;
/** 每帧的横向偏移量 */
offset: number;
/** 默认帧数 */
defaultFrame: number;
}
export interface IMaterialAsset
extends IDirtyTracker<boolean>,
IDirtyMarker<void> {
/** 图集的贴图数据 */
readonly data: ITextureComposedData;
}
export interface IAutotileProcessor {
/** 该自动元件处理器使用的素材管理器 */
readonly manager: IMaterialManager;
/**
* 设置一个自动元件的特殊连接方式,设置后当前自动元件将会单方面与目标元件连接,
* 一个自动元件可以与多个自动元件有特殊连接
* @param autotile 自动元件
* @param target 当前自动元件将会连接至的自动元件
*/
setConnection(autotile: number, target: number): void;
/**
* 获取自动元件的连接情况
* @param array 地图图块数组
* @param index 自动元件图块所在的索引
* @param width 地图每一行的宽度
*/
connect(
array: Uint32Array,
index: number,
width: number
): IAutotileConnection;
/**
* 检查一个图块与指定方向的连接方式
* @param connection 当前的连接
* @param center 中心点的图块数字
* @param target 连接点的图块数字
* @param direction 连接点的方向
* @returns 经过连接后的连接数字
*/
updateConnectionFor(
connection: number,
center: number,
target: number,
direction: AutotileConnection
): number;
/**
* 根据图块数字,获取指定自动元件经过连接的可渲染对象
* @param autotile 自动元件的图块数字
* @param connection 连接方式,上方连接是第一位,顺时针旋转位次依次升高
* @returns 连接方式的可渲染对象,可以通过偏移量依次获取其他帧
*/
render(autotile: number, connection: number): ITextureRenderable | null;
/**
* 根据图块贴图对象,获取指定自动元件经过连接的可渲染对象
* @param tile 自动元件的图块贴图数据
* @param connection 连接方式,上方连接是第一位,顺时针旋转位次依次升高
* @returns 连接方式的可渲染对象,可以通过偏移量依次获取其他帧
*/
renderWith(
tile: Readonly<IMaterialFramedData>,
connection: number
): ITextureRenderable | null;
/**
* 根据图块贴图对象,获取指定自动元件经过连接的可渲染对象,但是会假设传入的图块就是自动元件,不做不必要的判断
* @param tile 自动元件的图块贴图数据
* @param connection 连接方式,上方连接是第一位,顺时针旋转位次依次升高
* @returns 连接方式的可渲染对象,可以通过偏移量依次获取其他帧
*/
renderWithoutCheck(
tile: Readonly<IMaterialFramedData>,
connection: number
): ITextureRenderable | null;
/**
* 根据图块数字,获取指定自动元件经过链接的动态可渲染对象
* @param autotile 自动元件的图块数字
* @param connection 自动元件的连接方式
* @returns 生成器,每一个输出代表每一帧的渲染对象,不同自动元件的帧数可能不同
*/
renderAnimated(
autotile: number,
connection: number
): Generator<ITextureRenderable, void>;
/**
* 根据图块贴图对象,获取指定自动元件经过链接的动态可渲染对象
* @param autotile 自动元件的图块数字
* @param connection 自动元件的连接方式
* @returns 生成器,每一个输出代表每一帧的渲染对象,不同自动元件的帧数可能不同
*/
renderAnimatedWith(
tile: Readonly<IMaterialFramedData>,
connection: number
): Generator<ITextureRenderable, void>;
}
export interface IMaterialGetter {
/**
* 根据图块数字获取图块,可以获取额外素材,会自动将未缓存的额外素材缓存
* @param identifier 图块的图块数字
*/
getTile(identifier: number): Readonly<IMaterialFramedData> | null;
/**
* 根据图块标识符获取图块类型
* @param identifier 图块标识符,即图块数字
*/
getBlockCls(identifier: number): BlockCls;
/**
* 判断一个图块是否包含 `bigImage` 贴图,即是否是大怪物
* @param identifier 图块标识符,即图块数字
*/
isBigImage(identifier: number): boolean;
/**
* 根据图块标识符获取一个图块的 `bigImage` 贴图
* @param identifier 图块标识符,即图块数字
*/
getBigImage(identifier: number): Readonly<IMaterialFramedData> | null;
/**
* 根据图块标识符,首先判断是否是 `bigImage` 贴图,如果是,则返回 `bigImage` 贴图,
* 否则返回普通贴图。如果图块不存在,则返回 `null`
* @param identifier 图块标识符,即图块数字
*/
getIfBigImage(identifier: number): Readonly<IMaterialFramedData> | null;
/**
* 根据标识符获取图集信息
* @param identifier 图集的标识符
*/
getAsset(identifier: number): ITextureComposedData | null;
/**
* 根据额外素材索引获取额外素材
* @param identifier 额外素材的索引
*/
getTileset(identifier: number): ITexture | null;
/**
* 根据图片的索引获取图片
* @param identifier 图片的索引
*/
getImage(identifier: number): ITexture | null;
}
export interface IMaterialAliasGetter {
/**
* 根据图块 id 获取图块,可以获取额外素材,会自动将未缓存的额外素材缓存
* @param alias 图块 id
*/
getTileByAlias(alias: string): Readonly<IMaterialFramedData> | null;
/**
* 根据额外素材名称获取额外素材
* @param alias 额外素材名称
*/
getTilesetByAlias(alias: string): ITexture | null;
/**
* 根据图片名称获取图片
* @param alias 图片名称
*/
getImageByAlias(alias: string): ITexture | null;
/**
* 根据别名获取图集信息
* @param alias 图集的别名
*/
getAssetByAlias(alias: string): ITextureComposedData | null;
/**
* 根据图块别名获取图块类型
* @param alias 图块别名,即图块的 id
*/
getBlockClsByAlias(alias: string): BlockCls;
/**
* 根据图块别名获取一个图块的 `bigImage` 贴图
* @param alias 图块别名,即图块的 id
*/
getBigImageByAlias(alias: string): Readonly<IMaterialFramedData> | null;
}
export interface IMaterialManager
extends IMaterialGetter,
IMaterialAliasGetter {
/** 贴图存储,把 terrains 等内容单独分开存储 */
readonly tileStore: ITextureStore;
/** tilesets 贴图存储,每个 tileset 是一个贴图对象 */
readonly tilesetStore: ITextureStore;
/** 存储注册的图像的存储对象 */
readonly imageStore: ITextureStore;
/** 图集存储,将常用贴图存入其中 */
readonly assetStore: ITextureStore;
/** bigImage 存储,存储大怪物数据 */
readonly bigImageStore: ITextureStore;
/** 图集信息存储 */
readonly assetDataStore: Iterable<[number, ITextureComposedData]>;
/** 带有脏标记追踪的图集信息 */
readonly trackedAsset: ITrackedAssetData;
/** 图块类型映射 */
readonly clsMap: Map<number, BlockCls>;
/**
* 添加网格类型的贴图,包括 terrains 和 items 类型
* @param source 图像源
* @param map 贴图字符串 id 与图块数字映射,按照先从左到右,再从上到下的顺序映射
*/
addGrid(
source: SizedCanvasImageSource,
map: ArrayLike<IBlockIdentifier>
): Iterable<IMaterialData>;
/**
* 添加行动画的贴图,包括 animates enemys npcs enemy48 npc48 类型
* @param source 图像源
* @param map 贴图字符串 id 与图块数字映射,按从上到下的顺序映射
* @param frames 每一行的帧数
* @param height 每一行的高度
*/
addRowAnimate(
source: SizedCanvasImageSource,
map: ArrayLike<IBlockIdentifier>,
frames: number,
height: number
): Iterable<IMaterialData>;
/**
* 添加自动元件
* @param source 图像源
* @param identifier 自动元件的字符串 id 及图块数字
* @returns 由于自动元件是懒加载的,因此不会返回任何东西
*/
addAutotile(
source: SizedCanvasImageSource,
identifier: IBlockIdentifier
): void;
/**
* 添加一个 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 图块标识符,即图块数字
* @param defaultFrame 图块的默认帧数
*/
setDefaultFrame(identifier: number, defaultFrame: number): void;
/**
* 获取图块的默认帧数,-1 表示正常动画,非负整数表示默认使用指定帧数,除非单独指定
* @param identifier 图块标识符,即图块数字
*/
getDefaultFrame(identifier: number): number;
/**
* 缓存某个 tileset,当需要缓存多个时,请使用 {@link cacheTilesetList} 方法
* @param identifier tileset 的标识符,即图块数字
*/
cacheTileset(identifier: number): ITexture | null;
/**
* 缓存一系列 tileset
* @param identifierList 标识符列表,即图块数字列表
*/
cacheTilesetList(
identifierList: Iterable<number>
): Iterable<ITexture | null>;
/**
* 缓存某个自动元件,当需要缓存多个时,请使用 {@link cacheAutotileList} 方法
* @param identifier 自动元件标识符,即图块数字
*/
cacheAutotile(identifier: number): ITexture | null;
/**
* 缓存一系列自动元件
* @param identifierList 自动元件标识符列表,即图块数字列表
*/
cacheAutotileList(
identifierList: Iterable<number>
): Iterable<ITexture | null>;
/**
* 把常用素材打包成为图集形式供后续使用
*/
buildAssets(): Iterable<IMaterialAssetData>;
/**
* 将指定贴图打包进图集
* @param texture 贴图对象
*/
buildToAsset(texture: ITexture): IMaterialAssetData;
/**
* 将一系列贴图打包进贴图对象
* @param texture 贴图列表
*/
buildListToAsset(texture: Iterable<ITexture>): Iterable<IMaterialAssetData>;
/**
* 根据图块标识符在图集中获取对应的可渲染对象
* @param identifier 图块标识符,即图块数字
*/
getRenderable(identifier: number): ITextureRenderable | null;
/**
* 根据图块别名在图集中获取对应的可渲染对象
* @param alias 图块的别名,即图块的 id
*/
getRenderableByAlias(alias: string): ITextureRenderable | null;
/**
* 根据图块别名获取图块标识符,即图块数字
* @param alias 图块别名,即图块的 id
*/
getIdentifierByAlias(alias: string): number | undefined;
/**
* 根据图块标识符获取图块别名,即图块的 id
* @param identifier 图块标识符,即图块数字
*/
getAliasByIdentifier(identifier: number): string | undefined;
/**
* 设置一个图块的 `bigImage` 贴图,即大怪物贴图,但不止怪物能用
* @param identifier 图块标识符,即图块数字
* @param image `bigImage` 对应的贴图对象
* @param frames `bigImage` 的帧数,即贴图有多少帧
*/
setBigImage(
identifier: number,
image: ITexture,
frames: number
): IBigImageReturn;
/**
* 当前的所有图集中是否包含指定的贴图对象
* @param texture 贴图对象
*/
assetContainsTexture(texture: ITexture): boolean;
/**
* 获取指定贴图对象所属的图集索引
* @param texture 贴图对象
*/
getTextureAsset(texture: ITexture): number | undefined;
}
export interface IAssetBuilder {
/**
* 将图集打包器输出至指定贴图存储对象,只能输出到一个存储对象中,设置多个仅最后一个有效
* @param store 贴图存储对象
*/
pipe(store: ITextureStore): void;
/**
* 添加贴图对象至打包器
* @param texture 贴图对象
* @returns 当前打包的贴图对象对应的组合数据
*/
addTexture(texture: ITexture): ITextureComposedData;
/**
* 添加一个贴图对象列表至打包器
* @param texture 贴图对象列表
* @returns 当前打包的贴图列表的组合数据,每一项代表一个图集,只包含使用到的图集,之前已经打包完成的将不会在列表内
*/
addTextureList(texture: Iterable<ITexture>): Iterable<ITextureComposedData>;
/**
* 获取可追踪贴图对象
*/
tracked(): ITrackedAssetData;
/**
* 结束此打包器
*/
close(): void;
}
export interface ITrackedAssetData extends IDirtyTracker<Set<number>> {
/** 图像源列表 */
readonly sourceList: Map<number, ImageBitmap>;
/**
* 贴图引用跳接,`ImageBitmap` 的传递性能远好于其他类型,而贴图图集为了能够动态增加内容会使用画布类型,
* 因此需要把贴图生成为额外的 `ImageBitmap`,并提供引用跳接映射。值代表在 `sourceList` 中的索引。
*/
readonly skipRef: Map<SizedCanvasImageSource, number>;
/** 贴图数据 */
readonly materials: IMaterialGetter;
/**
* 取消使用此图集,释放相关资源
*/
close(): void;
/**
* 等待所有打包操作结束
*/
then(): Promise<void>;
}
@@ -0,0 +1,47 @@
import { ITexture } from '@motajs/render-assets';
import { BlockCls } from './types';
export function getClsByString(cls: Cls): BlockCls {
switch (cls) {
case 'terrains':
return BlockCls.Terrains;
case 'animates':
return BlockCls.Animates;
case 'autotile':
return BlockCls.Autotile;
case 'enemys':
return BlockCls.Enemys;
case 'items':
return BlockCls.Items;
case 'npcs':
return BlockCls.Npcs;
case 'npc48':
return BlockCls.Npc48;
case 'enemy48':
return BlockCls.Enemy48;
case 'tileset':
return BlockCls.Tileset;
default:
return BlockCls.Unknown;
}
}
export function getTextureFrame(cls: BlockCls, texture: ITexture) {
switch (cls) {
case BlockCls.Animates:
case BlockCls.Enemy48:
case BlockCls.Npc48:
return 4;
case BlockCls.Autotile:
return texture.width === 384 ? 4 : 1;
case BlockCls.Enemys:
case BlockCls.Npcs:
return 2;
case BlockCls.Items:
case BlockCls.Terrains:
case BlockCls.Tileset:
return 1;
case BlockCls.Unknown:
return 0;
}
}