mirror of
https://github.com/motajs/template.git
synced 2026-04-12 15:11:10 +08:00
refactor: 客户端加载
This commit is contained in:
parent
fdeee99d40
commit
4fc6db3e79
@ -1,9 +1,16 @@
|
||||
import { createMaterial } from './material';
|
||||
import { loading } from '@user/data-base';
|
||||
import { createMaterial, fallbackLoad } from './material';
|
||||
import { materials } from './ins';
|
||||
|
||||
export function create() {
|
||||
createMaterial();
|
||||
loading.once('loaded', () => {
|
||||
fallbackLoad(materials);
|
||||
loading.emit('assetBuilt');
|
||||
});
|
||||
}
|
||||
|
||||
export * from './load';
|
||||
export * from './material';
|
||||
|
||||
export * from './ins';
|
||||
|
||||
@ -1,4 +1,16 @@
|
||||
import { BGMPlayer, MotaAudioContext, SoundPlayer } from '@motajs/audio';
|
||||
import {
|
||||
AudioType,
|
||||
BGMPlayer,
|
||||
MotaAudioContext,
|
||||
OpusDecoder,
|
||||
SoundPlayer,
|
||||
VorbisDecoder
|
||||
} from '@motajs/audio';
|
||||
import { MotaAssetsLoader } from './load/loader';
|
||||
import { AutotileProcessor, MaterialManager } from './material';
|
||||
import { loadProgress } from '@user/data-base';
|
||||
|
||||
//#region 音频实例
|
||||
|
||||
/** 游戏全局音频上下文 */
|
||||
export const audioContext = new MotaAudioContext();
|
||||
@ -6,3 +18,29 @@ export const audioContext = new MotaAudioContext();
|
||||
export const soundPlayer = new SoundPlayer(audioContext);
|
||||
/** 音乐播放器 */
|
||||
export const bgmPlayer = new BGMPlayer(audioContext);
|
||||
|
||||
audioContext.registerDecoder(AudioType.Opus, () => new OpusDecoder());
|
||||
audioContext.registerDecoder(AudioType.Ogg, () => new VorbisDecoder());
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 素材实例
|
||||
|
||||
/** 素材管理器 */
|
||||
export const materials = new MaterialManager();
|
||||
/** 自动元件处理器 */
|
||||
export const autotile = new AutotileProcessor(materials);
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 加载实例
|
||||
|
||||
/** 全局加载实例 */
|
||||
export const loader = new MotaAssetsLoader(
|
||||
loadProgress,
|
||||
audioContext,
|
||||
soundPlayer,
|
||||
materials
|
||||
);
|
||||
|
||||
//#endregion
|
||||
|
||||
37
packages-user/client-base/src/load/data.ts
Normal file
37
packages-user/client-base/src/load/data.ts
Normal file
@ -0,0 +1,37 @@
|
||||
export const iconNames: string[] = [
|
||||
'floor',
|
||||
'lv',
|
||||
'hpmax',
|
||||
'hp',
|
||||
'atk',
|
||||
'def',
|
||||
'mdef',
|
||||
'money',
|
||||
'exp',
|
||||
'up',
|
||||
'book',
|
||||
'fly',
|
||||
'toolbox',
|
||||
'keyboard',
|
||||
'shop',
|
||||
'save',
|
||||
'load',
|
||||
'settings',
|
||||
'play',
|
||||
'pause',
|
||||
'stop',
|
||||
'speedDown',
|
||||
'speedUp',
|
||||
'rewind',
|
||||
'equipbox',
|
||||
'mana',
|
||||
'skill',
|
||||
'btn1',
|
||||
'btn2',
|
||||
'btn3',
|
||||
'btn4',
|
||||
'btn5',
|
||||
'btn6',
|
||||
'btn7',
|
||||
'btn8'
|
||||
];
|
||||
1
packages-user/client-base/src/load/index.ts
Normal file
1
packages-user/client-base/src/load/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './processor';
|
||||
532
packages-user/client-base/src/load/loader.ts
Normal file
532
packages-user/client-base/src/load/loader.ts
Normal file
@ -0,0 +1,532 @@
|
||||
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,
|
||||
LoadJSONProcessor,
|
||||
LoadTextProcessor,
|
||||
LoadZipProcessor
|
||||
} from './processor';
|
||||
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';
|
||||
|
||||
interface LoadTaskStore<T extends LoadDataType = LoadDataType, R = any> {
|
||||
/** 加载任务对象 */
|
||||
readonly task: ILoadTask<T, R>;
|
||||
/** 当 `onLoaded` 兑现后兑现的 `Promise` */
|
||||
readonly loadPromise: Promise<R>;
|
||||
/** 兑现 `loadPromise` */
|
||||
readonly loadResolve: (data: R) => void;
|
||||
/** 当加载任务完成时执行的函数 */
|
||||
readonly onLoaded: CustomLoadFunc<R>;
|
||||
}
|
||||
|
||||
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 jsonProcessor: ILoadTaskProcessor<LoadDataType.JSON, any>;
|
||||
readonly zipProcessor: ILoadTaskProcessor<LoadDataType.ArrayBuffer, JSZip>;
|
||||
|
||||
/** 当前已添加的加载任务 */
|
||||
private readonly tasks: Set<LoadTaskStore> = new Set();
|
||||
|
||||
/** 素材索引 */
|
||||
private materialsCounter: number = 0;
|
||||
/** 贴图行分割器,用于处理遗留 `icons.png` */
|
||||
private readonly rowSplitter: ITextureSplitter<number>;
|
||||
|
||||
constructor(
|
||||
readonly progress: ILoadProgressTotal,
|
||||
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.jsonProcessor = new LoadJSONProcessor();
|
||||
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.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> {
|
||||
this.progress.addTask(task);
|
||||
const { promise, resolve } = Promise.withResolvers<R>();
|
||||
const store: LoadTaskStore<LoadDataType, R> = {
|
||||
task,
|
||||
onLoaded,
|
||||
loadPromise: promise,
|
||||
loadResolve: resolve
|
||||
};
|
||||
this.tasks.add(store);
|
||||
return promise;
|
||||
}
|
||||
|
||||
load(): Promise<any[]> {
|
||||
const tasks = [...this.tasks].map(async task => {
|
||||
task.task.start();
|
||||
const data = await task.task.loaded();
|
||||
await task.onLoaded(data);
|
||||
task.loadResolve(data);
|
||||
return data;
|
||||
});
|
||||
return Promise.all(tasks);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
66
packages-user/client-base/src/load/processor.ts
Normal file
66
packages-user/client-base/src/load/processor.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { IMotaAudioContext } from '@motajs/audio';
|
||||
import { ILoadTask, ILoadTaskProcessor, LoadDataType } from '@motajs/loader';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
export class LoadImageProcessor implements ILoadTaskProcessor<
|
||||
LoadDataType.Blob,
|
||||
ImageBitmap
|
||||
> {
|
||||
process(response: Blob): Promise<ImageBitmap> {
|
||||
return createImageBitmap(response);
|
||||
}
|
||||
}
|
||||
|
||||
export class LoadAudioProcessor implements ILoadTaskProcessor<
|
||||
LoadDataType.Uint8Array,
|
||||
AudioBuffer | null
|
||||
> {
|
||||
constructor(private readonly ac: IMotaAudioContext) {}
|
||||
|
||||
process(response: Uint8Array<ArrayBuffer>): Promise<AudioBuffer | null> {
|
||||
return this.ac.decodeToAudioBuffer(response);
|
||||
}
|
||||
}
|
||||
|
||||
export class LoadFontProcessor implements ILoadTaskProcessor<
|
||||
LoadDataType.ArrayBuffer,
|
||||
FontFace
|
||||
> {
|
||||
process(
|
||||
response: ArrayBuffer,
|
||||
task: ILoadTask<LoadDataType.ArrayBuffer, FontFace>
|
||||
): Promise<FontFace> {
|
||||
const font = new FontFace(task.identifier, response);
|
||||
if (font.status === 'loaded') return Promise.resolve(font);
|
||||
else return font.load();
|
||||
}
|
||||
}
|
||||
|
||||
export class LoadZipProcessor implements ILoadTaskProcessor<
|
||||
LoadDataType.ArrayBuffer,
|
||||
JSZip
|
||||
> {
|
||||
async process(response: ArrayBuffer): Promise<JSZip> {
|
||||
const zip = new JSZip();
|
||||
await zip.loadAsync(response);
|
||||
return zip;
|
||||
}
|
||||
}
|
||||
|
||||
export class LoadTextProcessor implements ILoadTaskProcessor<
|
||||
LoadDataType.Text,
|
||||
string
|
||||
> {
|
||||
process(response: string): Promise<string> {
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
}
|
||||
|
||||
export class LoadJSONProcessor<T> implements ILoadTaskProcessor<
|
||||
LoadDataType.JSON,
|
||||
T
|
||||
> {
|
||||
process(response: any): Promise<T> {
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
}
|
||||
86
packages-user/client-base/src/load/types.ts
Normal file
86
packages-user/client-base/src/load/types.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import {
|
||||
ILoadProgressTotal,
|
||||
ILoadTask,
|
||||
ILoadTaskProcessor,
|
||||
LoadDataType
|
||||
} from '@motajs/loader';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
export type CustomLoadFunc<R> = (data: R) => Promise<void>;
|
||||
|
||||
export const enum CompressedUsage {
|
||||
// ---- 系统加载内容,不可更改
|
||||
Font,
|
||||
Image,
|
||||
Sound,
|
||||
Tileset,
|
||||
Autotile,
|
||||
Material,
|
||||
Animate
|
||||
}
|
||||
|
||||
export interface ICompressedMotaAssetsData {
|
||||
/** 此内容的名称 */
|
||||
readonly name: string;
|
||||
/** 此内容应该由什么方式读取 */
|
||||
readonly readAs: LoadDataType;
|
||||
/** 此内容的应用方式 */
|
||||
readonly usage: CompressedUsage;
|
||||
}
|
||||
|
||||
export interface ICompressedMotaAssetsLoadList {
|
||||
/** 压缩文件名称 */
|
||||
readonly file: string;
|
||||
/** 压缩包所包含的内容 */
|
||||
readonly content: ICompressedMotaAssetsData[];
|
||||
}
|
||||
|
||||
export interface IMotaAssetsLoader {
|
||||
/** 加载进度对象 */
|
||||
readonly progress: ILoadProgressTotal;
|
||||
/** 当前是否正在加载 */
|
||||
readonly loading: boolean;
|
||||
/** 当前是否已经加载完毕 */
|
||||
readonly loaded: boolean;
|
||||
|
||||
/** 图片处理器 */
|
||||
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>;
|
||||
/** JSON 处理器 */
|
||||
readonly jsonProcessor: ILoadTaskProcessor<LoadDataType.JSON, any>;
|
||||
/** `zip` 压缩包处理器 */
|
||||
readonly zipProcessor: ILoadTaskProcessor<LoadDataType.ArrayBuffer, JSZip>;
|
||||
|
||||
/**
|
||||
* 初始化系统加载任务
|
||||
*/
|
||||
initSystemLoadTask(): void;
|
||||
|
||||
/**
|
||||
* 添加自定义加载任务
|
||||
* @param task 自定义加载任务
|
||||
* @param onLoad 当任务加载完成时执行
|
||||
* @returns 一个 `Promise`,当添加的任务加载完毕,且 `onLoad` 返回的 `Promise` 兑现后兑现
|
||||
*/
|
||||
addCustomLoadTask<R>(
|
||||
task: ILoadTask<LoadDataType, R>,
|
||||
onLoad: CustomLoadFunc<R>
|
||||
): Promise<R>;
|
||||
|
||||
/**
|
||||
* 开始所有加载任务的加载工作
|
||||
* @returns 一个 `Promise`,当所有加载任务加载完成后兑现
|
||||
*/
|
||||
load(): Promise<any[]>;
|
||||
}
|
||||
@ -1,6 +1,9 @@
|
||||
import { ITexture } from '@motajs/render';
|
||||
import { materials } from './ins';
|
||||
import { IBlockIdentifier, IIndexedIdentifier } from './types';
|
||||
import {
|
||||
IBlockIdentifier,
|
||||
IIndexedIdentifier,
|
||||
IMaterialManager
|
||||
} from './types';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
function extractClsBlocks<C extends Exclude<Cls, 'tileset'>>(
|
||||
@ -47,7 +50,7 @@ function addAutotile(set: Set<number>, map?: readonly (readonly number[])[]) {
|
||||
/**
|
||||
* 兼容旧版加载
|
||||
*/
|
||||
export function fallbackLoad() {
|
||||
export function fallbackLoad(materials: IMaterialManager) {
|
||||
// 基本素材
|
||||
const icons = core.icons.icons;
|
||||
const images = core.material.images;
|
||||
@ -102,12 +105,6 @@ export function fallbackLoad() {
|
||||
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>();
|
||||
|
||||
@ -1,19 +1,12 @@
|
||||
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';
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
import { AutotileProcessor } from './autotile';
|
||||
import { MaterialManager } from './manager';
|
||||
|
||||
export const materials = new MaterialManager();
|
||||
export const autotile = new AutotileProcessor(materials);
|
||||
@ -102,8 +102,7 @@ export interface IMaterialFramedData {
|
||||
}
|
||||
|
||||
export interface IMaterialAsset
|
||||
extends IDirtyTracker<boolean>,
|
||||
IDirtyMarker<void> {
|
||||
extends IDirtyTracker<boolean>, IDirtyMarker<void> {
|
||||
/** 图集的贴图数据 */
|
||||
readonly data: ITextureComposedData;
|
||||
}
|
||||
@ -290,8 +289,7 @@ export interface IMaterialAliasGetter {
|
||||
}
|
||||
|
||||
export interface IMaterialManager
|
||||
extends IMaterialGetter,
|
||||
IMaterialAliasGetter {
|
||||
extends IMaterialGetter, IMaterialAliasGetter {
|
||||
/** 贴图存储,把 terrains 等内容单独分开存储 */
|
||||
readonly tileStore: ITextureStore;
|
||||
/** tilesets 贴图存储,每个 tileset 是一个贴图对象 */
|
||||
@ -331,7 +329,6 @@ export interface IMaterialManager
|
||||
addRowAnimate(
|
||||
source: SizedCanvasImageSource,
|
||||
map: ArrayLike<IBlockIdentifier>,
|
||||
frames: number,
|
||||
height: number
|
||||
): Iterable<IMaterialData>;
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { MotaOffscreenCanvas2D, SizedCanvasImageSource } from '@motajs/render';
|
||||
// 得出结论,ImageBitmap和Canvas的绘制性能不如Image,于是直接画Image就行,所以缓存基本上就是存Image
|
||||
|
||||
type ImageMapKeys = Exclude<Cls, 'tileset' | 'autotile'>;
|
||||
type ImageMap = Record<ImageMapKeys, HTMLImageElement>;
|
||||
type ImageMap = Record<ImageMapKeys, ImageBitmap>;
|
||||
|
||||
const i = (img: ImageMapKeys) => {
|
||||
return core.material.images[img];
|
||||
@ -21,10 +21,10 @@ interface AutotileCache {
|
||||
type AutotileCaches = Record<AllNumbersOf<'autotile'>, AutotileCache>;
|
||||
|
||||
interface TextureRequire {
|
||||
tileset: Record<string, HTMLImageElement>;
|
||||
material: Record<ImageMapKeys, HTMLImageElement>;
|
||||
tileset: Record<string, ImageBitmap>;
|
||||
material: Record<ImageMapKeys, ImageBitmap>;
|
||||
autotile: AutotileCaches;
|
||||
images: Record<ImageIds, HTMLImageElement>;
|
||||
images: Record<ImageIds, ImageBitmap>;
|
||||
}
|
||||
|
||||
interface RenderableDataBase {
|
||||
@ -49,10 +49,10 @@ export interface AutotileRenderable extends RenderableDataBase {
|
||||
}
|
||||
|
||||
class TextureCache {
|
||||
tileset!: Record<string, HTMLImageElement>;
|
||||
material: Record<ImageMapKeys, HTMLImageElement>;
|
||||
tileset!: Record<string, ImageBitmap>;
|
||||
material: Record<ImageMapKeys, ImageBitmap>;
|
||||
autotile!: AutotileCaches;
|
||||
images!: Record<ImageIds, HTMLImageElement>;
|
||||
images!: Record<ImageIds, ImageBitmap>;
|
||||
|
||||
idNumberMap!: IdToNumber;
|
||||
|
||||
@ -76,7 +76,7 @@ class TextureCache {
|
||||
characterTurn2: Dir2[] = ['leftup', 'rightup', 'rightdown', 'leftdown'];
|
||||
|
||||
constructor() {
|
||||
this.material = imageMap as Record<ImageMapKeys, HTMLImageElement>;
|
||||
this.material = imageMap as Record<ImageMapKeys, ImageBitmap>;
|
||||
}
|
||||
|
||||
init() {
|
||||
|
||||
@ -11,6 +11,7 @@ import { GameTitleUI } from './ui/title';
|
||||
import { createWeather } from './weather';
|
||||
import { createMainExtension } from './commonIns';
|
||||
import { createApp } from './renderer';
|
||||
import { LoadSceneUI } from './ui/load';
|
||||
|
||||
export function createGameRenderer() {
|
||||
const App = defineComponent(_props => {
|
||||
@ -23,6 +24,9 @@ export function createGameRenderer() {
|
||||
|
||||
mainRenderer.hide();
|
||||
createApp(App).mount(mainRenderer);
|
||||
|
||||
sceneController.open(LoadSceneUI, {});
|
||||
mainRenderer.show();
|
||||
}
|
||||
|
||||
export function createRender() {
|
||||
@ -31,11 +35,6 @@ export function createRender() {
|
||||
createAction();
|
||||
createWeather();
|
||||
|
||||
loading.once('loaded', () => {
|
||||
sceneController.open(GameTitleUI, {});
|
||||
mainRenderer.show();
|
||||
});
|
||||
|
||||
loading.once('assetBuilt', () => {
|
||||
createMainExtension();
|
||||
});
|
||||
|
||||
@ -54,6 +54,8 @@ export const MOVING_TOLERANCE = 60;
|
||||
/** 开关门动画的动画时长 */
|
||||
export const DOOR_ANIMATE_INTERVAL = 50;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 状态栏
|
||||
|
||||
/** 状态栏像素宽度 */
|
||||
@ -69,6 +71,8 @@ export const STATUS_BAR_COUNT = ENABLE_RIGHT_STATUS_BAR ? 2 : 1;
|
||||
/** 状态栏宽度的一半 */
|
||||
export const HALF_STATUS_WIDTH = STATUS_BAR_WIDTH / 2;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 游戏画面
|
||||
|
||||
/** 游戏画面像素宽度,宽=地图宽度+状态栏宽度*状态栏数量 */
|
||||
@ -91,6 +95,8 @@ export const CENTER_LOC: ElementLocator = [
|
||||
0.5
|
||||
];
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 通用配置
|
||||
|
||||
/** 弹框的宽度,使用在内置 UI 与组件中,包括确认框、选择框、等待框等 */
|
||||
@ -98,6 +104,31 @@ export const POP_BOX_WIDTH = MAP_WIDTH / 2;
|
||||
/** 默认字体 */
|
||||
export const DEFAULT_FONT = new Font('Verdana', 16);
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 加载界面
|
||||
|
||||
/** 加载界面的任务进度条半径 */
|
||||
export const LOAD_TASK_RADIUS = Math.min(MAIN_WIDTH, MAIN_HEIGHT) / 6;
|
||||
/** 加载界面的字节进度条纵轴位置 */
|
||||
export const LOAD_BYTE_HEIGHT = MAIN_HEIGHT / 2 + MAIN_HEIGHT / 4;
|
||||
/** 加载界面任务进度条的纵轴位置 */
|
||||
export const LOAD_TASK_CENTER_HEIGHT = MAIN_HEIGHT / 2 - MAIN_HEIGHT / 8;
|
||||
/** 加载界面字节进度条的长度 */
|
||||
export const LOAD_BYTE_LENGTH = MAIN_WIDTH - MAIN_WIDTH / 12;
|
||||
/** 加载界面任务进度条的粗细 */
|
||||
export const LOAD_TASK_LINE_WIDTH = 6;
|
||||
/** 加载界面字节进度条的粗细 */
|
||||
export const LOAD_BYTE_LINE_WIDTH = 6;
|
||||
/** 已加载部分进度条的颜色 */
|
||||
export const LOAD_LOADED_COLOR = '#57ff78';
|
||||
/** 未加载部分进度条的颜色 */
|
||||
export const LOAD_UNLOADED_COLOR = '#ccc';
|
||||
/** 加载界面的文字颜色 */
|
||||
export const LOAD_FONT_COLOR = '#fff';
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 存档界面
|
||||
|
||||
/** 存档缩略图尺寸 */
|
||||
@ -115,8 +146,13 @@ export const SAVE_DOWN_PAD = 30;
|
||||
/** 存档页码数,调高并不会影响性能,但是如果玩家存档太多的话会导致存档体积很大 */
|
||||
export const SAVE_PAGES = 1000;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 标题界面
|
||||
|
||||
/** 标题图 */
|
||||
export const TITLE_BACKGROUND_IMAGE = 'bg.jpg';
|
||||
|
||||
/** 标题文字中心横坐标 */
|
||||
export const TITLE_X = HALF_WIDTH;
|
||||
/** 标题文字中心纵坐标 */
|
||||
@ -136,3 +172,5 @@ export const BUTTONS_HEIGHT = 200;
|
||||
export const BUTTONS_X = HALF_WIDTH;
|
||||
/** 标题界面按钮左上角纵坐标 */
|
||||
export const BUTTONS_Y = MAIN_HEIGHT - BUTTONS_HEIGHT;
|
||||
|
||||
//#endregion
|
||||
|
||||
165
packages-user/client-modules/src/render/ui/load.tsx
Normal file
165
packages-user/client-modules/src/render/ui/load.tsx
Normal file
@ -0,0 +1,165 @@
|
||||
import { DefaultProps } from '@motajs/render-vue';
|
||||
import {
|
||||
GameUI,
|
||||
SetupComponentOptions,
|
||||
UIComponentProps
|
||||
} from '@motajs/system';
|
||||
import { defineComponent } from 'vue';
|
||||
import {
|
||||
FULL_LOC,
|
||||
LOAD_BYTE_HEIGHT,
|
||||
LOAD_BYTE_LENGTH,
|
||||
LOAD_BYTE_LINE_WIDTH,
|
||||
LOAD_FONT_COLOR,
|
||||
LOAD_LOADED_COLOR,
|
||||
LOAD_TASK_CENTER_HEIGHT,
|
||||
LOAD_TASK_LINE_WIDTH,
|
||||
LOAD_TASK_RADIUS,
|
||||
LOAD_UNLOADED_COLOR,
|
||||
MAIN_WIDTH
|
||||
} from '../shared';
|
||||
import { ElementLocator, Font, MotaOffscreenCanvas2D } from '@motajs/render';
|
||||
import { transitioned } from '../use';
|
||||
import { cosh, CurveMode, linear } from '@motajs/animate';
|
||||
import { loader } from '@user/client-base';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { sleep } from '@motajs/common';
|
||||
import { loading } from '@user/data-base';
|
||||
import { GameTitleUI } from './title';
|
||||
|
||||
export interface ILoadProps extends UIComponentProps, DefaultProps {}
|
||||
|
||||
const loadSceneProps = {
|
||||
props: ['controller', 'instance']
|
||||
} satisfies SetupComponentOptions<ILoadProps>;
|
||||
|
||||
export const LoadScene = defineComponent<ILoadProps>(props => {
|
||||
const taskFont = new Font('Verdana', 24);
|
||||
const byteFont = new Font('Verdana', 12);
|
||||
|
||||
/** 当前加载进度 */
|
||||
const taskProgress = transitioned(0, 500, cosh(2, CurveMode.EaseOut))!;
|
||||
const byteProgress = transitioned(0, 500, cosh(2, CurveMode.EaseOut))!;
|
||||
const alpha = transitioned(1, 400, linear())!;
|
||||
|
||||
// 两个进度条的位置
|
||||
const taskLoc: ElementLocator = [
|
||||
MAIN_WIDTH / 2,
|
||||
LOAD_TASK_CENTER_HEIGHT,
|
||||
LOAD_TASK_RADIUS * 2 + LOAD_TASK_LINE_WIDTH * 2,
|
||||
LOAD_TASK_RADIUS * 2 + LOAD_TASK_LINE_WIDTH * 2,
|
||||
0.5,
|
||||
0.5
|
||||
];
|
||||
const byteLoc: ElementLocator = [
|
||||
MAIN_WIDTH / 2,
|
||||
LOAD_BYTE_HEIGHT,
|
||||
LOAD_BYTE_LENGTH + LOAD_BYTE_LINE_WIDTH,
|
||||
LOAD_BYTE_LINE_WIDTH * 2 + byteFont.size,
|
||||
0.5,
|
||||
0.5
|
||||
];
|
||||
|
||||
const loadEnd = async () => {
|
||||
loading.emit('loaded');
|
||||
alpha.set(0);
|
||||
await sleep(400);
|
||||
props.controller.closeAll();
|
||||
props.controller.open(GameTitleUI, {});
|
||||
};
|
||||
|
||||
const startLoad = async () => {
|
||||
loader.initSystemLoadTask();
|
||||
loader.load().then(() => {
|
||||
loadEnd();
|
||||
});
|
||||
for await (const _ of loader.progress) {
|
||||
taskProgress.set(loader.progress.getLoadedTasks());
|
||||
byteProgress.set(loader.progress.getLoadedByte());
|
||||
}
|
||||
};
|
||||
|
||||
// 开始加载
|
||||
startLoad();
|
||||
|
||||
/** 渲染加载任务进度 */
|
||||
const renderTaskList = (canvas: MotaOffscreenCanvas2D) => {
|
||||
const ctx = canvas.ctx;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineWidth = LOAD_TASK_LINE_WIDTH;
|
||||
ctx.font = taskFont.string();
|
||||
const loaded = loader.progress.getLoadedTasks();
|
||||
const total = loader.progress.getAddedTasks();
|
||||
// 这里使用渐变参数,因为要有动画效果
|
||||
const progress = clamp(taskProgress.value / total, 0, 1);
|
||||
const cx = taskLoc[2]! / 2;
|
||||
const cy = taskLoc[3]! / 2;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, LOAD_TASK_RADIUS, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = LOAD_UNLOADED_COLOR;
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
const end = progress * Math.PI * 2 - Math.PI / 2;
|
||||
ctx.arc(cx, cy, LOAD_TASK_RADIUS, -Math.PI / 2, end);
|
||||
ctx.strokeStyle = LOAD_LOADED_COLOR;
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = LOAD_FONT_COLOR;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(`${loaded} / ${total}`, cx, cy + 3);
|
||||
};
|
||||
|
||||
/** 渲染加载字节进度 */
|
||||
const renderByteList = (canvas: MotaOffscreenCanvas2D) => {
|
||||
const ctx = canvas.ctx;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineWidth = LOAD_BYTE_LINE_WIDTH;
|
||||
ctx.font = byteFont.string();
|
||||
const total = loader.progress.getTotalByte();
|
||||
const loaded = loader.progress.getLoadedByte();
|
||||
// 这里使用渐变参数,因为要有动画效果
|
||||
const progress = clamp(byteProgress.value / total, 0, 1);
|
||||
const sx = LOAD_BYTE_LINE_WIDTH;
|
||||
const sy = byteFont.size + LOAD_BYTE_LINE_WIDTH;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(sx, sy);
|
||||
ctx.lineTo(sx + LOAD_BYTE_LENGTH, sy);
|
||||
ctx.strokeStyle = LOAD_UNLOADED_COLOR;
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(sx, sy);
|
||||
ctx.lineTo(sx + progress * LOAD_BYTE_LENGTH, sy);
|
||||
ctx.strokeStyle = LOAD_LOADED_COLOR;
|
||||
ctx.stroke();
|
||||
ctx.textAlign = 'right';
|
||||
ctx.textBaseline = 'bottom';
|
||||
ctx.fillStyle = LOAD_FONT_COLOR;
|
||||
const loadedMB = (loaded / 2 ** 20).toFixed(2);
|
||||
const totalMB = (total / 2 ** 20).toFixed(2);
|
||||
const percent = loader.progress.getByteRatio() * 100;
|
||||
ctx.fillText(
|
||||
`${loadedMB}MB / ${totalMB}MB | ${percent.toFixed(2)}%`,
|
||||
byteLoc[2]! - LOAD_BYTE_LINE_WIDTH,
|
||||
byteLoc[3]! - LOAD_BYTE_LINE_WIDTH * 2
|
||||
);
|
||||
};
|
||||
|
||||
return () => (
|
||||
<container loc={FULL_LOC} alpha={alpha.ref.value}>
|
||||
<custom
|
||||
loc={taskLoc}
|
||||
render={renderTaskList}
|
||||
bindings={[taskProgress.ref]}
|
||||
nocache
|
||||
/>
|
||||
<custom
|
||||
loc={byteLoc}
|
||||
render={renderByteList}
|
||||
bindings={[byteProgress.ref]}
|
||||
nocache
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
}, loadSceneProps);
|
||||
|
||||
export const LoadSceneUI = new GameUI('load-scene', LoadScene);
|
||||
@ -1,7 +1,7 @@
|
||||
import { GameUI, SetupComponentOptions } from '@motajs/system';
|
||||
import { computed, ComputedRef, defineComponent, shallowReactive } from 'vue';
|
||||
import { TextContent } from '../components';
|
||||
import { ElementLocator, Font, SizedCanvasImageSource } from '@motajs/render';
|
||||
import { ElementLocator, Font, ITexture } from '@motajs/render';
|
||||
import { MixedToolbar, ReplayingStatus } from './toolbar';
|
||||
import { openViewMap } from './viewmap';
|
||||
import { mainUIController } from './controller';
|
||||
@ -12,6 +12,7 @@ import {
|
||||
STATUS_BAR_WIDTH
|
||||
} from '../shared';
|
||||
import { DefaultProps } from '@motajs/render-vue';
|
||||
import { materials } from '@user/client-base';
|
||||
|
||||
export interface ILeftHeroStatus {
|
||||
/** 楼层 id */
|
||||
@ -69,27 +70,27 @@ export interface IRightHeroStatus {
|
||||
|
||||
interface StatusInfo {
|
||||
/** 图标 */
|
||||
icon: SizedCanvasImageSource;
|
||||
readonly icon: ITexture | null;
|
||||
/** 属性值,经过格式化 */
|
||||
value: ComputedRef<string>;
|
||||
readonly value: ComputedRef<string>;
|
||||
/** 字体 */
|
||||
font: Font;
|
||||
readonly font: Font;
|
||||
/** 文字颜色 */
|
||||
color: CanvasStyle;
|
||||
readonly color: CanvasStyle;
|
||||
}
|
||||
|
||||
interface KeyLikeItem {
|
||||
/** 属性值,经过格式化 */
|
||||
value: ComputedRef<string>;
|
||||
readonly value: ComputedRef<string>;
|
||||
/** 字体 */
|
||||
font: Font;
|
||||
readonly font: Font;
|
||||
/** 文字颜色 */
|
||||
color: CanvasStyle;
|
||||
readonly color: CanvasStyle;
|
||||
}
|
||||
|
||||
interface KeyLikeInfo {
|
||||
/** 这一行包含的内容 */
|
||||
items: KeyLikeItem[];
|
||||
readonly items: KeyLikeItem[];
|
||||
}
|
||||
|
||||
interface StatusBarProps<T> extends DefaultProps {
|
||||
@ -115,15 +116,15 @@ export const LeftStatusBar = defineComponent<StatusBarProps<ILeftHeroStatus>>(
|
||||
/** 状态属性的开始纵坐标 */
|
||||
const STATUS_Y = TITLE_HEIGHT + STATUS_PAD;
|
||||
|
||||
// 可以换成 core.material.images.images['xxx.png'] 来使用全塔属性注册的图片
|
||||
const hpIcon = core.statusBar.icons.hp;
|
||||
const atkIcon = core.statusBar.icons.atk;
|
||||
const defIcon = core.statusBar.icons.def;
|
||||
const mdefIcon = core.statusBar.icons.mdef;
|
||||
const moneyIcon = core.statusBar.icons.money;
|
||||
const expIcon = core.statusBar.icons.exp;
|
||||
const manaIcon = core.statusBar.icons.mana;
|
||||
const lvIcon = core.statusBar.icons.lv;
|
||||
// 可以换成 materials.getImageByAlias('xxx.png') 来使用全塔属性注册的图片
|
||||
const hpIcon = materials.getImageByAlias('icon-hp');
|
||||
const atkIcon = materials.getImageByAlias('icon-atk');
|
||||
const defIcon = materials.getImageByAlias('icon-def');
|
||||
const mdefIcon = materials.getImageByAlias('icon-mdef');
|
||||
const moneyIcon = materials.getImageByAlias('icon-money');
|
||||
const expIcon = materials.getImageByAlias('icon-exp');
|
||||
const manaIcon = materials.getImageByAlias('icon-mana');
|
||||
const lvIcon = materials.getImageByAlias('icon-lv');
|
||||
|
||||
const s = p.status;
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
HALF_WIDTH,
|
||||
MAIN_HEIGHT,
|
||||
MAIN_WIDTH,
|
||||
TITLE_BACKGROUND_IMAGE,
|
||||
TITLE_FILL,
|
||||
TITLE_STROKE,
|
||||
TITLE_STROKE_WIDTH,
|
||||
@ -34,6 +35,7 @@ import { MainSceneUI } from './main';
|
||||
import { adjustCover } from '../utils';
|
||||
import { cosh, CurveMode, linear } from '@motajs/animate';
|
||||
import { sleep } from '@motajs/common';
|
||||
import { materials } from '@user/client-base';
|
||||
|
||||
const enum TitleButton {
|
||||
StartGame,
|
||||
@ -62,12 +64,12 @@ const gameTitleProps = {
|
||||
} satisfies SetupComponentOptions<GameTitleProps>;
|
||||
|
||||
export const GameTitle = defineComponent<GameTitleProps>(props => {
|
||||
const bg = core.material.images.images['bg.jpg'];
|
||||
const bg = materials.getImageByAlias(TITLE_BACKGROUND_IMAGE);
|
||||
|
||||
//#region 计算背景图
|
||||
const [width, height] = adjustCover(
|
||||
bg.width,
|
||||
bg.height,
|
||||
bg?.width ?? MAIN_WIDTH,
|
||||
bg?.height ?? MAIN_HEIGHT,
|
||||
MAIN_WIDTH,
|
||||
MAIN_HEIGHT
|
||||
);
|
||||
|
||||
@ -25,6 +25,7 @@ import { MAIN_HEIGHT, FULL_LOC, POP_BOX_WIDTH, CENTER_LOC } from '../shared';
|
||||
import { openReplay, openSettings } from './settings';
|
||||
import { openViewMap } from './viewmap';
|
||||
import { DefaultProps } from '@motajs/render-vue';
|
||||
import { materials } from '@user/client-base';
|
||||
|
||||
interface ToolbarProps extends DefaultProps {
|
||||
loc?: ElementLocator;
|
||||
@ -73,15 +74,15 @@ export const PlayingToolbar = defineComponent<
|
||||
ToolbarEmits,
|
||||
keyof ToolbarEmits
|
||||
>((props, { emit }) => {
|
||||
const bookIcon = core.statusBar.icons.book;
|
||||
const flyIcon = core.statusBar.icons.fly;
|
||||
const toolIcon = core.statusBar.icons.toolbox;
|
||||
const equipIcon = core.statusBar.icons.equipbox;
|
||||
const keyIcon = core.statusBar.icons.keyboard;
|
||||
const shopIcon = core.statusBar.icons.shop;
|
||||
const saveIcon = core.statusBar.icons.save;
|
||||
const loadIcon = core.statusBar.icons.load;
|
||||
const setIcon = core.statusBar.icons.settings;
|
||||
const bookIcon = materials.getImageByAlias('icon-book');
|
||||
const flyIcon = materials.getImageByAlias('icon-fly');
|
||||
const toolIcon = materials.getImageByAlias('icon-toolbox');
|
||||
const equipIcon = materials.getImageByAlias('icon-equipbox');
|
||||
const keyIcon = materials.getImageByAlias('icon-keyboard');
|
||||
const shopIcon = materials.getImageByAlias('icon-shop');
|
||||
const saveIcon = materials.getImageByAlias('icon-save');
|
||||
const loadIcon = materials.getImageByAlias('icon-load');
|
||||
const setIcon = materials.getImageByAlias('icon-settings');
|
||||
|
||||
const iconFont = new Font('Verdana', 12);
|
||||
|
||||
@ -170,8 +171,8 @@ const replayingProps = {
|
||||
export const ReplayingToolbar = defineComponent<ReplayingProps>(props => {
|
||||
const status = props.status;
|
||||
|
||||
const bookIcon = core.statusBar.icons.book;
|
||||
const saveIcon = core.statusBar.icons.save;
|
||||
const bookIcon = materials.getImageByAlias('icon-book');
|
||||
const saveIcon = materials.getImageByAlias('icon-save');
|
||||
const font1 = Font.defaults({ size: 16 });
|
||||
const font2 = new Font('Verdana', 12);
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import { clamp } from 'lodash-es';
|
||||
|
||||
export class SunWeather extends Weather<CustomRenderItem> {
|
||||
/** 阳光图片 */
|
||||
private image: HTMLImageElement | null = null;
|
||||
private image: ImageBitmap | null = null;
|
||||
/** 阳光图片的不透明度 */
|
||||
private alpha: number = 0;
|
||||
/** 阳光的最大不透明度 */
|
||||
|
||||
@ -49,7 +49,7 @@ class GameLoading extends EventEmitter<GameLoadEvent> {
|
||||
* @param autotiles 自动元件数组
|
||||
*/
|
||||
onAutotileLoaded(
|
||||
autotiles: Partial<Record<AllIdsOf<'autotile'>, HTMLImageElement>>
|
||||
autotiles: Partial<Record<AllIdsOf<'autotile'>, ImageBitmap>>
|
||||
) {
|
||||
if (this.autotileListened) return;
|
||||
this.autotileListened = true;
|
||||
|
||||
@ -3,8 +3,7 @@
|
||||
export function initUI() {
|
||||
if (main.mode === 'editor') return;
|
||||
if (!main.replayChecking) {
|
||||
const { mainUi, fixedUi, mainSetting } =
|
||||
Mota.require('@motajs/legacy-ui');
|
||||
const { mainUi } = Mota.require('@motajs/legacy-ui');
|
||||
|
||||
ui.prototype.drawBook = function () {
|
||||
if (!core.isReplaying()) return mainUi.open('book');
|
||||
@ -25,11 +24,6 @@ export function initUI() {
|
||||
control.prototype.showStatusBar = function () {
|
||||
if (main.mode === 'editor') return;
|
||||
core.removeFlag('hideStatusBar');
|
||||
if (mainSetting.getValue('ui.tips')) {
|
||||
if (!fixedUi.hasName('tips')) {
|
||||
fixedUi.open('tips');
|
||||
}
|
||||
}
|
||||
core.updateStatusBar();
|
||||
};
|
||||
|
||||
@ -39,8 +33,6 @@ export function initUI() {
|
||||
// 如果原本就是隐藏的,则先显示
|
||||
if (!core.domStyle.showStatusBar) this.showStatusBar();
|
||||
if (core.isReplaying()) showToolbox = true;
|
||||
fixedUi.closeByName('tips');
|
||||
|
||||
core.setFlag('hideStatusBar', true);
|
||||
core.setFlag('showToolbox', showToolbox || null);
|
||||
core.updateStatusBar();
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import { IAudioVolumeEffect, IMotaAudioContext } from './types';
|
||||
import { IAudioVolumeEffect, IMotaAudioContext, ISoundPlayer } from './types';
|
||||
|
||||
type LocationArray = [number, number, number];
|
||||
|
||||
export class SoundPlayer<T extends string = SoundIds> {
|
||||
export class SoundPlayer<
|
||||
T extends string = SoundIds
|
||||
> implements ISoundPlayer<T> {
|
||||
/** 每个音效的唯一标识符 */
|
||||
private num: number = 0;
|
||||
|
||||
@ -50,13 +52,17 @@ export class SoundPlayer<T extends string = SoundIds> {
|
||||
* @param id 音效名称
|
||||
* @param data 音效的Uint8Array数据
|
||||
*/
|
||||
async add(id: T, data: Uint8Array) {
|
||||
const buffer = await this.ac.decodeToAudioBuffer(data);
|
||||
if (!buffer) {
|
||||
logger.warn(51, id);
|
||||
return;
|
||||
async add(id: T, data: Uint8Array | AudioBuffer) {
|
||||
if (data instanceof Uint8Array) {
|
||||
const buffer = await this.ac.decodeToAudioBuffer(data);
|
||||
if (!buffer) {
|
||||
logger.warn(51, id);
|
||||
return;
|
||||
}
|
||||
this.buffer.set(id, buffer);
|
||||
} else {
|
||||
this.buffer.set(id, data);
|
||||
}
|
||||
this.buffer.set(id, buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -123,6 +123,12 @@ export class AudioStreamSource
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
unpiped(controller: IStreamController): void {
|
||||
if (this.controller === controller) {
|
||||
this.controller = null;
|
||||
}
|
||||
}
|
||||
|
||||
async pump(data: Uint8Array | undefined, done: boolean): Promise<void> {
|
||||
if (!data || this.errored) return;
|
||||
if (!this.headerRecieved) {
|
||||
|
||||
@ -611,7 +611,7 @@ export interface ISoundPlayer<T extends string> {
|
||||
* @param id 音效名称
|
||||
* @param data 音效的Uint8Array数据
|
||||
*/
|
||||
add(id: T, data: Uint8Array): Promise<void>;
|
||||
add(id: T, data: Uint8Array | AudioBuffer): Promise<void>;
|
||||
|
||||
/**
|
||||
* 播放一个音效
|
||||
|
||||
@ -149,6 +149,7 @@
|
||||
"92": "Followers can only be added when the last follower is not moving.",
|
||||
"93": "Followers can only be removed when the last follower is not moving.",
|
||||
"94": "Expecting an excitation binding when using '$1'",
|
||||
"95": "Task adding is required before start loading.",
|
||||
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency."
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
|
||||
interface DisposableEvent<T> {
|
||||
active: [value: T];
|
||||
dispose: [value: T];
|
||||
destroy: [];
|
||||
}
|
||||
|
||||
export class Disposable<T> extends EventEmitter<DisposableEvent<T>> {
|
||||
protected _data?: T;
|
||||
set data(value: T | null) {
|
||||
if (this.destroyed) {
|
||||
throw new Error(
|
||||
`Cannot set value of destroyed disposable variable.`
|
||||
);
|
||||
}
|
||||
if (value !== null) this._data = value;
|
||||
}
|
||||
get data(): T | null {
|
||||
if (this.destroyed) {
|
||||
throw new Error(
|
||||
`Cannot get value of destroyed disposable variable.`
|
||||
);
|
||||
}
|
||||
if (!this.activated) {
|
||||
return null;
|
||||
}
|
||||
return this._data!;
|
||||
}
|
||||
|
||||
protected activated: boolean = false;
|
||||
protected destroyed: boolean = false;
|
||||
|
||||
constructor(data: T) {
|
||||
super();
|
||||
this._data = data;
|
||||
}
|
||||
|
||||
active() {
|
||||
if (this.activated) return;
|
||||
this.activated = true;
|
||||
this.emit('active', this._data!);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (!this.activated) return;
|
||||
this.activated = false;
|
||||
this.emit('dispose', this._data!);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.destroyed) return;
|
||||
this.destroyed = true;
|
||||
this.emit('destroy');
|
||||
delete this._data;
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,3 @@
|
||||
export * from './patch';
|
||||
export * from './disposable';
|
||||
export * from './eventEmitter';
|
||||
export * from './resource';
|
||||
export * from './utils';
|
||||
|
||||
@ -1,712 +0,0 @@
|
||||
import axios, { AxiosRequestConfig, ResponseType } from 'axios';
|
||||
import { EventEmitter } from './eventEmitter';
|
||||
import { Disposable } from './disposable';
|
||||
import { logger } from '@motajs/common';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
type ProgressFn = (now: number, total: number) => void;
|
||||
|
||||
interface ResourceType {
|
||||
text: string;
|
||||
buffer: ArrayBuffer;
|
||||
image: HTMLImageElement;
|
||||
material: HTMLImageElement;
|
||||
audio: HTMLAudioElement;
|
||||
json: any;
|
||||
zip: JSZip;
|
||||
byte: Uint8Array;
|
||||
}
|
||||
|
||||
interface ResourceMap {
|
||||
text: TextResource;
|
||||
buffer: BufferResource;
|
||||
image: ImageResource;
|
||||
material: MaterialResource;
|
||||
audio: AudioResource;
|
||||
json: JSONResource;
|
||||
zip: ZipResource;
|
||||
byte: ByteResource;
|
||||
}
|
||||
|
||||
interface CompressedLoadListItem {
|
||||
type: keyof ResourceType;
|
||||
name: string;
|
||||
usage: string;
|
||||
}
|
||||
type CompressedLoadList = Record<string, CompressedLoadListItem[]>;
|
||||
|
||||
const types: Record<keyof ResourceType, JSZip.OutputType> = {
|
||||
text: 'string',
|
||||
buffer: 'arraybuffer',
|
||||
image: 'blob',
|
||||
material: 'blob',
|
||||
audio: 'arraybuffer',
|
||||
json: 'string',
|
||||
zip: 'arraybuffer',
|
||||
byte: 'uint8array'
|
||||
};
|
||||
|
||||
const base = import.meta.env.DEV ? '/' : '';
|
||||
|
||||
function toURL(uri: string) {
|
||||
return import.meta.env.DEV ? uri : `${import.meta.env.BASE_URL}${uri}`;
|
||||
}
|
||||
|
||||
export abstract class Resource<T = any> extends Disposable<string> {
|
||||
type = 'none';
|
||||
|
||||
uri: string = '';
|
||||
resource?: T;
|
||||
loaded: boolean = false;
|
||||
|
||||
/**
|
||||
* 创建一个资源
|
||||
* @param uri 资源的URI,格式为 type/file
|
||||
* @param type 资源类型,不填为none,并会抛出警告
|
||||
*/
|
||||
constructor(uri: string, type: string = 'none') {
|
||||
super(uri);
|
||||
this.type = type;
|
||||
this.uri = uri;
|
||||
|
||||
if (this.type === 'none') {
|
||||
logger.warn(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载这个资源,需要被子类override
|
||||
*/
|
||||
abstract load(onProgress?: ProgressFn): Promise<T>;
|
||||
|
||||
/**
|
||||
* 解析资源URI,解析为一个URL,可以直接由请求获取
|
||||
*/
|
||||
abstract resolveURI(): string;
|
||||
|
||||
/**
|
||||
* 获取资源数据,当数据未加载完毕或未启用时返回null
|
||||
*/
|
||||
getData(): T | null {
|
||||
if (!this.activated || !this.loaded) return null;
|
||||
if (this.resource === null || this.resource === void 0) return null;
|
||||
return this.resource;
|
||||
}
|
||||
}
|
||||
|
||||
export class ImageResource extends Resource<HTMLImageElement> {
|
||||
/**
|
||||
* 创建一个图片资源
|
||||
* @param uri 图片资源的URI,格式为 image/file,例如 'image/project/images/hero.png'
|
||||
*/
|
||||
constructor(uri: string) {
|
||||
super(uri, 'image');
|
||||
}
|
||||
|
||||
load(_onProgress?: ProgressFn): Promise<HTMLImageElement> {
|
||||
const img = new Image();
|
||||
img.src = this.resolveURI();
|
||||
this.resource = img;
|
||||
return new Promise<HTMLImageElement>(res => {
|
||||
img.loading = 'eager';
|
||||
img.addEventListener('load', () => {
|
||||
this.loaded = true;
|
||||
img.setAttribute('_width', img.width.toString());
|
||||
img.setAttribute('_height', img.height.toString());
|
||||
res(img);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
resolveURI(): string {
|
||||
return toURL(`${base}${findURL(this.uri)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class MaterialResource extends ImageResource {
|
||||
/**
|
||||
* 创建一个material资源
|
||||
* @param uri 资源的URI,格式为 material/file,例如 'material/enemys.png'
|
||||
*/
|
||||
constructor(uri: string) {
|
||||
super(uri);
|
||||
this.type = 'material';
|
||||
}
|
||||
|
||||
override resolveURI(): string {
|
||||
return toURL(`${base}project/materials/${findURL(this.uri)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class TextResource extends Resource<string> {
|
||||
/**
|
||||
* 创建一个文字资源
|
||||
* @param uri 文字资源的URI,格式为 text/file,例如 'text/myText.txt'
|
||||
* 这样的话会加载塔根目录下的 myText.txt 文件
|
||||
*/
|
||||
constructor(uri: string) {
|
||||
super(uri, 'text');
|
||||
}
|
||||
|
||||
load(onProgress?: ProgressFn): Promise<string> {
|
||||
return new Promise(res => {
|
||||
createAxiosLoader<string>(
|
||||
this.resolveURI(),
|
||||
'text',
|
||||
onProgress
|
||||
).then(value => {
|
||||
this.resource = value.data;
|
||||
this.loaded = true;
|
||||
res(value.data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
resolveURI(): string {
|
||||
return toURL(`${base}${findURL(this.uri)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class BufferResource extends Resource<ArrayBuffer> {
|
||||
/**
|
||||
* 创建一个二进制缓冲区资源
|
||||
* @param uri 资源的URI,格式为 buffer/file,例如 'buffer/myBuffer.mp3'
|
||||
*/
|
||||
constructor(uri: string) {
|
||||
super(uri, 'buffer');
|
||||
}
|
||||
|
||||
load(onProgress?: ProgressFn): Promise<ArrayBuffer> {
|
||||
return new Promise(res => {
|
||||
createAxiosLoader<ArrayBuffer>(
|
||||
this.resolveURI(),
|
||||
'arraybuffer',
|
||||
onProgress
|
||||
).then(value => {
|
||||
this.resource = value.data;
|
||||
this.loaded = true;
|
||||
res(value.data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
resolveURI(): string {
|
||||
return toURL(`${base}${findURL(this.uri)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class ByteResource extends Resource<Uint8Array> {
|
||||
/**
|
||||
* 创建一个二进制缓冲区资源
|
||||
* @param uri 资源的URI,格式为 byte/file,例如 'byte/myBuffer.mp3'
|
||||
*/
|
||||
constructor(uri: string) {
|
||||
super(uri, 'buffer');
|
||||
}
|
||||
|
||||
async load(_onProgress?: ProgressFn): Promise<Uint8Array> {
|
||||
const response = await fetch(this.resolveURI());
|
||||
const data = await response.arrayBuffer();
|
||||
this.resource = new Uint8Array(data);
|
||||
return this.resource;
|
||||
}
|
||||
|
||||
resolveURI(): string {
|
||||
return toURL(`${base}${findURL(this.uri)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class JSONResource<T = any> extends Resource<T> {
|
||||
/**
|
||||
* 创建一个JSON对象资源
|
||||
* @param uri 资源的URI,格式为 json/file,例如 'buffer/myJSON.json'
|
||||
*/
|
||||
constructor(uri: string) {
|
||||
super(uri, 'json');
|
||||
}
|
||||
|
||||
load(onProgress?: ProgressFn): Promise<any> {
|
||||
return new Promise(res => {
|
||||
createAxiosLoader<any>(this.resolveURI(), 'json', onProgress).then(
|
||||
value => {
|
||||
this.resource = value.data;
|
||||
this.loaded = true;
|
||||
res(value.data);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
resolveURI(): string {
|
||||
return toURL(`${base}${findURL(this.uri)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class AudioResource extends Resource<HTMLAudioElement> {
|
||||
/**
|
||||
* 创建一个音乐资源
|
||||
* @param uri 音乐资源的URI,格式为 audio/file,例如 'audio/bgm.mp3'
|
||||
* 注意这个资源类型为 bgm 等只在播放时才开始流式加载的音乐资源类型,
|
||||
* 对于需要一次性加载完毕的需要使用 BufferResource 进行加载,
|
||||
* 并可以通过 AudioPlayer 类进行解析播放
|
||||
*/
|
||||
constructor(uri: string) {
|
||||
super(uri, 'audio');
|
||||
}
|
||||
|
||||
load(_onProgress?: ProgressFn): Promise<HTMLAudioElement> {
|
||||
const audio = new Audio();
|
||||
audio.src = this.resolveURI();
|
||||
audio.preload = 'none';
|
||||
this.resource = audio;
|
||||
return new Promise<HTMLAudioElement>(res => {
|
||||
this.loaded = true;
|
||||
res(audio);
|
||||
});
|
||||
}
|
||||
|
||||
resolveURI(): string {
|
||||
return toURL(`${base}project/bgms/${findURL(this.uri)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class ZipResource extends Resource<JSZip> {
|
||||
/**
|
||||
* 创建一个zip压缩资源
|
||||
* @param uri 资源的URI,格式为 zip/file,例如 'zip/myZip.h5data'
|
||||
* 注意后缀名不要是zip,不然有的浏览器会触发下载,而不是加载
|
||||
*/
|
||||
constructor(uri: string) {
|
||||
super(uri, 'zip');
|
||||
this.type = 'zip';
|
||||
}
|
||||
|
||||
async load(onProgress?: ProgressFn): Promise<JSZip> {
|
||||
const data = await new Promise<ArrayBuffer>(res => {
|
||||
createAxiosLoader<ArrayBuffer>(
|
||||
this.resolveURI(),
|
||||
'arraybuffer',
|
||||
onProgress
|
||||
).then(value => {
|
||||
res(value.data);
|
||||
});
|
||||
});
|
||||
const unzipped = await JSZip.loadAsync(data);
|
||||
this.resource = unzipped;
|
||||
this.loaded = true;
|
||||
return unzipped;
|
||||
}
|
||||
|
||||
resolveURI(): string {
|
||||
return toURL(`${base}${findURL(this.uri)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function createAxiosLoader<T = any>(
|
||||
url: string,
|
||||
responseType: ResponseType,
|
||||
onProgress?: (now: number, total: number) => void
|
||||
) {
|
||||
const config: AxiosRequestConfig<T> = {};
|
||||
config.responseType = responseType;
|
||||
if (onProgress) {
|
||||
config.onDownloadProgress = e => {
|
||||
onProgress(e.loaded, e.total ?? 0);
|
||||
};
|
||||
}
|
||||
return axios.get<T>(url, config);
|
||||
}
|
||||
|
||||
function findURL(uri: string) {
|
||||
return uri.slice(uri.indexOf('/') + 1);
|
||||
}
|
||||
|
||||
export const resourceTypeMap = {
|
||||
text: TextResource,
|
||||
buffer: BufferResource,
|
||||
image: ImageResource,
|
||||
material: MaterialResource,
|
||||
audio: AudioResource,
|
||||
json: JSONResource,
|
||||
zip: ZipResource,
|
||||
byte: ByteResource
|
||||
};
|
||||
|
||||
interface LoadEvent<T extends keyof ResourceType> {
|
||||
progress: (
|
||||
type: keyof ResourceType,
|
||||
uri: string,
|
||||
now: number,
|
||||
total: number
|
||||
) => void;
|
||||
load: (resource: ResourceMap[T]) => void | Promise<any>;
|
||||
loadStart: (resource: ResourceMap[T]) => void;
|
||||
}
|
||||
|
||||
type TaskProgressFn = (
|
||||
loadedByte: number,
|
||||
totalByte: number,
|
||||
loadedTask: number,
|
||||
totalTask: number
|
||||
) => void;
|
||||
|
||||
export class LoadTask<
|
||||
T extends keyof ResourceType = keyof ResourceType
|
||||
> extends EventEmitter<LoadEvent<T>> {
|
||||
static totalByte: number = 0;
|
||||
static loadedByte: number = 0;
|
||||
static totalTask: number = 0;
|
||||
static loadedTask: number = 0;
|
||||
static errorTask: number = 0;
|
||||
|
||||
/** 所有的资源,包括没有添加到加载任务里面的 */
|
||||
static store: Map<string, Resource> = new Map();
|
||||
static taskList: Set<LoadTask> = new Set();
|
||||
static loadedTaskList: Set<LoadTask> = new Set();
|
||||
|
||||
private static progress: TaskProgressFn;
|
||||
private static caledTask: Set<string> = new Set();
|
||||
|
||||
resource: Resource;
|
||||
type: T;
|
||||
uri: string;
|
||||
|
||||
private loadingStarted: boolean = false;
|
||||
loading: boolean = false;
|
||||
loaded: number = 0;
|
||||
|
||||
/**
|
||||
* 新建一个加载任务
|
||||
* @param type 任务类型
|
||||
* @param uri 加载内容的URL
|
||||
*/
|
||||
constructor(type: T, uri: string) {
|
||||
super();
|
||||
this.resource = new resourceTypeMap[type](uri);
|
||||
this.type = type;
|
||||
this.uri = uri;
|
||||
LoadTask.store.set(uri, this.resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行加载过程,当加载完毕后,返回的Promise将会被resolve
|
||||
* @returns 加载的Promise
|
||||
*/
|
||||
async load(): Promise<ResourceType[T]> {
|
||||
if (this.loadingStarted) {
|
||||
logger.warn(2, this.resource.type, this.resource.uri);
|
||||
return new Promise<void>(res => res());
|
||||
}
|
||||
this.loadingStarted = true;
|
||||
let totalByte = 0;
|
||||
const load = this.resource
|
||||
.load((now, total) => {
|
||||
this.loading = true;
|
||||
this.emit('progress', this.type, this.uri, now, total);
|
||||
if (!LoadTask.caledTask.has(this.uri) && total !== 0) {
|
||||
LoadTask.totalByte += total;
|
||||
totalByte = total;
|
||||
LoadTask.caledTask.add(this.uri);
|
||||
}
|
||||
this.loaded = now;
|
||||
})
|
||||
.catch(() => {
|
||||
LoadTask.errorTask++;
|
||||
logger.error(2, this.resource.type, this.resource.uri);
|
||||
});
|
||||
this.emit('loadStart', this.resource);
|
||||
const value = await load;
|
||||
LoadTask.loadedTaskList.add(this);
|
||||
this.loaded = totalByte;
|
||||
LoadTask.loadedTask++;
|
||||
await Promise.all(this.emit('load', this.resource));
|
||||
return await value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建一个加载任务,同时将任务加入加载列表
|
||||
* @param type 任务类型
|
||||
* @param uri 加载内容的URI
|
||||
*/
|
||||
static add<T extends keyof ResourceType>(
|
||||
type: T,
|
||||
uri: string
|
||||
): LoadTask<T> {
|
||||
const task = new LoadTask(type, uri);
|
||||
this.taskList.add(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将一个加载任务加入加载列表
|
||||
* @param task 要加入列表的任务
|
||||
*/
|
||||
static addTask(task: LoadTask) {
|
||||
this.taskList.add(task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行所有加载
|
||||
*/
|
||||
static async load() {
|
||||
this.totalTask = this.taskList.size;
|
||||
const fn = () => {
|
||||
this.loadedByte = [...this.taskList].reduce((prev, curr) => {
|
||||
return prev + curr.loaded;
|
||||
}, 0);
|
||||
this.progress?.(
|
||||
this.loadedByte,
|
||||
this.totalByte,
|
||||
this.loadedTask,
|
||||
this.totalTask
|
||||
);
|
||||
};
|
||||
fn();
|
||||
const interval = window.setInterval(fn, 100);
|
||||
const data = await Promise.all([...this.taskList].map(v => v.load()));
|
||||
window.clearInterval(interval);
|
||||
this.loadedByte = this.totalByte;
|
||||
fn();
|
||||
this.progress?.(
|
||||
this.totalByte,
|
||||
this.totalByte,
|
||||
this.totalTask,
|
||||
this.totalTask
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当加载进度改变时执行的函数
|
||||
*/
|
||||
static onProgress(progress: TaskProgressFn) {
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置加载设置
|
||||
*/
|
||||
static reset() {
|
||||
this.loadedByte = 0;
|
||||
this.loadedTask = 0;
|
||||
this.totalByte = 0;
|
||||
this.totalTask = 0;
|
||||
this.errorTask = 0;
|
||||
this.caledTask.clear();
|
||||
this.taskList.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export function loadDefaultResource() {
|
||||
const data = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d;
|
||||
const icon = icons_4665ee12_3a1f_44a4_bea3_0fccba634dc1;
|
||||
// bgm
|
||||
// data.main.bgms.forEach(v => {
|
||||
// const res = LoadTask.add('audio', `audio/${v}`);
|
||||
// Mota.r(() => {
|
||||
// res.once('loadStart', res => {
|
||||
// Mota.require('var', 'bgm').add(`bgms.${v}`, res.resource!);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// fonts
|
||||
data.main.fonts.forEach(v => {
|
||||
const res = LoadTask.add('buffer', `buffer/project/fonts/${v}.ttf`);
|
||||
Mota.r(() => {
|
||||
res.once('load', res => {
|
||||
document.fonts.add(new FontFace(v, res.resource!));
|
||||
});
|
||||
});
|
||||
});
|
||||
// image
|
||||
data.main.images.forEach(v => {
|
||||
const res = LoadTask.add('image', `image/project/images/${v}`);
|
||||
res.once('load', res => {
|
||||
core.material.images.images[v] = res.resource!;
|
||||
});
|
||||
});
|
||||
// sound
|
||||
data.main.sounds.forEach(v => {
|
||||
const res = LoadTask.add('byte', `byte/project/sounds/${v}`);
|
||||
Mota.r(() => {
|
||||
res.once('load', res => {
|
||||
const { soundPlayer } = Mota.require('@user/client-base');
|
||||
soundPlayer.add(v, res.resource!);
|
||||
});
|
||||
});
|
||||
});
|
||||
// tileset
|
||||
data.main.tilesets.forEach(v => {
|
||||
const res = LoadTask.add('image', `image/project/tilesets/${v}`);
|
||||
res.once('load', res => {
|
||||
core.material.images.tilesets[v] = res.resource!;
|
||||
});
|
||||
});
|
||||
// autotile
|
||||
const autotiles: Partial<Record<AllIdsOf<'autotile'>, HTMLImageElement>> =
|
||||
{};
|
||||
Object.keys(icon.autotile).forEach(v => {
|
||||
const res = LoadTask.add('image', `image/project/autotiles/${v}.png`);
|
||||
res.once('load', res => {
|
||||
autotiles[v as AllIdsOf<'autotile'>] = res.resource;
|
||||
const { loading } = Mota.require('@user/data-base');
|
||||
loading.addAutotileLoaded();
|
||||
loading.onAutotileLoaded(autotiles);
|
||||
core.material.images.autotile[v as AllIdsOf<'autotile'>] =
|
||||
res.resource!;
|
||||
});
|
||||
});
|
||||
// materials
|
||||
const imgs = core.materials.slice() as SelectKey<
|
||||
MaterialImages,
|
||||
HTMLImageElement
|
||||
>[];
|
||||
imgs.push('keyboard');
|
||||
core.materials
|
||||
.map(v => `${v}.png`)
|
||||
.forEach(v => {
|
||||
const res = LoadTask.add('material', `material/${v}`);
|
||||
res.once('load', res => {
|
||||
// @ts-expect-error 不能推导
|
||||
core.material.images[
|
||||
v.slice(0, -4) as SelectKey<
|
||||
MaterialImages,
|
||||
HTMLImageElement
|
||||
>
|
||||
] = res.resource;
|
||||
});
|
||||
});
|
||||
// animates
|
||||
{
|
||||
const res = LoadTask.add(
|
||||
'text',
|
||||
`text/all/__all_animates__?v=${
|
||||
main.version
|
||||
}&id=${data.main.animates.join(',')}`
|
||||
);
|
||||
res.once('load', res => {
|
||||
const data = res.resource!.split('@@@~~~###~~~@@@');
|
||||
data.forEach((v, i) => {
|
||||
const id = main.animates[i];
|
||||
if (v === '') {
|
||||
throw new Error(`Cannot find animate: '${id}'`);
|
||||
}
|
||||
core.material.animates[id] = core.loader._loadAnimate(v);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadCompressedResource() {
|
||||
const data = await axios.get(toURL('loadList.json'), {
|
||||
responseType: 'text'
|
||||
});
|
||||
const list: CompressedLoadList = JSON.parse(data.data);
|
||||
|
||||
// 对于区域内容,按照zip格式进行加载,然后解压处理
|
||||
const autotiles: Partial<Record<AllIdsOf<'autotile'>, HTMLImageElement>> =
|
||||
{};
|
||||
const materialImages = core.materials.slice() as SelectKey<
|
||||
MaterialImages,
|
||||
HTMLImageElement
|
||||
>[];
|
||||
materialImages.push('keyboard');
|
||||
|
||||
Object.entries(list).forEach(v => {
|
||||
const [uri, list] = v;
|
||||
const res = LoadTask.add('zip', `zip/${uri}`);
|
||||
|
||||
res.once('load', resource => {
|
||||
const res = resource.resource;
|
||||
if (!res) return;
|
||||
return Promise.all(
|
||||
list.map(async v => {
|
||||
const { type, name, usage } = v;
|
||||
const asyncType = types[type];
|
||||
const value = await res
|
||||
.file(`${type}/${name}`)
|
||||
?.async(asyncType);
|
||||
|
||||
if (!value) return;
|
||||
|
||||
// 图片类型的资源
|
||||
if (type === 'image') {
|
||||
const img = value as Blob;
|
||||
const image = new Image();
|
||||
image.src = URL.createObjectURL(img);
|
||||
image.addEventListener('load', () => {
|
||||
image.setAttribute(
|
||||
'_width',
|
||||
image.width.toString()
|
||||
);
|
||||
image.setAttribute(
|
||||
'_height',
|
||||
image.height.toString()
|
||||
);
|
||||
});
|
||||
|
||||
// 图片
|
||||
if (usage === 'image') {
|
||||
core.material.images.images[name as ImageIds] =
|
||||
image;
|
||||
} else if (usage === 'tileset') {
|
||||
// 额外素材
|
||||
core.material.images.tilesets[name] = image;
|
||||
} else if (usage === 'autotile') {
|
||||
// 自动元件
|
||||
autotiles[
|
||||
name.slice(0, -4) as AllIdsOf<'autotile'>
|
||||
] = image;
|
||||
const { loading } = Mota.require('@user/data-base');
|
||||
loading.addAutotileLoaded();
|
||||
loading.onAutotileLoaded(autotiles);
|
||||
core.material.images.autotile[
|
||||
name.slice(0, -4) as AllIdsOf<'autotile'>
|
||||
] = image;
|
||||
}
|
||||
} else if (type === 'material') {
|
||||
const img = value as Blob;
|
||||
const image = new Image();
|
||||
image.src = URL.createObjectURL(img);
|
||||
image.addEventListener('load', () => {
|
||||
image.setAttribute(
|
||||
'_width',
|
||||
image.width.toString()
|
||||
);
|
||||
image.setAttribute(
|
||||
'_height',
|
||||
image.height.toString()
|
||||
);
|
||||
});
|
||||
|
||||
// material
|
||||
if (materialImages.some(v => name === v + '.png')) {
|
||||
core.material.images[
|
||||
name.slice(0, -4) as SelectKey<
|
||||
MaterialImages,
|
||||
HTMLImageElement
|
||||
>
|
||||
] = image;
|
||||
}
|
||||
}
|
||||
|
||||
if (usage === 'font') {
|
||||
const font = value as ArrayBuffer;
|
||||
document.fonts.add(
|
||||
new FontFace(name.slice(0, -4), font)
|
||||
);
|
||||
} else if (usage === 'sound' && main.mode === 'play') {
|
||||
const { soundPlayer } =
|
||||
Mota.require('@user/client-base');
|
||||
soundPlayer.add(name as SoundIds, value as Uint8Array);
|
||||
} else if (usage === 'animate') {
|
||||
const ani = value as string;
|
||||
core.material.animates[
|
||||
name.slice(0, -8) as AnimationIds
|
||||
] = core.loader._loadAnimate(ani);
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -4,7 +4,7 @@ import { isMobile } from '../use';
|
||||
import { MotaSetting } from '../setting';
|
||||
import { triggerFullscreen } from '../utils';
|
||||
import settingsText from '../data/settings.json';
|
||||
import { fixedUi, mainUi } from './uiIns';
|
||||
import { mainUi } from './uiIns';
|
||||
import { mainSetting } from './settingIns';
|
||||
|
||||
//#region legacy-ui
|
||||
@ -13,8 +13,6 @@ export function createUI() {
|
||||
const { hook } = Mota.require('@user/data-base');
|
||||
hook.once('mounted', () => {
|
||||
const ui = document.getElementById('ui-main')!;
|
||||
const fixed = document.getElementById('ui-fixed')!;
|
||||
|
||||
const blur = mainSetting.getSetting('screen.blur');
|
||||
|
||||
mainUi.on('start', () => {
|
||||
@ -34,12 +32,6 @@ export function createUI() {
|
||||
core.closePanel();
|
||||
}
|
||||
});
|
||||
fixedUi.on('start', () => {
|
||||
fixed.style.display = 'block';
|
||||
});
|
||||
fixedUi.on('end', () => {
|
||||
fixed.style.display = 'none';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -123,21 +115,11 @@ function handleAudioSetting<T extends number | boolean>(
|
||||
}
|
||||
}
|
||||
|
||||
function handleUiSetting<T extends number | boolean>(key: string, n: T, _o: T) {
|
||||
if (key === 'danmaku') {
|
||||
if (n) {
|
||||
fixedUi.open('danmaku');
|
||||
} else {
|
||||
fixedUi.closeByName('danmaku');
|
||||
}
|
||||
} else if (key === 'tips') {
|
||||
if (n && core.isPlaying()) {
|
||||
fixedUi.open('tips');
|
||||
} else {
|
||||
fixedUi.closeByName('tips');
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleUiSetting<T extends number | boolean>(
|
||||
_key: string,
|
||||
_n: T,
|
||||
_o: T
|
||||
) {}
|
||||
|
||||
// ----- 游戏的所有设置项
|
||||
mainSetting
|
||||
|
||||
@ -14,7 +14,3 @@ mainUi.register(
|
||||
new GameUi('virtualKey', VirtualKey)
|
||||
);
|
||||
mainUi.showAll();
|
||||
|
||||
export const fixedUi = new UiController(true);
|
||||
fixedUi.register(new GameUi('load', UI.Load));
|
||||
fixedUi.showAll();
|
||||
|
||||
@ -6,4 +6,3 @@ export { default as Settings } from './settings.vue';
|
||||
export { default as Shop } from './shop.vue';
|
||||
export { default as Toolbox } from './toolbox.vue';
|
||||
export { default as Hotkey } from './hotkey.vue';
|
||||
export { default as Load } from './load.vue';
|
||||
|
||||
@ -1,106 +0,0 @@
|
||||
<template>
|
||||
<div id="load">
|
||||
<Progress
|
||||
class="task-progress"
|
||||
type="circle"
|
||||
:percent="(loading / totalTask) * 100"
|
||||
:success="{ percent: (loaded / totalTask) * 100 }"
|
||||
>
|
||||
<template #format>
|
||||
<span>{{ loaded }} / {{ totalTask }}</span>
|
||||
</template>
|
||||
</Progress>
|
||||
<div class="byte-div">
|
||||
<span class="byte-progress-tip"
|
||||
>{{ formatSize(loadedByte) }} /
|
||||
{{ formatSize(totalByte) }}</span
|
||||
>
|
||||
<Progress
|
||||
class="byte-progress"
|
||||
type="line"
|
||||
:percent="loadedPercent"
|
||||
></Progress>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import {
|
||||
loadCompressedResource,
|
||||
loadDefaultResource,
|
||||
LoadTask
|
||||
} from '@motajs/legacy-common';
|
||||
import { formatSize } from '../utils';
|
||||
import { logger } from '@motajs/common';
|
||||
import { sleep } from 'mutate-animate';
|
||||
import { IMountedVBind } from '../interface';
|
||||
import { Progress } from 'ant-design-vue';
|
||||
|
||||
const props = defineProps<IMountedVBind>();
|
||||
|
||||
const loading = ref(0);
|
||||
const loaded = ref(0);
|
||||
const loadedByte = ref(0);
|
||||
const loadedPercent = ref(0);
|
||||
const totalByte = ref(0);
|
||||
const totalTask = ref(0);
|
||||
|
||||
let loadDiv: HTMLDivElement;
|
||||
|
||||
onMounted(async () => {
|
||||
if (import.meta.env.DEV) loadDefaultResource();
|
||||
else await loadCompressedResource();
|
||||
|
||||
LoadTask.onProgress(() => {
|
||||
const loadingNum = [...LoadTask.taskList].filter(v => v.loading).length;
|
||||
|
||||
loadedByte.value = LoadTask.loadedByte;
|
||||
loadedPercent.value = parseFloat(
|
||||
((LoadTask.loadedByte / LoadTask.totalByte) * 100).toFixed(2)
|
||||
);
|
||||
loading.value = loadingNum;
|
||||
loaded.value = LoadTask.loadedTask;
|
||||
totalByte.value = LoadTask.totalByte;
|
||||
totalTask.value = LoadTask.totalTask;
|
||||
});
|
||||
|
||||
LoadTask.load().then(async () => {
|
||||
core.loader._loadMaterials_afterLoad();
|
||||
core._afterLoadResources(props.callback);
|
||||
logger.log(`Resource load end.`);
|
||||
loadDiv.style.opacity = '0';
|
||||
await sleep(500);
|
||||
Mota.require('@user/data-base').loading.emit('loaded');
|
||||
await sleep(500);
|
||||
props.controller.close(props.num);
|
||||
});
|
||||
loadDiv = document.getElementById('load') as HTMLDivElement;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#load {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
font-family: 'Arial';
|
||||
transition: opacity 1s linear;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.byte-div {
|
||||
width: 50%;
|
||||
margin-top: 10vh;
|
||||
}
|
||||
|
||||
.byte-progress {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -1 +1,4 @@
|
||||
export * from './progress';
|
||||
export * from './stream';
|
||||
export * from './task';
|
||||
export * from './types';
|
||||
|
||||
91
packages/loader/src/progress.ts
Normal file
91
packages/loader/src/progress.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { clamp } from 'lodash-es';
|
||||
import { ILoadProgressTotal, ILoadTask, LoadDataType } from './types';
|
||||
import { logger } from '@motajs/common';
|
||||
|
||||
export class LoadProgressTotal<
|
||||
T extends LoadDataType = LoadDataType,
|
||||
R = any
|
||||
> implements ILoadProgressTotal<T, R> {
|
||||
/** 当前已经附着的加载任务 */
|
||||
private readonly attached: Map<ILoadTask<T, R>, number> = new Map();
|
||||
/** 当前已经加载完毕的任务 */
|
||||
readonly loadedTasks: Set<ILoadTask<T, R>> = new Set();
|
||||
/** 当前已经添加的任务 */
|
||||
readonly addedTasks: Set<ILoadTask<T, R>> = new Set();
|
||||
|
||||
/** 总加载量 */
|
||||
private total: number = 0;
|
||||
/** 当前已经加载的字节数 */
|
||||
private loaded: number = 0;
|
||||
|
||||
/** 下一次触发 `onProgress` 时兑现 */
|
||||
private nextPromise: Promise<void>;
|
||||
/** 兑现当前的 `nextPromise` */
|
||||
private nextResolve: () => void;
|
||||
|
||||
async *[Symbol.asyncIterator]() {
|
||||
while (true) {
|
||||
if (this.loadedTasks.size === this.addedTasks.size) {
|
||||
return;
|
||||
}
|
||||
yield this.nextPromise;
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
const { promise, resolve } = Promise.withResolvers<void>();
|
||||
this.nextPromise = promise;
|
||||
this.nextResolve = resolve;
|
||||
}
|
||||
|
||||
addTask(task: ILoadTask<T, R>) {
|
||||
this.addedTasks.add(task);
|
||||
}
|
||||
|
||||
onProgress(task: ILoadTask<T, R>, loaded: number, total: number): void {
|
||||
if (!this.addedTasks.has(task)) {
|
||||
logger.warn(95);
|
||||
return;
|
||||
}
|
||||
if (!this.attached.has(task)) {
|
||||
this.total += total;
|
||||
}
|
||||
if (task.contentLoaded) {
|
||||
this.loadedTasks.add(task);
|
||||
}
|
||||
const before = this.attached.getOrInsert(task, 0);
|
||||
if (total !== 0) {
|
||||
this.loaded += loaded - before;
|
||||
}
|
||||
this.attached.set(task, loaded);
|
||||
this.nextResolve();
|
||||
const { promise, resolve } = Promise.withResolvers<void>();
|
||||
this.nextPromise = promise;
|
||||
this.nextResolve = resolve;
|
||||
}
|
||||
|
||||
getLoadedByte(): number {
|
||||
return this.loaded;
|
||||
}
|
||||
|
||||
getTotalByte(): number {
|
||||
return this.loaded;
|
||||
}
|
||||
|
||||
getLoadedTasks(): number {
|
||||
return this.loadedTasks.size;
|
||||
}
|
||||
|
||||
getAddedTasks(): number {
|
||||
return this.addedTasks.size;
|
||||
}
|
||||
|
||||
getTaskRatio(): number {
|
||||
return this.loadedTasks.size / this.addedTasks.size;
|
||||
}
|
||||
|
||||
getByteRatio(): number {
|
||||
if (this.total === 0) return 0;
|
||||
return clamp(this.loaded / this.total, 0, 1);
|
||||
}
|
||||
}
|
||||
@ -1,76 +1,15 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { IStreamLoader, IStreamReader } from './types';
|
||||
|
||||
export interface IStreamController<T = void> {
|
||||
readonly loading: boolean;
|
||||
|
||||
/**
|
||||
* 开始流传输
|
||||
*/
|
||||
start(): Promise<T>;
|
||||
|
||||
/**
|
||||
* 主动终止流传输
|
||||
* @param reason 终止原因
|
||||
*/
|
||||
cancel(reason?: string): void;
|
||||
}
|
||||
|
||||
export interface IStreamReader<T = any> {
|
||||
/**
|
||||
* 接受字节流流传输的数据
|
||||
* @param data 传入的字节流数据,只包含本分块的内容
|
||||
* @param done 是否传输完成
|
||||
*/
|
||||
pump(
|
||||
data: Uint8Array | undefined,
|
||||
done: boolean,
|
||||
response: Response
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* 当前对象被传递给加载流时执行的函数
|
||||
* @param controller 传输流控制对象
|
||||
*/
|
||||
piped(controller: IStreamController<T>): void;
|
||||
|
||||
/**
|
||||
* 开始流传输
|
||||
* @param stream 传输流对象
|
||||
* @param controller 传输流控制对象
|
||||
*/
|
||||
start(
|
||||
stream: ReadableStream,
|
||||
controller: IStreamController<T>,
|
||||
response: Response
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* 结束流传输
|
||||
* @param done 是否传输完成,如果为 false 的话,说明可能是由于出现错误导致的终止
|
||||
* @param reason 如果没有传输完成,那么表示失败的原因
|
||||
*/
|
||||
end(done: boolean, reason?: string): void;
|
||||
}
|
||||
|
||||
interface StreamLoaderEvent {
|
||||
data: [data: Uint8Array | undefined, done: boolean];
|
||||
}
|
||||
|
||||
export class StreamLoader
|
||||
extends EventEmitter<StreamLoaderEvent>
|
||||
implements IStreamController<void>
|
||||
{
|
||||
export class StreamLoader implements IStreamLoader {
|
||||
/** 传输目标 */
|
||||
private target: Set<IStreamReader> = new Set();
|
||||
/** 读取流对象 */
|
||||
private stream?: ReadableStream;
|
||||
private stream: ReadableStream | null = null;
|
||||
|
||||
loading: boolean = false;
|
||||
|
||||
constructor(public readonly url: string) {
|
||||
super();
|
||||
}
|
||||
constructor(public readonly url: string) {}
|
||||
|
||||
/**
|
||||
* 将加载流传递给字节流读取对象
|
||||
@ -83,7 +22,14 @@ export class StreamLoader
|
||||
}
|
||||
this.target.add(reader);
|
||||
reader.piped(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
unpipe(reader: IStreamReader): void {
|
||||
if (this.loading) {
|
||||
logger.warn(46);
|
||||
return;
|
||||
}
|
||||
this.target.delete(reader);
|
||||
}
|
||||
|
||||
async start() {
|
||||
|
||||
158
packages/loader/src/task.ts
Normal file
158
packages/loader/src/task.ts
Normal file
@ -0,0 +1,158 @@
|
||||
import { sumBy } from 'lodash-es';
|
||||
import {
|
||||
ILoadDataTypeMap,
|
||||
ILoadTask,
|
||||
ILoadTaskInit,
|
||||
ILoadTaskProcessor,
|
||||
ILoadTaskProgress,
|
||||
LoadDataType,
|
||||
RequestMethod
|
||||
} from './types';
|
||||
|
||||
/** 文字解码 */
|
||||
const loadTextDecoder = new TextDecoder();
|
||||
|
||||
export class LoadTask<T extends LoadDataType, R> implements ILoadTask<T, R> {
|
||||
readonly dataType: T;
|
||||
readonly identifier: string;
|
||||
readonly url: string | URL;
|
||||
readonly processor: ILoadTaskProcessor<T, R>;
|
||||
readonly progress: ILoadTaskProgress<T, R>;
|
||||
readonly method?: RequestMethod;
|
||||
readonly body?: BodyInit;
|
||||
readonly headers?: HeadersInit;
|
||||
|
||||
contentLoaded: boolean = false;
|
||||
loadedByte: number = 0;
|
||||
totalByte: number = 0;
|
||||
|
||||
/** 加载的 `Promise` */
|
||||
private readonly loadPromise: Promise<R>;
|
||||
/** 兑现加载对象 */
|
||||
private readonly loadResolve: (data: R) => void;
|
||||
|
||||
/** 加载结果 */
|
||||
private loadedData: R | null = null;
|
||||
|
||||
constructor(init: ILoadTaskInit<T, R>) {
|
||||
this.dataType = init.dataType;
|
||||
this.identifier = init.identifier;
|
||||
this.url = this.resolveURL(init.url);
|
||||
this.processor = init.processor;
|
||||
this.progress = init.progress;
|
||||
this.method = init.method;
|
||||
this.body = init.body;
|
||||
this.headers = init.headers;
|
||||
|
||||
const { promise, resolve } = Promise.withResolvers<R>();
|
||||
this.loadPromise = promise;
|
||||
this.loadResolve = resolve;
|
||||
}
|
||||
|
||||
private resolveURL(url: string | URL) {
|
||||
if (typeof url === 'string') {
|
||||
return `${import.meta.env.BASE_URL}${url}`;
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
private processUnstreamableResponse(
|
||||
response: Response
|
||||
): Promise<ILoadDataTypeMap[T]> {
|
||||
switch (this.dataType) {
|
||||
case LoadDataType.ArrayBuffer:
|
||||
return response.arrayBuffer();
|
||||
case LoadDataType.Blob:
|
||||
return response.blob();
|
||||
case LoadDataType.JSON:
|
||||
return response.json();
|
||||
case LoadDataType.Text:
|
||||
return response.text();
|
||||
case LoadDataType.Uint8Array:
|
||||
return response.bytes();
|
||||
}
|
||||
}
|
||||
|
||||
private processStreamChunkResponse(
|
||||
chunks: Uint8Array<ArrayBuffer>[]
|
||||
): ILoadDataTypeMap[T] {
|
||||
if (this.dataType === LoadDataType.Blob) {
|
||||
return new Blob(chunks);
|
||||
}
|
||||
const totalLength = sumBy(chunks, value => value.length);
|
||||
const stacked: Uint8Array<ArrayBuffer> = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
stacked.set(chunks[i], offset);
|
||||
offset += chunks[i].length;
|
||||
}
|
||||
switch (this.dataType) {
|
||||
case LoadDataType.ArrayBuffer:
|
||||
return stacked.buffer;
|
||||
case LoadDataType.Uint8Array:
|
||||
return stacked;
|
||||
}
|
||||
const text = loadTextDecoder.decode(stacked);
|
||||
switch (this.dataType) {
|
||||
case LoadDataType.Text:
|
||||
return text;
|
||||
case LoadDataType.JSON:
|
||||
return JSON.parse(text);
|
||||
}
|
||||
}
|
||||
|
||||
private async processResponse(response: Response) {
|
||||
const reader = response.body?.getReader();
|
||||
const contentLength = response.headers.get('Content-Length') ?? '0';
|
||||
const total = parseInt(contentLength, 10);
|
||||
this.loadedByte = 0;
|
||||
this.totalByte = total;
|
||||
this.progress.onProgress(this, 0, total);
|
||||
if (!reader) {
|
||||
const data = await this.processUnstreamableResponse(response);
|
||||
this.loadedByte = this.totalByte;
|
||||
this.contentLoaded = true;
|
||||
this.progress.onProgress(this, this.loadedByte, this.totalByte);
|
||||
const processed = await this.processor.process(data, this);
|
||||
this.loadedData = processed;
|
||||
this.loadResolve(processed);
|
||||
return;
|
||||
}
|
||||
let received = 0;
|
||||
const chunks: Uint8Array<ArrayBuffer>[] = [];
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (value) {
|
||||
chunks.push(value);
|
||||
received += value.byteLength;
|
||||
}
|
||||
if (done) this.contentLoaded = true;
|
||||
this.loadedByte = received;
|
||||
this.progress.onProgress(this, received, total);
|
||||
if (done) break;
|
||||
}
|
||||
const data = this.processStreamChunkResponse(chunks);
|
||||
const processed = await this.processor.process(data, this);
|
||||
this.loadedData = processed;
|
||||
this.loadResolve(processed);
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
const response = await fetch(this.url, {
|
||||
method: this.method,
|
||||
body: this.body,
|
||||
headers: this.headers
|
||||
});
|
||||
this.processResponse(response);
|
||||
return;
|
||||
}
|
||||
|
||||
loaded(): Promise<R> {
|
||||
return this.loadPromise;
|
||||
}
|
||||
|
||||
getLoadedData(): R | null {
|
||||
return this.loadedData;
|
||||
}
|
||||
}
|
||||
228
packages/loader/src/types.ts
Normal file
228
packages/loader/src/types.ts
Normal file
@ -0,0 +1,228 @@
|
||||
//#region 流传输
|
||||
|
||||
export interface IStreamController {
|
||||
/** 当前是否正在加载 */
|
||||
readonly loading: boolean;
|
||||
|
||||
/**
|
||||
* 开始流传输
|
||||
*/
|
||||
start(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 主动终止流传输
|
||||
* @param reason 终止原因
|
||||
*/
|
||||
cancel(reason?: string): void;
|
||||
}
|
||||
|
||||
export interface IStreamReader {
|
||||
/**
|
||||
* 接受字节流流传输的数据
|
||||
* @param data 传入的字节流数据,只包含本分块的内容
|
||||
* @param done 是否传输完成
|
||||
*/
|
||||
pump(
|
||||
data: Uint8Array | undefined,
|
||||
done: boolean,
|
||||
response: Response
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* 当前对象被传递给加载流时执行的函数
|
||||
* @param controller 传输流控制对象
|
||||
*/
|
||||
piped(controller: IStreamController): void;
|
||||
|
||||
/**
|
||||
* 当前对象取消指定加载流传输时执行的函数
|
||||
* @param controller 传输流控制对象
|
||||
*/
|
||||
unpiped(controller: IStreamController): void;
|
||||
|
||||
/**
|
||||
* 开始流传输
|
||||
* @param stream 传输流对象
|
||||
* @param controller 传输流控制对象
|
||||
*/
|
||||
start(
|
||||
stream: ReadableStream,
|
||||
controller: IStreamController,
|
||||
response: Response
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* 结束流传输
|
||||
* @param done 是否传输完成,如果为 false 的话,说明可能是由于出现错误导致的终止
|
||||
* @param reason 如果没有传输完成,那么表示失败的原因
|
||||
*/
|
||||
end(done: boolean, reason?: string): void;
|
||||
}
|
||||
|
||||
export interface IStreamLoader extends IStreamController {
|
||||
/**
|
||||
* 将加载流传递给字节流读取对象
|
||||
* @param reader 字节流读取对象
|
||||
*/
|
||||
pipe(reader: IStreamReader): void;
|
||||
|
||||
/**
|
||||
* 取消一个字节流读取对象的绑定
|
||||
* @param reader 字节流读取对象
|
||||
*/
|
||||
unpipe(reader: IStreamReader): void;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 加载任务
|
||||
|
||||
export const enum LoadDataType {
|
||||
ArrayBuffer,
|
||||
Uint8Array,
|
||||
Blob,
|
||||
Text,
|
||||
JSON
|
||||
}
|
||||
|
||||
export interface ILoadDataTypeMap {
|
||||
[LoadDataType.ArrayBuffer]: ArrayBuffer;
|
||||
[LoadDataType.Uint8Array]: Uint8Array<ArrayBuffer>;
|
||||
[LoadDataType.Blob]: Blob;
|
||||
[LoadDataType.Text]: string;
|
||||
[LoadDataType.JSON]: any;
|
||||
}
|
||||
|
||||
export interface ILoadTaskProcessor<T extends LoadDataType, R> {
|
||||
/**
|
||||
* 处理加载内容
|
||||
* @param response 处理前加载结果
|
||||
* @param task 加载任务对象
|
||||
*/
|
||||
process(response: ILoadDataTypeMap[T], task: ILoadTask<T, R>): Promise<R>;
|
||||
}
|
||||
|
||||
export interface ILoadTaskProgress<T extends LoadDataType, R> {
|
||||
/**
|
||||
* 更新加载进度
|
||||
* @param task 加载任务对象
|
||||
* @param loaded 已加载的字节数
|
||||
* @param total 文件总计字节数,如果此值为零说明无法读取到 `Content-Length`
|
||||
*/
|
||||
onProgress(task: ILoadTask<T, R>, loaded: number, total: number): void;
|
||||
}
|
||||
|
||||
export const enum RequestMethod {
|
||||
GET = 'GET',
|
||||
POST = 'POST',
|
||||
HEAD = 'HEAD',
|
||||
PUT = 'PUT',
|
||||
DELETE = 'DELETE',
|
||||
CONNECT = 'CONNECT',
|
||||
OPTIONS = 'OPTIONS',
|
||||
TRACE = 'TRACE',
|
||||
PATCH = 'PATCH'
|
||||
}
|
||||
|
||||
export interface ILoadTaskInit<T extends LoadDataType, R> {
|
||||
/** 请求响应格式 */
|
||||
readonly dataType: T;
|
||||
/** 加载任务标识符 */
|
||||
readonly identifier: string;
|
||||
/** 加载目标 URL */
|
||||
readonly url: string | URL;
|
||||
/** 加载的处理对象,用于处理加载结果等 */
|
||||
readonly processor: ILoadTaskProcessor<T, R>;
|
||||
/** 加载进度对象,用于监控加载进度 */
|
||||
readonly progress: ILoadTaskProgress<T, R>;
|
||||
/** 请求模式 */
|
||||
readonly method?: RequestMethod;
|
||||
/** 请求体 */
|
||||
readonly body?: BodyInit;
|
||||
/** 请求头 */
|
||||
readonly headers?: HeadersInit;
|
||||
}
|
||||
|
||||
export interface ILoadTask<T extends LoadDataType, R> extends ILoadTaskInit<
|
||||
T,
|
||||
R
|
||||
> {
|
||||
/** 当前是否加载完毕 */
|
||||
readonly contentLoaded: boolean;
|
||||
/** 已经加载的字节数 */
|
||||
readonly loadedByte: number;
|
||||
/** 该加载任务的总体字节数 */
|
||||
readonly totalByte: number;
|
||||
|
||||
/**
|
||||
* 开始此加载计划,返回一个 `Promise`,当得到服务器的响应后兑现
|
||||
*/
|
||||
start(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 返回一个 `Promise`,当本计划加载完毕后兑现,兑现结果是加载结果
|
||||
*/
|
||||
loaded(): Promise<R>;
|
||||
|
||||
/**
|
||||
* 获取加载完成后的加载结果
|
||||
*/
|
||||
getLoadedData(): R | null;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 内置组件
|
||||
|
||||
export interface ILoadProgressTotal<
|
||||
T extends LoadDataType = LoadDataType,
|
||||
R = any
|
||||
> extends ILoadTaskProgress<T, R> {
|
||||
/** 已经添加的加载任务对象 */
|
||||
readonly addedTasks: Set<ILoadTask<T, R>>;
|
||||
/** 当前已经加载完毕的任务对象 */
|
||||
readonly loadedTasks: Set<ILoadTask<T, R>>;
|
||||
|
||||
/**
|
||||
* 迭代加载进度,当 `yield` 的值被兑现时,说明加载进度更新
|
||||
*/
|
||||
[Symbol.asyncIterator](): AsyncGenerator<void, void, void>;
|
||||
|
||||
/**
|
||||
* 向该进度监听器添加加载任务对象
|
||||
* @param task 加载任务对象
|
||||
*/
|
||||
addTask(task: ILoadTask<T, R>): void;
|
||||
|
||||
/**
|
||||
* 获取总体已加载的字节数
|
||||
*/
|
||||
getLoadedByte(): number;
|
||||
|
||||
/**
|
||||
* 获取总体需要加载的字节数
|
||||
*/
|
||||
getTotalByte(): number;
|
||||
|
||||
/**
|
||||
* 获取已经加载的字节数与总体需要加载的字节数之比
|
||||
*/
|
||||
getByteRatio(): number;
|
||||
|
||||
/**
|
||||
* 获取已经加载完毕的加载任务数量
|
||||
*/
|
||||
getLoadedTasks(): number;
|
||||
|
||||
/**
|
||||
* 获取此进度监听器已经添加的加载任务对象
|
||||
*/
|
||||
getAddedTasks(): number;
|
||||
|
||||
/**
|
||||
* 获取已经加载完毕的任务数量与已添加的加载任务数量之比
|
||||
*/
|
||||
getTaskRatio(): number;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
@ -16,8 +16,10 @@ import {
|
||||
LineParams,
|
||||
QuadParams,
|
||||
RectRCircleParams,
|
||||
RectREllipseParams
|
||||
RectREllipseParams,
|
||||
ITexture
|
||||
} from '@motajs/render';
|
||||
import { Ref } from 'vue';
|
||||
|
||||
export interface BaseProps {
|
||||
/** 元素的横坐标 */
|
||||
@ -81,6 +83,8 @@ export interface BaseProps {
|
||||
export interface CustomProps extends BaseProps {
|
||||
/** 自定义的渲染函数 */
|
||||
render?: CustomRenderFunction;
|
||||
/** 更新绑定,当数组中的任意一项更新时将会自动更新此元素的渲染 */
|
||||
bindings?: Ref<any>[];
|
||||
}
|
||||
|
||||
export interface ContainerProps extends BaseProps {}
|
||||
@ -111,7 +115,7 @@ export interface TextProps extends BaseProps {
|
||||
|
||||
export interface ImageProps extends BaseProps {
|
||||
/** 图片对象 */
|
||||
image: CanvasImageSource;
|
||||
image?: ITexture | null;
|
||||
}
|
||||
|
||||
export interface CommentProps extends BaseProps {
|
||||
|
||||
@ -9,13 +9,15 @@ import {
|
||||
Image,
|
||||
IRenderItem,
|
||||
IRenderTreeRoot,
|
||||
ITexture,
|
||||
Line,
|
||||
Path,
|
||||
QuadraticCurve,
|
||||
Rect,
|
||||
RectR,
|
||||
Shader,
|
||||
Text
|
||||
Text,
|
||||
Texture
|
||||
} from '@motajs/render';
|
||||
import { IRenderTagInfo, IRenderTagManager, TagCreateFunction } from './types';
|
||||
import { logger } from '@motajs/common';
|
||||
@ -24,13 +26,13 @@ export class RenderTagManager implements IRenderTagManager {
|
||||
/** 标签注册映射 */
|
||||
private readonly tagRegistry: Map<string, IRenderTagInfo> = new Map();
|
||||
/** 空图片 */
|
||||
private readonly emptyImg: HTMLCanvasElement;
|
||||
private readonly emptyImg: ITexture;
|
||||
|
||||
constructor(readonly renderer: IRenderTreeRoot) {
|
||||
const emptyImage = document.createElement('canvas');
|
||||
emptyImage.width = 1;
|
||||
emptyImage.height = 1;
|
||||
this.emptyImg = emptyImage;
|
||||
this.emptyImg = new Texture(emptyImage);
|
||||
|
||||
this.resgiterIntrinsicTags();
|
||||
}
|
||||
@ -52,17 +54,7 @@ export class RenderTagManager implements IRenderTagManager {
|
||||
const { text = '', nocache = true, cache = false } = props;
|
||||
return this.renderer.createElement(Text, text, cache && !nocache);
|
||||
});
|
||||
this.registerTag('image', props => {
|
||||
if (!props) {
|
||||
return this.renderer.createElement(Image, this.emptyImg, false);
|
||||
}
|
||||
const {
|
||||
image = this.emptyImg,
|
||||
nocache = true,
|
||||
cache = false
|
||||
} = props;
|
||||
return this.renderer.createElement(Image, image, cache && !nocache);
|
||||
});
|
||||
this.registerTag('image', this.createStandardElement(false, Image));
|
||||
this.registerTag('shader', this.createNoParamElement(Shader));
|
||||
this.registerTag('comment', props => {
|
||||
if (!props) return this.renderer.createElement(Comment);
|
||||
|
||||
@ -2,9 +2,9 @@ import { isNil } from 'lodash-es';
|
||||
import { ITexture, ITextureStore } from './types';
|
||||
import { logger } from '@motajs/common';
|
||||
|
||||
export class TextureStore<T extends ITexture = ITexture>
|
||||
implements ITextureStore<T>
|
||||
{
|
||||
export class TextureStore<
|
||||
T extends ITexture = ITexture
|
||||
> implements ITextureStore<T> {
|
||||
private readonly texMap: Map<number, T> = new Map();
|
||||
private readonly invMap: Map<T, number> = new Map();
|
||||
private readonly aliasMap: Map<string, number> = new Map();
|
||||
|
||||
@ -2,6 +2,7 @@ import { RenderItem, Transform, MotaOffscreenCanvas2D } from '.';
|
||||
import { CanvasStyle } from '../types';
|
||||
import { Font } from '../style';
|
||||
import { IRenderImage, IRenderText } from './types';
|
||||
import { ITexture } from '../assets';
|
||||
|
||||
/** 文字的安全填充,会填充在文字的上侧和下侧,防止削顶和削底 */
|
||||
const SAFE_PAD = 1;
|
||||
@ -147,31 +148,31 @@ export class Text extends RenderItem implements IRenderText {
|
||||
}
|
||||
|
||||
export class Image extends RenderItem implements IRenderImage {
|
||||
image: CanvasImageSource;
|
||||
image: ITexture | null;
|
||||
|
||||
constructor(image: CanvasImageSource, enableCache: boolean = false) {
|
||||
constructor(enableCache: boolean = false) {
|
||||
super(enableCache);
|
||||
this.image = image;
|
||||
if (image instanceof VideoFrame || image instanceof SVGElement) {
|
||||
this.size(200, 200);
|
||||
} else {
|
||||
this.size(image.width, image.height);
|
||||
}
|
||||
this.image = null;
|
||||
}
|
||||
|
||||
protected render(
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
_transform: Transform
|
||||
): void {
|
||||
if (!this.image) return;
|
||||
const ctx = canvas.ctx;
|
||||
ctx.drawImage(this.image, 0, 0, this.width, this.height);
|
||||
const {
|
||||
source,
|
||||
rect: { x, y, w, h }
|
||||
} = this.image.render();
|
||||
ctx.drawImage(source, x, y, w, h, 0, 0, this.width, this.height);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置图片资源
|
||||
* @param image 图片资源
|
||||
*/
|
||||
setImage(image: CanvasImageSource) {
|
||||
setImage(image: ITexture | null) {
|
||||
this.image = image;
|
||||
this.update();
|
||||
}
|
||||
@ -183,7 +184,8 @@ export class Image extends RenderItem implements IRenderImage {
|
||||
): boolean {
|
||||
switch (key) {
|
||||
case 'image':
|
||||
this.setImage(nextValue);
|
||||
if (!nextValue) this.setImage(null);
|
||||
else this.setImage(nextValue);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@ -12,6 +12,7 @@ import { DefineComponent, DefineSetupFnComponent } from 'vue';
|
||||
import { JSX } from 'vue/jsx-runtime';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { SizedCanvasImageSource } from '../types';
|
||||
import { ITexture } from '../assets';
|
||||
|
||||
//#region 功能类型
|
||||
|
||||
@ -572,13 +573,13 @@ export interface IRenderText extends IRenderItem {
|
||||
|
||||
export interface IRenderImage extends IRenderItem {
|
||||
/** 当前元素的图片内容 */
|
||||
readonly image: CanvasImageSource;
|
||||
readonly image: ITexture | null;
|
||||
|
||||
/**
|
||||
* 设置图片资源
|
||||
* @param image 图片资源
|
||||
*/
|
||||
setImage(image: CanvasImageSource): void;
|
||||
setImage(image: ITexture): void;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@ -122,8 +122,7 @@ var data_comment_c456ea59_6018_45ef_8bcc_211a24c627dc = {
|
||||
"_range": "editor.mode.checkUnique(thiseval)",
|
||||
"_directory": "./project/fonts/",
|
||||
"_transform": (function (one) {
|
||||
if (one.endsWith(".ttf")) return one.substring(0, one.lastIndexOf('.'));
|
||||
return null;
|
||||
return one;
|
||||
}).toString(),
|
||||
"_docs": "使用字体",
|
||||
"_data": "在此存放所有可能使用的字体 \n 字体名不能使用中文,不能带空格或特殊字符"
|
||||
|
||||
@ -2046,9 +2046,6 @@ control.prototype._doSL_load = function (id, callback) {
|
||||
core.saves.autosave.now,
|
||||
1
|
||||
)[0];
|
||||
if (!main.replayChecking) {
|
||||
Mota.require('@motajs/legacy-ui').fixedUi.closeByName('start');
|
||||
}
|
||||
if (core.isPlaying() && !core.status.gameOver) {
|
||||
core.control.autosave(0);
|
||||
core.saves.autosave.now -= 1;
|
||||
@ -2063,11 +2060,6 @@ control.prototype._doSL_load = function (id, callback) {
|
||||
id == 'autoSave' ? id : 'save' + id,
|
||||
null,
|
||||
function (data) {
|
||||
if (!main.replayChecking && data) {
|
||||
Mota.require('@motajs/legacy-ui').fixedUi.closeByName(
|
||||
'start'
|
||||
);
|
||||
}
|
||||
if (id == 'autoSave' && data != null) {
|
||||
core.saves.autosave.data = data;
|
||||
if (!(core.saves.autosave.data instanceof Array)) {
|
||||
|
||||
@ -297,18 +297,6 @@ core.prototype.init = async function (coreData, callback) {
|
||||
core._afterLoadResources(callback);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (main.renderLoaded)
|
||||
Mota.require('@motajs/legacy-ui').fixedUi.open('load', {
|
||||
callback
|
||||
});
|
||||
else {
|
||||
Mota.require('@user/data-base').hook.once('renderLoaded', () => {
|
||||
Mota.require('@motajs/legacy-ui').fixedUi.open('load', {
|
||||
callback
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -64,8 +64,8 @@ icons.prototype.getTilesetOffset = function (id) {
|
||||
for (var i in core.tilesets) {
|
||||
var imgName = core.tilesets[i];
|
||||
var img = core.material.images.tilesets[imgName];
|
||||
var width = Math.floor(parseInt(img.getAttribute('_width')) / 32),
|
||||
height = Math.floor(parseInt(img.getAttribute('_height')) / 32);
|
||||
var width = Math.floor(img.width / 32),
|
||||
height = Math.floor(img.height / 32);
|
||||
if (id >= startOffset && id < startOffset + width * height) {
|
||||
var x = (id - startOffset) % width,
|
||||
y = Math.floor((id - startOffset) / width);
|
||||
|
||||
@ -139,6 +139,7 @@ loader.prototype._loadMaterials_async = function (onprogress, onfinished) {
|
||||
};
|
||||
|
||||
loader.prototype._loadMaterials_afterLoad = function () {
|
||||
if (main.mode === 'play') return;
|
||||
var images = core.splitImage(core.material.images['icons']);
|
||||
for (var key in core.statusBar.icons) {
|
||||
if (typeof core.statusBar.icons[key] == 'number') {
|
||||
@ -602,11 +603,11 @@ loader.prototype.freeBgm = function (name) {
|
||||
name = core.getMappedName(name);
|
||||
if (!core.material.bgms[name]) return;
|
||||
// 从cachedBgms中删除
|
||||
core.musicStatus.cachedBgms = core.musicStatus.cachedBgms.filter(function (
|
||||
t
|
||||
) {
|
||||
return t != name;
|
||||
});
|
||||
core.musicStatus.cachedBgms = core.musicStatus.cachedBgms.filter(
|
||||
function (t) {
|
||||
return t != name;
|
||||
}
|
||||
);
|
||||
// 清掉缓存
|
||||
core.material.bgms[name].removeAttribute('src');
|
||||
core.material.bgms[name].load();
|
||||
|
||||
@ -20,6 +20,7 @@ function main() {
|
||||
|
||||
this.dom = {
|
||||
body: document.body,
|
||||
// 这些是给编辑器留的
|
||||
gameDraw: document.getElementById('game-draw'),
|
||||
gameCanvas: document.getElementsByClassName('gameCanvas'),
|
||||
inputDiv: document.getElementById('inputDiv'),
|
||||
@ -64,6 +65,8 @@ function main() {
|
||||
'icons'
|
||||
];
|
||||
|
||||
// 这些也是给编辑器留的
|
||||
this.canvas = {};
|
||||
this.statusBar = {
|
||||
image: {},
|
||||
icons: {
|
||||
@ -105,8 +108,8 @@ function main() {
|
||||
btn8: 34
|
||||
}
|
||||
};
|
||||
|
||||
this.floors = {};
|
||||
this.canvas = {};
|
||||
|
||||
this.__VERSION__ = '2.10.0';
|
||||
this.__VERSION_CODE__ = 610;
|
||||
|
||||
@ -9,8 +9,13 @@ import { transformAsync } from '@babel/core';
|
||||
import archiver from 'archiver';
|
||||
import { createWriteStream } from 'fs';
|
||||
import { zip } from 'compressing';
|
||||
import { RequiredData, RequiredIconsData, ResourceType } from './types';
|
||||
import { splitResource, SplittedResource } from './build-resource';
|
||||
import { RequiredData, RequiredIconsData } from './types';
|
||||
import {
|
||||
CompressedUsage,
|
||||
LoadDataType,
|
||||
splitResource,
|
||||
SplittedResource
|
||||
} from './build-resource';
|
||||
import { formatSize } from './utils';
|
||||
|
||||
/** 打包调试 */
|
||||
@ -292,9 +297,9 @@ async function getAllChars(client: RollupOutput[]) {
|
||||
}
|
||||
|
||||
interface CompressedLoadListItem {
|
||||
type: ResourceType;
|
||||
name: string;
|
||||
usage: string;
|
||||
readonly readAs: LoadDataType;
|
||||
readonly name: string;
|
||||
readonly usage: CompressedUsage;
|
||||
}
|
||||
|
||||
type CompressedLoadList = Record<string, CompressedLoadListItem[]>;
|
||||
@ -309,7 +314,7 @@ function generateResourceJSON(resources: SplittedResource[]) {
|
||||
const uri = `project/resource/${file.fileName}`;
|
||||
file.content.forEach(content => {
|
||||
const item: CompressedLoadListItem = {
|
||||
type: content.type,
|
||||
readAs: content.readAs,
|
||||
name: content.name,
|
||||
usage: content.usage
|
||||
};
|
||||
@ -468,7 +473,7 @@ async function buildGame() {
|
||||
await Promise.all(
|
||||
fonts.map(v => {
|
||||
const fontmin = new Fontmin();
|
||||
const src = resolve(tempDir, 'client/project/fonts', `${v}.ttf`);
|
||||
const src = resolve(tempDir, 'client/project/fonts', v);
|
||||
const dest = resolve(tempDir, 'fonts');
|
||||
const plugin = Fontmin.glyph({
|
||||
text: [...chars].join('')
|
||||
|
||||
@ -1,20 +1,34 @@
|
||||
import JSZip from 'jszip';
|
||||
import {
|
||||
RequiredData,
|
||||
RequiredIconsData,
|
||||
ResourceType,
|
||||
ResourceUsage
|
||||
} from './types';
|
||||
import { RequiredData, RequiredIconsData } from './types';
|
||||
import { Stats } from 'fs';
|
||||
import { readdir, readFile, stat } from 'fs/promises';
|
||||
import { resolve } from 'path';
|
||||
import { fileHash } from './utils';
|
||||
|
||||
export const enum CompressedUsage {
|
||||
// ---- 系统加载内容,不可更改
|
||||
Font,
|
||||
Image,
|
||||
Sound,
|
||||
Tileset,
|
||||
Autotile,
|
||||
Material,
|
||||
Animate
|
||||
}
|
||||
|
||||
export const enum LoadDataType {
|
||||
ArrayBuffer,
|
||||
Uint8Array,
|
||||
Blob,
|
||||
Text,
|
||||
JSON
|
||||
}
|
||||
|
||||
export interface ResourceInfo {
|
||||
name: string;
|
||||
type: ResourceType;
|
||||
usage: ResourceUsage;
|
||||
stats: Stats;
|
||||
readonly name: string;
|
||||
readonly readAs: LoadDataType;
|
||||
readonly usage: CompressedUsage;
|
||||
readonly stats: Stats;
|
||||
}
|
||||
|
||||
export interface SplittedResource {
|
||||
@ -26,35 +40,52 @@ export interface SplittedResource {
|
||||
}
|
||||
|
||||
interface ResourceContent extends ResourceInfo {
|
||||
content: string | Buffer | Uint8Array;
|
||||
exceed: boolean;
|
||||
readonly content: string | Buffer | Uint8Array;
|
||||
readonly exceed: boolean;
|
||||
}
|
||||
|
||||
interface ResourcePath {
|
||||
name: string;
|
||||
path: string;
|
||||
usage: ResourceUsage;
|
||||
readonly name: string;
|
||||
readonly path: string;
|
||||
readonly usage: CompressedUsage;
|
||||
}
|
||||
|
||||
function getTypeByUsage(usage: ResourceUsage): ResourceType {
|
||||
function getTypeByUsage(usage: CompressedUsage): LoadDataType {
|
||||
switch (usage) {
|
||||
case 'animate':
|
||||
return 'text';
|
||||
case 'autotile':
|
||||
case 'image':
|
||||
case 'tileset':
|
||||
return 'image';
|
||||
case 'sound':
|
||||
return 'byte';
|
||||
case 'font':
|
||||
return 'buffer';
|
||||
case 'material':
|
||||
return 'material';
|
||||
case CompressedUsage.Animate:
|
||||
return LoadDataType.Text;
|
||||
case CompressedUsage.Autotile:
|
||||
case CompressedUsage.Image:
|
||||
case CompressedUsage.Tileset:
|
||||
case CompressedUsage.Material:
|
||||
return LoadDataType.Blob;
|
||||
case CompressedUsage.Font:
|
||||
case CompressedUsage.Sound:
|
||||
return LoadDataType.ArrayBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
function readFileOfType(path: string, type: ResourceType) {
|
||||
if (type === 'text') {
|
||||
function 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';
|
||||
}
|
||||
}
|
||||
|
||||
function readFileOfType(path: string, type: LoadDataType) {
|
||||
if (type === LoadDataType.Text) {
|
||||
return readFile(path, 'utf-8');
|
||||
} else {
|
||||
return readFile(path);
|
||||
@ -64,7 +95,7 @@ function readFileOfType(path: string, type: ResourceType) {
|
||||
async function compressFiles(files: ResourceContent[]) {
|
||||
const zip = new JSZip();
|
||||
files.forEach(v => {
|
||||
const dir = `${v.type}/${v.name}`;
|
||||
const dir = `${getZipFolderByUsage(v.usage)}/${v.name}`;
|
||||
zip.file(dir, v.content);
|
||||
});
|
||||
const buffer = await zip.generateAsync({
|
||||
@ -107,37 +138,37 @@ export async function splitResource(
|
||||
...animates.map<ResourcePath>(v => ({
|
||||
name: `${v}.animate`,
|
||||
path: resolve(base, 'project/animates', `${v}.animate`),
|
||||
usage: 'animate'
|
||||
usage: CompressedUsage.Animate
|
||||
})),
|
||||
...fonts.map<ResourcePath>(v => ({
|
||||
name: `${v}.ttf`,
|
||||
path: resolve(fontsDir, `${v}.ttf`),
|
||||
usage: 'font'
|
||||
name: v,
|
||||
path: resolve(fontsDir, v),
|
||||
usage: CompressedUsage.Font
|
||||
})),
|
||||
...images.map<ResourcePath>(v => ({
|
||||
name: v,
|
||||
path: resolve(base, 'project/images', v),
|
||||
usage: 'image'
|
||||
usage: CompressedUsage.Image
|
||||
})),
|
||||
...sounds.map<ResourcePath>(v => ({
|
||||
name: v,
|
||||
path: resolve(base, 'project/sounds', v),
|
||||
usage: 'sound'
|
||||
usage: CompressedUsage.Sound
|
||||
})),
|
||||
...tilesets.map<ResourcePath>(v => ({
|
||||
name: v,
|
||||
path: resolve(base, 'project/tilesets', v),
|
||||
usage: 'tileset'
|
||||
usage: CompressedUsage.Tileset
|
||||
})),
|
||||
...autotiles.map<ResourcePath>(v => ({
|
||||
name: `${v}.png`,
|
||||
path: resolve(base, 'project/autotiles', `${v}.png`),
|
||||
usage: 'autotile'
|
||||
usage: CompressedUsage.Autotile
|
||||
})),
|
||||
...materials.map<ResourcePath>(v => ({
|
||||
name: v,
|
||||
path: resolve(base, 'project/materials', v),
|
||||
usage: 'material'
|
||||
usage: CompressedUsage.Material
|
||||
}))
|
||||
];
|
||||
|
||||
@ -154,7 +185,7 @@ export async function splitResource(
|
||||
const type = getTypeByUsage(usage);
|
||||
const content = await readFileOfType(path, type);
|
||||
const info: ResourceContent = {
|
||||
type,
|
||||
readAs: type,
|
||||
name,
|
||||
usage,
|
||||
stats,
|
||||
|
||||
@ -73,11 +73,11 @@ import fs from 'fs/promises';
|
||||
'./src/types/source/data.d.ts',
|
||||
`
|
||||
${floorId}
|
||||
${d.images.length > 0 ? imgs : 'type ImageIds = never\n'}
|
||||
${d.animates.length > 0 ? anis : 'type AnimationIds = never\n'}
|
||||
${d.sounds.length > 0 ? sounds : 'type SoundIds = never\n'}
|
||||
${d.bgms.length > 0 ? bgms : 'type BgmIds = never\n'}
|
||||
${d.fonts.length > 0 ? fonts : 'type FontIds = never\n'}
|
||||
${d.images.length > 0 ? imgs : 'type ImageIds = string;\n'}
|
||||
${d.animates.length > 0 ? anis : 'type AnimationIds = string;\n'}
|
||||
${d.sounds.length > 0 ? sounds : 'type SoundIds = string;\n'}
|
||||
${d.bgms.length > 0 ? bgms : 'type BgmIds = string;\n'}
|
||||
${d.fonts.length > 0 ? fonts : 'type FontIds = string;\n'}
|
||||
${names}
|
||||
`,
|
||||
'utf-8'
|
||||
|
||||
@ -590,11 +590,11 @@ async function doDeclaration(type: string, data: string) {
|
||||
'src/types/source/data.d.ts',
|
||||
`
|
||||
${floorId}
|
||||
${d.images.length > 0 ? imgs : 'type ImageIds = never\n'}
|
||||
${d.animates.length > 0 ? anis : 'type AnimationIds = never\n'}
|
||||
${d.sounds.length > 0 ? sounds : 'type SoundIds = never\n'}
|
||||
${d.bgms.length > 0 ? bgms : 'type BgmIds = never\n'}
|
||||
${d.fonts.length > 0 ? fonts : 'type FontIds = never\n'}
|
||||
${d.images.length > 0 ? imgs : 'type ImageIds = string;\n'}
|
||||
${d.animates.length > 0 ? anis : 'type AnimationIds = string;\n'}
|
||||
${d.sounds.length > 0 ? sounds : 'type SoundIds = string;\n'}
|
||||
${d.bgms.length > 0 ? bgms : 'type BgmIds = string;\n'}
|
||||
${d.fonts.length > 0 ? fonts : 'type FontIds = string;\n'}
|
||||
${names}
|
||||
`,
|
||||
'utf-8'
|
||||
|
||||
11
src/App.vue
11
src/App.vue
@ -16,21 +16,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ui-fixed">
|
||||
<template v-for="ui of fixedUi.stack" :key="ui.num">
|
||||
<component
|
||||
:is="ui.ui.component"
|
||||
v-bind="ui.vBind ?? {}"
|
||||
v-on="ui.vOn ?? {}"
|
||||
></component>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { mainUi, fixedUi } from '@motajs/legacy-ui';
|
||||
import { mainUi } from '@motajs/legacy-ui';
|
||||
|
||||
onMounted(() => {
|
||||
const { hook } = Mota.require('@user/data-base');
|
||||
|
||||
46
src/types/declaration/core.d.ts
vendored
46
src/types/declaration/core.d.ts
vendored
@ -76,33 +76,33 @@ type MaterialImages = {
|
||||
/**
|
||||
* 各个类型的图块的图片
|
||||
*/
|
||||
[C in Exclude<Cls, 'tileset' | 'autotile'>]: HTMLImageElement;
|
||||
[C in Exclude<Cls, 'tileset' | 'autotile'>]: ImageBitmap;
|
||||
} & {
|
||||
/**
|
||||
* 空气墙
|
||||
*/
|
||||
airwall: HTMLImageElement;
|
||||
airwall: ImageBitmap;
|
||||
|
||||
/**
|
||||
* 自动元件
|
||||
*/
|
||||
autotile: Record<AllIdsOf<'autotile'>, HTMLImageElement>;
|
||||
autotile: Record<AllIdsOf<'autotile'>, ImageBitmap>;
|
||||
|
||||
/**
|
||||
* 全塔属性注册的图片
|
||||
*/
|
||||
images: Record<ImageIds, HTMLImageElement>;
|
||||
images: Record<ImageIds, ImageBitmap>;
|
||||
|
||||
/**
|
||||
* 额外素材
|
||||
*/
|
||||
tilesets: Record<string, HTMLImageElement>;
|
||||
tilesets: Record<string, ImageBitmap>;
|
||||
|
||||
keyboard: HTMLImageElement;
|
||||
keyboard: ImageBitmap;
|
||||
|
||||
hero: HTMLImageElement;
|
||||
hero: ImageBitmap;
|
||||
|
||||
icons: HTMLImageElement;
|
||||
icons: ImageBitmap;
|
||||
};
|
||||
|
||||
interface Material {
|
||||
@ -694,25 +694,6 @@ interface CoreValues {
|
||||
floorChangeTime: number;
|
||||
}
|
||||
|
||||
type CoreStatusBarElements = {
|
||||
/**
|
||||
* @deprecated 已失效,此接口已经不会被使用到\
|
||||
* 状态栏图标信息
|
||||
*/
|
||||
readonly icons: Record<string, HTMLImageElement>;
|
||||
|
||||
/**
|
||||
* @deprecated 已失效,此接口已经不会被使用到\
|
||||
* 状态栏的图标元素
|
||||
*/
|
||||
readonly image: Record<string, HTMLImageElement>;
|
||||
} & {
|
||||
/**
|
||||
* @deprecated 已失效,此接口已经不会被使用到\
|
||||
*/
|
||||
readonly [key: string]: HTMLElement;
|
||||
};
|
||||
|
||||
type Materials = [
|
||||
'animates',
|
||||
'enemys',
|
||||
@ -1094,12 +1075,6 @@ interface Main extends MainData {
|
||||
*/
|
||||
readonly bgmRemoteRoot: string;
|
||||
|
||||
/**
|
||||
* @deprecated 已失效,此接口已经不会被使用到\
|
||||
* 所有的系统画布
|
||||
*/
|
||||
readonly canvas: Record<string, CanvasRenderingContext2D>;
|
||||
|
||||
/**
|
||||
* 获得所有楼层的信息,等同于core.floors,但两者不是引用关系
|
||||
*/
|
||||
@ -1141,11 +1116,6 @@ interface Main extends MainData {
|
||||
*/
|
||||
readonly supportBunch: boolean;
|
||||
|
||||
/**
|
||||
* 状态栏的图标信息
|
||||
*/
|
||||
readonly statusBar: CoreStatusBarElements;
|
||||
|
||||
/**
|
||||
* 游戏版本
|
||||
*/
|
||||
|
||||
2
src/types/source/data.d.ts
vendored
2
src/types/source/data.d.ts
vendored
@ -48,7 +48,7 @@ type SoundIds =
|
||||
type BgmIds =
|
||||
| 'bgm.opus'
|
||||
|
||||
type FontIds = never
|
||||
type FontIds = string;
|
||||
|
||||
interface NameMap {
|
||||
'确定': 'confirm.opus';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user