mirror of
https://github.com/motajs/template.git
synced 2026-09-22 00:00:17 +08:00
506 lines
16 KiB
TypeScript
506 lines
16 KiB
TypeScript
import {
|
|
ILoadProgressTotal,
|
|
LoadDataType,
|
|
ILoadTask,
|
|
LoadTask,
|
|
ILoadTaskProcessor
|
|
} from '@motajs/loader';
|
|
import {
|
|
CompressedUsage,
|
|
CustomLoadFunc,
|
|
ICompressedMotaAssetsData,
|
|
ICompressedMotaAssetsLoadList,
|
|
IMotaAssetsLoader
|
|
} from './types';
|
|
import JSZip from 'jszip';
|
|
import {
|
|
LoadAudioProcessor,
|
|
LoadFontProcessor,
|
|
LoadImageProcessor,
|
|
LoadTextProcessor,
|
|
LoadZipProcessor
|
|
} from '@user/data-base';
|
|
import { IMotaAudioContext, ISoundPlayer } from '@motajs/audio';
|
|
import { loading } from '@user/data-base';
|
|
import { IMaterialManager } from '../material';
|
|
import { ITextureSplitter, Texture, TextureRowSplitter } from '@motajs/render';
|
|
import { iconNames } from './data';
|
|
import { IMotaDataLoader } from '@user/data-base';
|
|
|
|
export class MotaAssetsLoader implements IMotaAssetsLoader {
|
|
/** 当前是否正在进行加载 */
|
|
loading: boolean = false;
|
|
/** 当前加载工作是否已经完成 */
|
|
loaded: boolean = false;
|
|
|
|
readonly imageProcessor: ILoadTaskProcessor<LoadDataType.Blob, ImageBitmap>;
|
|
readonly audioProcessor: ILoadTaskProcessor<
|
|
LoadDataType.Uint8Array,
|
|
AudioBuffer | null
|
|
>;
|
|
readonly fontProcessor: ILoadTaskProcessor<
|
|
LoadDataType.ArrayBuffer,
|
|
FontFace
|
|
>;
|
|
readonly textProcessor: ILoadTaskProcessor<LoadDataType.Text, string>;
|
|
readonly zipProcessor: ILoadTaskProcessor<LoadDataType.ArrayBuffer, JSZip>;
|
|
|
|
/** 素材索引 */
|
|
private materialsCounter: number = 0;
|
|
/** 贴图行分割器,用于处理遗留 `icons.png` */
|
|
private readonly rowSplitter: ITextureSplitter<number>;
|
|
|
|
constructor(
|
|
readonly progress: ILoadProgressTotal,
|
|
private readonly dataLoader: IMotaDataLoader,
|
|
private readonly ac: IMotaAudioContext,
|
|
private readonly sounds: ISoundPlayer<SoundIds>,
|
|
private readonly materials: IMaterialManager
|
|
) {
|
|
this.imageProcessor = new LoadImageProcessor();
|
|
this.audioProcessor = new LoadAudioProcessor(ac);
|
|
this.fontProcessor = new LoadFontProcessor();
|
|
this.textProcessor = new LoadTextProcessor();
|
|
this.zipProcessor = new LoadZipProcessor();
|
|
this.rowSplitter = new TextureRowSplitter();
|
|
}
|
|
|
|
//#region 其他处理
|
|
|
|
private splitMaterialIcons(image: ImageBitmap) {
|
|
const tex = new Texture(image);
|
|
const splitted = [...this.rowSplitter.split(tex, 32)];
|
|
for (let i = 0; i < splitted.length; i++) {
|
|
const name = iconNames[i] ? `icon-${iconNames[i]}` : `icons-${i}`;
|
|
// todo: 早晚删除 icons.png
|
|
const index = this.materialsCounter++;
|
|
this.materials.imageStore.addTexture(index, splitted[i]);
|
|
this.materials.imageStore.alias(index, name);
|
|
}
|
|
}
|
|
|
|
//#region 加载后处理
|
|
|
|
/**
|
|
* 当字体加载完成后的操作
|
|
* @param font 字体名称
|
|
* @param fontFace 字体 `FontFace` 对象
|
|
*/
|
|
private fontLoaded(font: string, fontFace: FontFace) {
|
|
const suffix = font.lastIndexOf('.');
|
|
const family = font.slice(0, suffix);
|
|
fontFace.family = family;
|
|
document.fonts.add(fontFace);
|
|
return Promise.resolve();
|
|
}
|
|
|
|
/**
|
|
* 图片加载完成后的操作
|
|
* @param name 图片名称
|
|
* @param image 图片的 `ImageBitmap`
|
|
*/
|
|
private customImagesLoaded(name: ImageIds, image: ImageBitmap) {
|
|
core.material.images.images[name] = image;
|
|
this.materials.addImage(image, {
|
|
index: this.materialsCounter++,
|
|
alias: name
|
|
});
|
|
return Promise.resolve();
|
|
}
|
|
|
|
/**
|
|
* 音效加载完成后的操作
|
|
* @param name 音效名称
|
|
* @param buffer 音效解析完毕的 `AudioBuffer`
|
|
*/
|
|
private soundLoaded(name: SoundIds, buffer: AudioBuffer | null) {
|
|
if (buffer) {
|
|
this.sounds.add(name, buffer);
|
|
}
|
|
return Promise.resolve();
|
|
}
|
|
|
|
/**
|
|
* 当 tileset 加载完成后的操作
|
|
* @param name tileset 名称
|
|
* @param image 图片 `ImageBitmap`
|
|
*/
|
|
private tilesetLoaded(name: string, image: ImageBitmap) {
|
|
core.material.images.tilesets[name] = image;
|
|
// this.materials.addTileset(image, {
|
|
// index: this.materialsCounter++,
|
|
// alias: name
|
|
// });
|
|
return Promise.resolve();
|
|
}
|
|
|
|
/**
|
|
* 当自动元件加载完成后的操作
|
|
* @param autotiles 自动元件存储对象
|
|
* @param name 自动元件名称
|
|
* @param image 自动元件的 `ImageBitmap`
|
|
*/
|
|
private autotileLoaded(
|
|
autotiles: Partial<Record<AllIdsOf<'autotile'>, ImageBitmap>>,
|
|
name: AllIdsOf<'autotile'>,
|
|
image: ImageBitmap
|
|
) {
|
|
autotiles[name] = image;
|
|
loading.addAutotileLoaded();
|
|
loading.onAutotileLoaded(autotiles);
|
|
core.material.images.autotile[name] = image;
|
|
// const num = icon.autotile[name];
|
|
// this.materials.addAutotile(image, {
|
|
// id: name,
|
|
// num,
|
|
// cls: 'autotile'
|
|
// });
|
|
return Promise.resolve();
|
|
}
|
|
|
|
/**
|
|
* 当素材加载完成后的操作
|
|
* @param name 素材名称
|
|
* @param image 素材 `ImageBitmap`
|
|
*/
|
|
private materialLoaded(name: string, image: ImageBitmap) {
|
|
core.material.images[
|
|
name.slice(0, -4) as SelectKey<MaterialImages, ImageBitmap>
|
|
] = image;
|
|
if (name === 'icons.png') {
|
|
this.splitMaterialIcons(image);
|
|
}
|
|
return Promise.resolve();
|
|
}
|
|
|
|
/**
|
|
* 当动画加载完成后的操作
|
|
* @param animation 动画内容
|
|
*/
|
|
private animationLoaded(animation: string) {
|
|
const data = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d;
|
|
const rows = animation.split('@@@~~~###~~~@@@');
|
|
rows.forEach((value, i) => {
|
|
const id = data.main.animates[i];
|
|
if (value.length === 0) {
|
|
throw new Error(`Cannot find animate: '${id}'`);
|
|
}
|
|
core.material.animates[id] = core.loader._loadAnimate(value);
|
|
});
|
|
return Promise.resolve();
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region 加载流程
|
|
|
|
/**
|
|
* 开发时的加载流程
|
|
*/
|
|
private developingLoad() {
|
|
const data = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d;
|
|
const icon = icons_4665ee12_3a1f_44a4_bea3_0fccba634dc1;
|
|
// font
|
|
data.main.fonts.forEach(font => {
|
|
const url = `project/fonts/${font}`;
|
|
const task = new LoadTask<LoadDataType.ArrayBuffer, FontFace>({
|
|
url,
|
|
identifier: `@system-font/${font}`,
|
|
dataType: LoadDataType.ArrayBuffer,
|
|
processor: this.fontProcessor,
|
|
progress: this.progress
|
|
});
|
|
this.addCustomLoadTask(task, data => this.fontLoaded(font, data));
|
|
});
|
|
|
|
// image
|
|
data.main.images.forEach(image => {
|
|
const url = `project/images/${image}`;
|
|
const task = new LoadTask<LoadDataType.Blob, ImageBitmap>({
|
|
url,
|
|
identifier: `@system-image/${image}`,
|
|
dataType: LoadDataType.Blob,
|
|
processor: this.imageProcessor,
|
|
progress: this.progress
|
|
});
|
|
this.addCustomLoadTask(task, data =>
|
|
this.customImagesLoaded(image, data)
|
|
);
|
|
});
|
|
|
|
// sound
|
|
data.main.sounds.forEach(sound => {
|
|
const url = `project/sounds/${sound}`;
|
|
const task = new LoadTask<
|
|
LoadDataType.Uint8Array,
|
|
AudioBuffer | null
|
|
>({
|
|
url,
|
|
identifier: `@system-sound/${sound}`,
|
|
dataType: LoadDataType.Uint8Array,
|
|
processor: this.audioProcessor,
|
|
progress: this.progress
|
|
});
|
|
this.addCustomLoadTask(task, data => this.soundLoaded(sound, data));
|
|
});
|
|
|
|
// tileset
|
|
data.main.tilesets.forEach(tileset => {
|
|
const url = `project/tilesets/${tileset}`;
|
|
const task = new LoadTask<LoadDataType.Blob, ImageBitmap>({
|
|
url,
|
|
identifier: `@system-tileset/${tileset}`,
|
|
dataType: LoadDataType.Blob,
|
|
processor: this.imageProcessor,
|
|
progress: this.progress
|
|
});
|
|
this.addCustomLoadTask(task, data =>
|
|
this.tilesetLoaded(tileset, data)
|
|
);
|
|
});
|
|
|
|
// autotile
|
|
const autotiles: Partial<Record<AllIdsOf<'autotile'>, ImageBitmap>> =
|
|
{};
|
|
Object.keys(icon.autotile).forEach(key => {
|
|
const url = `project/autotiles/${key}.png`;
|
|
const task = new LoadTask<LoadDataType.Blob, ImageBitmap>({
|
|
url,
|
|
identifier: `@system-autotile/${key}`,
|
|
dataType: LoadDataType.Blob,
|
|
processor: this.imageProcessor,
|
|
progress: this.progress
|
|
});
|
|
this.addCustomLoadTask(task, data =>
|
|
this.autotileLoaded(
|
|
autotiles,
|
|
key as AllIdsOf<'autotile'>,
|
|
data
|
|
)
|
|
);
|
|
});
|
|
|
|
// material
|
|
const materialImages = core.materials.slice() as SelectKey<
|
|
MaterialImages,
|
|
ImageBitmap
|
|
>[];
|
|
materialImages.push('keyboard');
|
|
materialImages
|
|
.map(v => `${v}.png`)
|
|
.forEach(materialName => {
|
|
const url = `project/materials/${materialName}`;
|
|
const task = new LoadTask<LoadDataType.Blob, ImageBitmap>({
|
|
url,
|
|
identifier: `@system-material/${materialName}`,
|
|
dataType: LoadDataType.Blob,
|
|
processor: this.imageProcessor,
|
|
progress: this.progress
|
|
});
|
|
this.addCustomLoadTask(task, data =>
|
|
this.materialLoaded(materialName, data)
|
|
);
|
|
});
|
|
|
|
// animate
|
|
const animatesUrl = `all/__all_animates__?v=${main.version}&id=${data.main.animates.join(',')}`;
|
|
const animateTask = new LoadTask<LoadDataType.Text, string>({
|
|
url: animatesUrl,
|
|
identifier: '@system-animates',
|
|
dataType: LoadDataType.Text,
|
|
processor: this.textProcessor,
|
|
progress: this.progress
|
|
});
|
|
this.addCustomLoadTask(animateTask, data => this.animationLoaded(data));
|
|
}
|
|
|
|
/**
|
|
* 获取 `JSZip` 读取方式
|
|
* @param type 加载类型
|
|
*/
|
|
private getZipOutputType(type: LoadDataType): JSZip.OutputType {
|
|
switch (type) {
|
|
case LoadDataType.Text:
|
|
case LoadDataType.JSON:
|
|
return 'string';
|
|
case LoadDataType.ArrayBuffer:
|
|
return 'arraybuffer';
|
|
case LoadDataType.Blob:
|
|
return 'blob';
|
|
case LoadDataType.Uint8Array:
|
|
return 'uint8array';
|
|
default:
|
|
return 'uint8array';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 根据应用方式获取其所在文件夹
|
|
* @param usage 压缩内容的应用方式
|
|
*/
|
|
private getZipFolderByUsage(usage: CompressedUsage): string {
|
|
switch (usage) {
|
|
case CompressedUsage.Image:
|
|
return 'image';
|
|
case CompressedUsage.Tileset:
|
|
return 'tileset';
|
|
case CompressedUsage.Autotile:
|
|
return 'autotile';
|
|
case CompressedUsage.Material:
|
|
return 'material';
|
|
case CompressedUsage.Font:
|
|
return 'font';
|
|
case CompressedUsage.Sound:
|
|
return 'sound';
|
|
case CompressedUsage.Animate:
|
|
return 'animate';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 处理压缩文件
|
|
* @param name 文件名称
|
|
* @param value 文件内容
|
|
* @param usage 文件的应用方式
|
|
*/
|
|
private async processZipFile(
|
|
name: string,
|
|
value: unknown,
|
|
usage: CompressedUsage
|
|
) {
|
|
switch (usage) {
|
|
case CompressedUsage.Image: {
|
|
const image = await createImageBitmap(value as Blob);
|
|
await this.customImagesLoaded(name as ImageIds, image);
|
|
break;
|
|
}
|
|
case CompressedUsage.Tileset: {
|
|
const image = await createImageBitmap(value as Blob);
|
|
await this.tilesetLoaded(name, image);
|
|
break;
|
|
}
|
|
case CompressedUsage.Material: {
|
|
const image = await createImageBitmap(value as Blob);
|
|
await this.materialLoaded(name, image);
|
|
break;
|
|
}
|
|
case CompressedUsage.Font: {
|
|
const fontFace = new FontFace(
|
|
name.slice(0, -4),
|
|
value as ArrayBuffer
|
|
);
|
|
await fontFace.load();
|
|
await this.fontLoaded(name, fontFace);
|
|
break;
|
|
}
|
|
case CompressedUsage.Sound: {
|
|
const buffer = await this.ac.decodeToAudioBuffer(
|
|
value as Uint8Array<ArrayBuffer>
|
|
);
|
|
await this.soundLoaded(name as SoundIds, buffer);
|
|
break;
|
|
}
|
|
case CompressedUsage.Animate: {
|
|
await this.animationLoaded(value as string);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 处理单个压缩包
|
|
* @param list 当前压缩包中包含的内容
|
|
* @param zip 压缩包
|
|
*/
|
|
private async handleZip(list: ICompressedMotaAssetsData[], zip: JSZip) {
|
|
const autotiles: Partial<Record<AllIdsOf<'autotile'>, ImageBitmap>> =
|
|
{};
|
|
const materialImages = core.materials.slice() as SelectKey<
|
|
MaterialImages,
|
|
ImageBitmap
|
|
>[];
|
|
materialImages.push('keyboard');
|
|
|
|
const promises = list.map(async item => {
|
|
const { readAs, name, usage } = item;
|
|
const folder = this.getZipFolderByUsage(usage);
|
|
const file = zip.file(`${folder}/${name}`);
|
|
if (!file) return;
|
|
const value = await file.async(this.getZipOutputType(readAs));
|
|
|
|
if (usage === CompressedUsage.Autotile) {
|
|
const image = await createImageBitmap(value as Blob);
|
|
await this.autotileLoaded(
|
|
autotiles,
|
|
name.slice(0, -4) as AllIdsOf<'autotile'>,
|
|
image
|
|
);
|
|
}
|
|
|
|
await this.processZipFile(name, value, usage);
|
|
});
|
|
|
|
await Promise.all(promises);
|
|
}
|
|
|
|
/**
|
|
* 游戏中加载(压缩后)
|
|
*/
|
|
private async playingLoad() {
|
|
const loadListTask = new LoadTask<
|
|
LoadDataType.JSON,
|
|
ICompressedMotaAssetsLoadList
|
|
>({
|
|
url: `loadList.json`,
|
|
dataType: LoadDataType.JSON,
|
|
identifier: '@system-loadList',
|
|
processor: this.dataLoader.jsonProcessor,
|
|
progress: { onProgress() {} }
|
|
});
|
|
|
|
loadListTask.start();
|
|
const loadList = await loadListTask.loaded();
|
|
|
|
const zipTask = new LoadTask<LoadDataType.ArrayBuffer, JSZip>({
|
|
url: loadList.file,
|
|
identifier: `@system-zip/${loadList.file}`,
|
|
dataType: LoadDataType.ArrayBuffer,
|
|
processor: this.zipProcessor,
|
|
progress: this.progress
|
|
});
|
|
|
|
this.addCustomLoadTask(zipTask, zip => {
|
|
return this.handleZip(loadList.content, zip);
|
|
});
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region 对外接口
|
|
|
|
initSystemLoadTask(): void {
|
|
if (import.meta.env.DEV) {
|
|
this.developingLoad();
|
|
} else {
|
|
this.playingLoad();
|
|
}
|
|
}
|
|
|
|
addCustomLoadTask<R>(
|
|
task: ILoadTask<LoadDataType, R>,
|
|
onLoaded: CustomLoadFunc<R>
|
|
): Promise<R> {
|
|
return this.dataLoader.addCustomLoadTask(task, onLoaded);
|
|
}
|
|
|
|
async load(): Promise<void> {
|
|
this.loading = true;
|
|
this.loaded = false;
|
|
await this.dataLoader.load();
|
|
this.loading = false;
|
|
this.loaded = true;
|
|
}
|
|
|
|
//#endregion
|
|
}
|