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() {
|
export function create() {
|
||||||
createMaterial();
|
createMaterial();
|
||||||
|
loading.once('loaded', () => {
|
||||||
|
fallbackLoad(materials);
|
||||||
|
loading.emit('assetBuilt');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export * from './load';
|
||||||
export * from './material';
|
export * from './material';
|
||||||
|
|
||||||
export * from './ins';
|
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();
|
export const audioContext = new MotaAudioContext();
|
||||||
@ -6,3 +18,29 @@ export const audioContext = new MotaAudioContext();
|
|||||||
export const soundPlayer = new SoundPlayer(audioContext);
|
export const soundPlayer = new SoundPlayer(audioContext);
|
||||||
/** 音乐播放器 */
|
/** 音乐播放器 */
|
||||||
export const bgmPlayer = new BGMPlayer(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 { ITexture } from '@motajs/render';
|
||||||
import { materials } from './ins';
|
import {
|
||||||
import { IBlockIdentifier, IIndexedIdentifier } from './types';
|
IBlockIdentifier,
|
||||||
|
IIndexedIdentifier,
|
||||||
|
IMaterialManager
|
||||||
|
} from './types';
|
||||||
import { isNil } from 'lodash-es';
|
import { isNil } from 'lodash-es';
|
||||||
|
|
||||||
function extractClsBlocks<C extends Exclude<Cls, 'tileset'>>(
|
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 icons = core.icons.icons;
|
||||||
const images = core.material.images;
|
const images = core.material.images;
|
||||||
@ -102,12 +105,6 @@ export function fallbackLoad() {
|
|||||||
materials.addTileset(img, identifier);
|
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
|
// 地图上出现过的 tileset
|
||||||
const tilesetSet = new Set<number>();
|
const tilesetSet = new Set<number>();
|
||||||
const autotileSet = 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';
|
import { createAutotile } from './autotile';
|
||||||
|
|
||||||
export function createMaterial() {
|
export function createMaterial() {
|
||||||
createAutotile();
|
createAutotile();
|
||||||
loading.once('loaded', () => {
|
|
||||||
fallbackLoad();
|
|
||||||
loading.emit('assetBuilt');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export * from './autotile';
|
export * from './autotile';
|
||||||
export * from './builder';
|
export * from './builder';
|
||||||
export * from './fallback';
|
export * from './fallback';
|
||||||
export * from './ins';
|
|
||||||
export * from './manager';
|
export * from './manager';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './utils';
|
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
|
export interface IMaterialAsset
|
||||||
extends IDirtyTracker<boolean>,
|
extends IDirtyTracker<boolean>, IDirtyMarker<void> {
|
||||||
IDirtyMarker<void> {
|
|
||||||
/** 图集的贴图数据 */
|
/** 图集的贴图数据 */
|
||||||
readonly data: ITextureComposedData;
|
readonly data: ITextureComposedData;
|
||||||
}
|
}
|
||||||
@ -290,8 +289,7 @@ export interface IMaterialAliasGetter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IMaterialManager
|
export interface IMaterialManager
|
||||||
extends IMaterialGetter,
|
extends IMaterialGetter, IMaterialAliasGetter {
|
||||||
IMaterialAliasGetter {
|
|
||||||
/** 贴图存储,把 terrains 等内容单独分开存储 */
|
/** 贴图存储,把 terrains 等内容单独分开存储 */
|
||||||
readonly tileStore: ITextureStore;
|
readonly tileStore: ITextureStore;
|
||||||
/** tilesets 贴图存储,每个 tileset 是一个贴图对象 */
|
/** tilesets 贴图存储,每个 tileset 是一个贴图对象 */
|
||||||
@ -331,7 +329,6 @@ export interface IMaterialManager
|
|||||||
addRowAnimate(
|
addRowAnimate(
|
||||||
source: SizedCanvasImageSource,
|
source: SizedCanvasImageSource,
|
||||||
map: ArrayLike<IBlockIdentifier>,
|
map: ArrayLike<IBlockIdentifier>,
|
||||||
frames: number,
|
|
||||||
height: number
|
height: number
|
||||||
): Iterable<IMaterialData>;
|
): Iterable<IMaterialData>;
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { MotaOffscreenCanvas2D, SizedCanvasImageSource } from '@motajs/render';
|
|||||||
// 得出结论,ImageBitmap和Canvas的绘制性能不如Image,于是直接画Image就行,所以缓存基本上就是存Image
|
// 得出结论,ImageBitmap和Canvas的绘制性能不如Image,于是直接画Image就行,所以缓存基本上就是存Image
|
||||||
|
|
||||||
type ImageMapKeys = Exclude<Cls, 'tileset' | 'autotile'>;
|
type ImageMapKeys = Exclude<Cls, 'tileset' | 'autotile'>;
|
||||||
type ImageMap = Record<ImageMapKeys, HTMLImageElement>;
|
type ImageMap = Record<ImageMapKeys, ImageBitmap>;
|
||||||
|
|
||||||
const i = (img: ImageMapKeys) => {
|
const i = (img: ImageMapKeys) => {
|
||||||
return core.material.images[img];
|
return core.material.images[img];
|
||||||
@ -21,10 +21,10 @@ interface AutotileCache {
|
|||||||
type AutotileCaches = Record<AllNumbersOf<'autotile'>, AutotileCache>;
|
type AutotileCaches = Record<AllNumbersOf<'autotile'>, AutotileCache>;
|
||||||
|
|
||||||
interface TextureRequire {
|
interface TextureRequire {
|
||||||
tileset: Record<string, HTMLImageElement>;
|
tileset: Record<string, ImageBitmap>;
|
||||||
material: Record<ImageMapKeys, HTMLImageElement>;
|
material: Record<ImageMapKeys, ImageBitmap>;
|
||||||
autotile: AutotileCaches;
|
autotile: AutotileCaches;
|
||||||
images: Record<ImageIds, HTMLImageElement>;
|
images: Record<ImageIds, ImageBitmap>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RenderableDataBase {
|
interface RenderableDataBase {
|
||||||
@ -49,10 +49,10 @@ export interface AutotileRenderable extends RenderableDataBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class TextureCache {
|
class TextureCache {
|
||||||
tileset!: Record<string, HTMLImageElement>;
|
tileset!: Record<string, ImageBitmap>;
|
||||||
material: Record<ImageMapKeys, HTMLImageElement>;
|
material: Record<ImageMapKeys, ImageBitmap>;
|
||||||
autotile!: AutotileCaches;
|
autotile!: AutotileCaches;
|
||||||
images!: Record<ImageIds, HTMLImageElement>;
|
images!: Record<ImageIds, ImageBitmap>;
|
||||||
|
|
||||||
idNumberMap!: IdToNumber;
|
idNumberMap!: IdToNumber;
|
||||||
|
|
||||||
@ -76,7 +76,7 @@ class TextureCache {
|
|||||||
characterTurn2: Dir2[] = ['leftup', 'rightup', 'rightdown', 'leftdown'];
|
characterTurn2: Dir2[] = ['leftup', 'rightup', 'rightdown', 'leftdown'];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.material = imageMap as Record<ImageMapKeys, HTMLImageElement>;
|
this.material = imageMap as Record<ImageMapKeys, ImageBitmap>;
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { GameTitleUI } from './ui/title';
|
|||||||
import { createWeather } from './weather';
|
import { createWeather } from './weather';
|
||||||
import { createMainExtension } from './commonIns';
|
import { createMainExtension } from './commonIns';
|
||||||
import { createApp } from './renderer';
|
import { createApp } from './renderer';
|
||||||
|
import { LoadSceneUI } from './ui/load';
|
||||||
|
|
||||||
export function createGameRenderer() {
|
export function createGameRenderer() {
|
||||||
const App = defineComponent(_props => {
|
const App = defineComponent(_props => {
|
||||||
@ -23,6 +24,9 @@ export function createGameRenderer() {
|
|||||||
|
|
||||||
mainRenderer.hide();
|
mainRenderer.hide();
|
||||||
createApp(App).mount(mainRenderer);
|
createApp(App).mount(mainRenderer);
|
||||||
|
|
||||||
|
sceneController.open(LoadSceneUI, {});
|
||||||
|
mainRenderer.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createRender() {
|
export function createRender() {
|
||||||
@ -31,11 +35,6 @@ export function createRender() {
|
|||||||
createAction();
|
createAction();
|
||||||
createWeather();
|
createWeather();
|
||||||
|
|
||||||
loading.once('loaded', () => {
|
|
||||||
sceneController.open(GameTitleUI, {});
|
|
||||||
mainRenderer.show();
|
|
||||||
});
|
|
||||||
|
|
||||||
loading.once('assetBuilt', () => {
|
loading.once('assetBuilt', () => {
|
||||||
createMainExtension();
|
createMainExtension();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -54,6 +54,8 @@ export const MOVING_TOLERANCE = 60;
|
|||||||
/** 开关门动画的动画时长 */
|
/** 开关门动画的动画时长 */
|
||||||
export const DOOR_ANIMATE_INTERVAL = 50;
|
export const DOOR_ANIMATE_INTERVAL = 50;
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
//#region 状态栏
|
//#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;
|
export const HALF_STATUS_WIDTH = STATUS_BAR_WIDTH / 2;
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
//#region 游戏画面
|
//#region 游戏画面
|
||||||
|
|
||||||
/** 游戏画面像素宽度,宽=地图宽度+状态栏宽度*状态栏数量 */
|
/** 游戏画面像素宽度,宽=地图宽度+状态栏宽度*状态栏数量 */
|
||||||
@ -91,6 +95,8 @@ export const CENTER_LOC: ElementLocator = [
|
|||||||
0.5
|
0.5
|
||||||
];
|
];
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
//#region 通用配置
|
//#region 通用配置
|
||||||
|
|
||||||
/** 弹框的宽度,使用在内置 UI 与组件中,包括确认框、选择框、等待框等 */
|
/** 弹框的宽度,使用在内置 UI 与组件中,包括确认框、选择框、等待框等 */
|
||||||
@ -98,6 +104,31 @@ export const POP_BOX_WIDTH = MAP_WIDTH / 2;
|
|||||||
/** 默认字体 */
|
/** 默认字体 */
|
||||||
export const DEFAULT_FONT = new Font('Verdana', 16);
|
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 存档界面
|
//#region 存档界面
|
||||||
|
|
||||||
/** 存档缩略图尺寸 */
|
/** 存档缩略图尺寸 */
|
||||||
@ -115,8 +146,13 @@ export const SAVE_DOWN_PAD = 30;
|
|||||||
/** 存档页码数,调高并不会影响性能,但是如果玩家存档太多的话会导致存档体积很大 */
|
/** 存档页码数,调高并不会影响性能,但是如果玩家存档太多的话会导致存档体积很大 */
|
||||||
export const SAVE_PAGES = 1000;
|
export const SAVE_PAGES = 1000;
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
//#region 标题界面
|
//#region 标题界面
|
||||||
|
|
||||||
|
/** 标题图 */
|
||||||
|
export const TITLE_BACKGROUND_IMAGE = 'bg.jpg';
|
||||||
|
|
||||||
/** 标题文字中心横坐标 */
|
/** 标题文字中心横坐标 */
|
||||||
export const TITLE_X = HALF_WIDTH;
|
export const TITLE_X = HALF_WIDTH;
|
||||||
/** 标题文字中心纵坐标 */
|
/** 标题文字中心纵坐标 */
|
||||||
@ -136,3 +172,5 @@ export const BUTTONS_HEIGHT = 200;
|
|||||||
export const BUTTONS_X = HALF_WIDTH;
|
export const BUTTONS_X = HALF_WIDTH;
|
||||||
/** 标题界面按钮左上角纵坐标 */
|
/** 标题界面按钮左上角纵坐标 */
|
||||||
export const BUTTONS_Y = MAIN_HEIGHT - BUTTONS_HEIGHT;
|
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 { GameUI, SetupComponentOptions } from '@motajs/system';
|
||||||
import { computed, ComputedRef, defineComponent, shallowReactive } from 'vue';
|
import { computed, ComputedRef, defineComponent, shallowReactive } from 'vue';
|
||||||
import { TextContent } from '../components';
|
import { TextContent } from '../components';
|
||||||
import { ElementLocator, Font, SizedCanvasImageSource } from '@motajs/render';
|
import { ElementLocator, Font, ITexture } from '@motajs/render';
|
||||||
import { MixedToolbar, ReplayingStatus } from './toolbar';
|
import { MixedToolbar, ReplayingStatus } from './toolbar';
|
||||||
import { openViewMap } from './viewmap';
|
import { openViewMap } from './viewmap';
|
||||||
import { mainUIController } from './controller';
|
import { mainUIController } from './controller';
|
||||||
@ -12,6 +12,7 @@ import {
|
|||||||
STATUS_BAR_WIDTH
|
STATUS_BAR_WIDTH
|
||||||
} from '../shared';
|
} from '../shared';
|
||||||
import { DefaultProps } from '@motajs/render-vue';
|
import { DefaultProps } from '@motajs/render-vue';
|
||||||
|
import { materials } from '@user/client-base';
|
||||||
|
|
||||||
export interface ILeftHeroStatus {
|
export interface ILeftHeroStatus {
|
||||||
/** 楼层 id */
|
/** 楼层 id */
|
||||||
@ -69,27 +70,27 @@ export interface IRightHeroStatus {
|
|||||||
|
|
||||||
interface StatusInfo {
|
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 {
|
interface KeyLikeItem {
|
||||||
/** 属性值,经过格式化 */
|
/** 属性值,经过格式化 */
|
||||||
value: ComputedRef<string>;
|
readonly value: ComputedRef<string>;
|
||||||
/** 字体 */
|
/** 字体 */
|
||||||
font: Font;
|
readonly font: Font;
|
||||||
/** 文字颜色 */
|
/** 文字颜色 */
|
||||||
color: CanvasStyle;
|
readonly color: CanvasStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface KeyLikeInfo {
|
interface KeyLikeInfo {
|
||||||
/** 这一行包含的内容 */
|
/** 这一行包含的内容 */
|
||||||
items: KeyLikeItem[];
|
readonly items: KeyLikeItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StatusBarProps<T> extends DefaultProps {
|
interface StatusBarProps<T> extends DefaultProps {
|
||||||
@ -115,15 +116,15 @@ export const LeftStatusBar = defineComponent<StatusBarProps<ILeftHeroStatus>>(
|
|||||||
/** 状态属性的开始纵坐标 */
|
/** 状态属性的开始纵坐标 */
|
||||||
const STATUS_Y = TITLE_HEIGHT + STATUS_PAD;
|
const STATUS_Y = TITLE_HEIGHT + STATUS_PAD;
|
||||||
|
|
||||||
// 可以换成 core.material.images.images['xxx.png'] 来使用全塔属性注册的图片
|
// 可以换成 materials.getImageByAlias('xxx.png') 来使用全塔属性注册的图片
|
||||||
const hpIcon = core.statusBar.icons.hp;
|
const hpIcon = materials.getImageByAlias('icon-hp');
|
||||||
const atkIcon = core.statusBar.icons.atk;
|
const atkIcon = materials.getImageByAlias('icon-atk');
|
||||||
const defIcon = core.statusBar.icons.def;
|
const defIcon = materials.getImageByAlias('icon-def');
|
||||||
const mdefIcon = core.statusBar.icons.mdef;
|
const mdefIcon = materials.getImageByAlias('icon-mdef');
|
||||||
const moneyIcon = core.statusBar.icons.money;
|
const moneyIcon = materials.getImageByAlias('icon-money');
|
||||||
const expIcon = core.statusBar.icons.exp;
|
const expIcon = materials.getImageByAlias('icon-exp');
|
||||||
const manaIcon = core.statusBar.icons.mana;
|
const manaIcon = materials.getImageByAlias('icon-mana');
|
||||||
const lvIcon = core.statusBar.icons.lv;
|
const lvIcon = materials.getImageByAlias('icon-lv');
|
||||||
|
|
||||||
const s = p.status;
|
const s = p.status;
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
HALF_WIDTH,
|
HALF_WIDTH,
|
||||||
MAIN_HEIGHT,
|
MAIN_HEIGHT,
|
||||||
MAIN_WIDTH,
|
MAIN_WIDTH,
|
||||||
|
TITLE_BACKGROUND_IMAGE,
|
||||||
TITLE_FILL,
|
TITLE_FILL,
|
||||||
TITLE_STROKE,
|
TITLE_STROKE,
|
||||||
TITLE_STROKE_WIDTH,
|
TITLE_STROKE_WIDTH,
|
||||||
@ -34,6 +35,7 @@ import { MainSceneUI } from './main';
|
|||||||
import { adjustCover } from '../utils';
|
import { adjustCover } from '../utils';
|
||||||
import { cosh, CurveMode, linear } from '@motajs/animate';
|
import { cosh, CurveMode, linear } from '@motajs/animate';
|
||||||
import { sleep } from '@motajs/common';
|
import { sleep } from '@motajs/common';
|
||||||
|
import { materials } from '@user/client-base';
|
||||||
|
|
||||||
const enum TitleButton {
|
const enum TitleButton {
|
||||||
StartGame,
|
StartGame,
|
||||||
@ -62,12 +64,12 @@ const gameTitleProps = {
|
|||||||
} satisfies SetupComponentOptions<GameTitleProps>;
|
} satisfies SetupComponentOptions<GameTitleProps>;
|
||||||
|
|
||||||
export const GameTitle = defineComponent<GameTitleProps>(props => {
|
export const GameTitle = defineComponent<GameTitleProps>(props => {
|
||||||
const bg = core.material.images.images['bg.jpg'];
|
const bg = materials.getImageByAlias(TITLE_BACKGROUND_IMAGE);
|
||||||
|
|
||||||
//#region 计算背景图
|
//#region 计算背景图
|
||||||
const [width, height] = adjustCover(
|
const [width, height] = adjustCover(
|
||||||
bg.width,
|
bg?.width ?? MAIN_WIDTH,
|
||||||
bg.height,
|
bg?.height ?? MAIN_HEIGHT,
|
||||||
MAIN_WIDTH,
|
MAIN_WIDTH,
|
||||||
MAIN_HEIGHT
|
MAIN_HEIGHT
|
||||||
);
|
);
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import { MAIN_HEIGHT, FULL_LOC, POP_BOX_WIDTH, CENTER_LOC } from '../shared';
|
|||||||
import { openReplay, openSettings } from './settings';
|
import { openReplay, openSettings } from './settings';
|
||||||
import { openViewMap } from './viewmap';
|
import { openViewMap } from './viewmap';
|
||||||
import { DefaultProps } from '@motajs/render-vue';
|
import { DefaultProps } from '@motajs/render-vue';
|
||||||
|
import { materials } from '@user/client-base';
|
||||||
|
|
||||||
interface ToolbarProps extends DefaultProps {
|
interface ToolbarProps extends DefaultProps {
|
||||||
loc?: ElementLocator;
|
loc?: ElementLocator;
|
||||||
@ -73,15 +74,15 @@ export const PlayingToolbar = defineComponent<
|
|||||||
ToolbarEmits,
|
ToolbarEmits,
|
||||||
keyof ToolbarEmits
|
keyof ToolbarEmits
|
||||||
>((props, { emit }) => {
|
>((props, { emit }) => {
|
||||||
const bookIcon = core.statusBar.icons.book;
|
const bookIcon = materials.getImageByAlias('icon-book');
|
||||||
const flyIcon = core.statusBar.icons.fly;
|
const flyIcon = materials.getImageByAlias('icon-fly');
|
||||||
const toolIcon = core.statusBar.icons.toolbox;
|
const toolIcon = materials.getImageByAlias('icon-toolbox');
|
||||||
const equipIcon = core.statusBar.icons.equipbox;
|
const equipIcon = materials.getImageByAlias('icon-equipbox');
|
||||||
const keyIcon = core.statusBar.icons.keyboard;
|
const keyIcon = materials.getImageByAlias('icon-keyboard');
|
||||||
const shopIcon = core.statusBar.icons.shop;
|
const shopIcon = materials.getImageByAlias('icon-shop');
|
||||||
const saveIcon = core.statusBar.icons.save;
|
const saveIcon = materials.getImageByAlias('icon-save');
|
||||||
const loadIcon = core.statusBar.icons.load;
|
const loadIcon = materials.getImageByAlias('icon-load');
|
||||||
const setIcon = core.statusBar.icons.settings;
|
const setIcon = materials.getImageByAlias('icon-settings');
|
||||||
|
|
||||||
const iconFont = new Font('Verdana', 12);
|
const iconFont = new Font('Verdana', 12);
|
||||||
|
|
||||||
@ -170,8 +171,8 @@ const replayingProps = {
|
|||||||
export const ReplayingToolbar = defineComponent<ReplayingProps>(props => {
|
export const ReplayingToolbar = defineComponent<ReplayingProps>(props => {
|
||||||
const status = props.status;
|
const status = props.status;
|
||||||
|
|
||||||
const bookIcon = core.statusBar.icons.book;
|
const bookIcon = materials.getImageByAlias('icon-book');
|
||||||
const saveIcon = core.statusBar.icons.save;
|
const saveIcon = materials.getImageByAlias('icon-save');
|
||||||
const font1 = Font.defaults({ size: 16 });
|
const font1 = Font.defaults({ size: 16 });
|
||||||
const font2 = new Font('Verdana', 12);
|
const font2 = new Font('Verdana', 12);
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { clamp } from 'lodash-es';
|
|||||||
|
|
||||||
export class SunWeather extends Weather<CustomRenderItem> {
|
export class SunWeather extends Weather<CustomRenderItem> {
|
||||||
/** 阳光图片 */
|
/** 阳光图片 */
|
||||||
private image: HTMLImageElement | null = null;
|
private image: ImageBitmap | null = null;
|
||||||
/** 阳光图片的不透明度 */
|
/** 阳光图片的不透明度 */
|
||||||
private alpha: number = 0;
|
private alpha: number = 0;
|
||||||
/** 阳光的最大不透明度 */
|
/** 阳光的最大不透明度 */
|
||||||
|
|||||||
@ -49,7 +49,7 @@ class GameLoading extends EventEmitter<GameLoadEvent> {
|
|||||||
* @param autotiles 自动元件数组
|
* @param autotiles 自动元件数组
|
||||||
*/
|
*/
|
||||||
onAutotileLoaded(
|
onAutotileLoaded(
|
||||||
autotiles: Partial<Record<AllIdsOf<'autotile'>, HTMLImageElement>>
|
autotiles: Partial<Record<AllIdsOf<'autotile'>, ImageBitmap>>
|
||||||
) {
|
) {
|
||||||
if (this.autotileListened) return;
|
if (this.autotileListened) return;
|
||||||
this.autotileListened = true;
|
this.autotileListened = true;
|
||||||
|
|||||||
@ -3,8 +3,7 @@
|
|||||||
export function initUI() {
|
export function initUI() {
|
||||||
if (main.mode === 'editor') return;
|
if (main.mode === 'editor') return;
|
||||||
if (!main.replayChecking) {
|
if (!main.replayChecking) {
|
||||||
const { mainUi, fixedUi, mainSetting } =
|
const { mainUi } = Mota.require('@motajs/legacy-ui');
|
||||||
Mota.require('@motajs/legacy-ui');
|
|
||||||
|
|
||||||
ui.prototype.drawBook = function () {
|
ui.prototype.drawBook = function () {
|
||||||
if (!core.isReplaying()) return mainUi.open('book');
|
if (!core.isReplaying()) return mainUi.open('book');
|
||||||
@ -25,11 +24,6 @@ export function initUI() {
|
|||||||
control.prototype.showStatusBar = function () {
|
control.prototype.showStatusBar = function () {
|
||||||
if (main.mode === 'editor') return;
|
if (main.mode === 'editor') return;
|
||||||
core.removeFlag('hideStatusBar');
|
core.removeFlag('hideStatusBar');
|
||||||
if (mainSetting.getValue('ui.tips')) {
|
|
||||||
if (!fixedUi.hasName('tips')) {
|
|
||||||
fixedUi.open('tips');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
core.updateStatusBar();
|
core.updateStatusBar();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -39,8 +33,6 @@ export function initUI() {
|
|||||||
// 如果原本就是隐藏的,则先显示
|
// 如果原本就是隐藏的,则先显示
|
||||||
if (!core.domStyle.showStatusBar) this.showStatusBar();
|
if (!core.domStyle.showStatusBar) this.showStatusBar();
|
||||||
if (core.isReplaying()) showToolbox = true;
|
if (core.isReplaying()) showToolbox = true;
|
||||||
fixedUi.closeByName('tips');
|
|
||||||
|
|
||||||
core.setFlag('hideStatusBar', true);
|
core.setFlag('hideStatusBar', true);
|
||||||
core.setFlag('showToolbox', showToolbox || null);
|
core.setFlag('showToolbox', showToolbox || null);
|
||||||
core.updateStatusBar();
|
core.updateStatusBar();
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { logger } from '@motajs/common';
|
import { logger } from '@motajs/common';
|
||||||
import { IAudioVolumeEffect, IMotaAudioContext } from './types';
|
import { IAudioVolumeEffect, IMotaAudioContext, ISoundPlayer } from './types';
|
||||||
|
|
||||||
type LocationArray = [number, number, number];
|
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;
|
private num: number = 0;
|
||||||
|
|
||||||
@ -50,13 +52,17 @@ export class SoundPlayer<T extends string = SoundIds> {
|
|||||||
* @param id 音效名称
|
* @param id 音效名称
|
||||||
* @param data 音效的Uint8Array数据
|
* @param data 音效的Uint8Array数据
|
||||||
*/
|
*/
|
||||||
async add(id: T, data: Uint8Array) {
|
async add(id: T, data: Uint8Array | AudioBuffer) {
|
||||||
|
if (data instanceof Uint8Array) {
|
||||||
const buffer = await this.ac.decodeToAudioBuffer(data);
|
const buffer = await this.ac.decodeToAudioBuffer(data);
|
||||||
if (!buffer) {
|
if (!buffer) {
|
||||||
logger.warn(51, id);
|
logger.warn(51, id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.buffer.set(id, buffer);
|
this.buffer.set(id, buffer);
|
||||||
|
} else {
|
||||||
|
this.buffer.set(id, data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -123,6 +123,12 @@ export class AudioStreamSource
|
|||||||
this.controller = controller;
|
this.controller = controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unpiped(controller: IStreamController): void {
|
||||||
|
if (this.controller === controller) {
|
||||||
|
this.controller = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async pump(data: Uint8Array | undefined, done: boolean): Promise<void> {
|
async pump(data: Uint8Array | undefined, done: boolean): Promise<void> {
|
||||||
if (!data || this.errored) return;
|
if (!data || this.errored) return;
|
||||||
if (!this.headerRecieved) {
|
if (!this.headerRecieved) {
|
||||||
|
|||||||
@ -611,7 +611,7 @@ export interface ISoundPlayer<T extends string> {
|
|||||||
* @param id 音效名称
|
* @param id 音效名称
|
||||||
* @param data 音效的Uint8Array数据
|
* @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.",
|
"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.",
|
"93": "Followers can only be removed when the last follower is not moving.",
|
||||||
"94": "Expecting an excitation binding when using '$1'",
|
"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."
|
"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 './patch';
|
||||||
export * from './disposable';
|
|
||||||
export * from './eventEmitter';
|
export * from './eventEmitter';
|
||||||
export * from './resource';
|
|
||||||
export * from './utils';
|
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 { MotaSetting } from '../setting';
|
||||||
import { triggerFullscreen } from '../utils';
|
import { triggerFullscreen } from '../utils';
|
||||||
import settingsText from '../data/settings.json';
|
import settingsText from '../data/settings.json';
|
||||||
import { fixedUi, mainUi } from './uiIns';
|
import { mainUi } from './uiIns';
|
||||||
import { mainSetting } from './settingIns';
|
import { mainSetting } from './settingIns';
|
||||||
|
|
||||||
//#region legacy-ui
|
//#region legacy-ui
|
||||||
@ -13,8 +13,6 @@ export function createUI() {
|
|||||||
const { hook } = Mota.require('@user/data-base');
|
const { hook } = Mota.require('@user/data-base');
|
||||||
hook.once('mounted', () => {
|
hook.once('mounted', () => {
|
||||||
const ui = document.getElementById('ui-main')!;
|
const ui = document.getElementById('ui-main')!;
|
||||||
const fixed = document.getElementById('ui-fixed')!;
|
|
||||||
|
|
||||||
const blur = mainSetting.getSetting('screen.blur');
|
const blur = mainSetting.getSetting('screen.blur');
|
||||||
|
|
||||||
mainUi.on('start', () => {
|
mainUi.on('start', () => {
|
||||||
@ -34,12 +32,6 @@ export function createUI() {
|
|||||||
core.closePanel();
|
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) {
|
function handleUiSetting<T extends number | boolean>(
|
||||||
if (key === 'danmaku') {
|
_key: string,
|
||||||
if (n) {
|
_n: T,
|
||||||
fixedUi.open('danmaku');
|
_o: T
|
||||||
} else {
|
) {}
|
||||||
fixedUi.closeByName('danmaku');
|
|
||||||
}
|
|
||||||
} else if (key === 'tips') {
|
|
||||||
if (n && core.isPlaying()) {
|
|
||||||
fixedUi.open('tips');
|
|
||||||
} else {
|
|
||||||
fixedUi.closeByName('tips');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- 游戏的所有设置项
|
// ----- 游戏的所有设置项
|
||||||
mainSetting
|
mainSetting
|
||||||
|
|||||||
@ -14,7 +14,3 @@ mainUi.register(
|
|||||||
new GameUi('virtualKey', VirtualKey)
|
new GameUi('virtualKey', VirtualKey)
|
||||||
);
|
);
|
||||||
mainUi.showAll();
|
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 Shop } from './shop.vue';
|
||||||
export { default as Toolbox } from './toolbox.vue';
|
export { default as Toolbox } from './toolbox.vue';
|
||||||
export { default as Hotkey } from './hotkey.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 './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 { logger } from '@motajs/common';
|
||||||
import EventEmitter from 'eventemitter3';
|
import { IStreamLoader, IStreamReader } from './types';
|
||||||
|
|
||||||
export interface IStreamController<T = void> {
|
export class StreamLoader implements IStreamLoader {
|
||||||
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>
|
|
||||||
{
|
|
||||||
/** 传输目标 */
|
/** 传输目标 */
|
||||||
private target: Set<IStreamReader> = new Set();
|
private target: Set<IStreamReader> = new Set();
|
||||||
/** 读取流对象 */
|
/** 读取流对象 */
|
||||||
private stream?: ReadableStream;
|
private stream: ReadableStream | null = null;
|
||||||
|
|
||||||
loading: boolean = false;
|
loading: boolean = false;
|
||||||
|
|
||||||
constructor(public readonly url: string) {
|
constructor(public readonly url: string) {}
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将加载流传递给字节流读取对象
|
* 将加载流传递给字节流读取对象
|
||||||
@ -83,7 +22,14 @@ export class StreamLoader
|
|||||||
}
|
}
|
||||||
this.target.add(reader);
|
this.target.add(reader);
|
||||||
reader.piped(this);
|
reader.piped(this);
|
||||||
return this;
|
}
|
||||||
|
|
||||||
|
unpipe(reader: IStreamReader): void {
|
||||||
|
if (this.loading) {
|
||||||
|
logger.warn(46);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.target.delete(reader);
|
||||||
}
|
}
|
||||||
|
|
||||||
async start() {
|
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,
|
LineParams,
|
||||||
QuadParams,
|
QuadParams,
|
||||||
RectRCircleParams,
|
RectRCircleParams,
|
||||||
RectREllipseParams
|
RectREllipseParams,
|
||||||
|
ITexture
|
||||||
} from '@motajs/render';
|
} from '@motajs/render';
|
||||||
|
import { Ref } from 'vue';
|
||||||
|
|
||||||
export interface BaseProps {
|
export interface BaseProps {
|
||||||
/** 元素的横坐标 */
|
/** 元素的横坐标 */
|
||||||
@ -81,6 +83,8 @@ export interface BaseProps {
|
|||||||
export interface CustomProps extends BaseProps {
|
export interface CustomProps extends BaseProps {
|
||||||
/** 自定义的渲染函数 */
|
/** 自定义的渲染函数 */
|
||||||
render?: CustomRenderFunction;
|
render?: CustomRenderFunction;
|
||||||
|
/** 更新绑定,当数组中的任意一项更新时将会自动更新此元素的渲染 */
|
||||||
|
bindings?: Ref<any>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContainerProps extends BaseProps {}
|
export interface ContainerProps extends BaseProps {}
|
||||||
@ -111,7 +115,7 @@ export interface TextProps extends BaseProps {
|
|||||||
|
|
||||||
export interface ImageProps extends BaseProps {
|
export interface ImageProps extends BaseProps {
|
||||||
/** 图片对象 */
|
/** 图片对象 */
|
||||||
image: CanvasImageSource;
|
image?: ITexture | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CommentProps extends BaseProps {
|
export interface CommentProps extends BaseProps {
|
||||||
|
|||||||
@ -9,13 +9,15 @@ import {
|
|||||||
Image,
|
Image,
|
||||||
IRenderItem,
|
IRenderItem,
|
||||||
IRenderTreeRoot,
|
IRenderTreeRoot,
|
||||||
|
ITexture,
|
||||||
Line,
|
Line,
|
||||||
Path,
|
Path,
|
||||||
QuadraticCurve,
|
QuadraticCurve,
|
||||||
Rect,
|
Rect,
|
||||||
RectR,
|
RectR,
|
||||||
Shader,
|
Shader,
|
||||||
Text
|
Text,
|
||||||
|
Texture
|
||||||
} from '@motajs/render';
|
} from '@motajs/render';
|
||||||
import { IRenderTagInfo, IRenderTagManager, TagCreateFunction } from './types';
|
import { IRenderTagInfo, IRenderTagManager, TagCreateFunction } from './types';
|
||||||
import { logger } from '@motajs/common';
|
import { logger } from '@motajs/common';
|
||||||
@ -24,13 +26,13 @@ export class RenderTagManager implements IRenderTagManager {
|
|||||||
/** 标签注册映射 */
|
/** 标签注册映射 */
|
||||||
private readonly tagRegistry: Map<string, IRenderTagInfo> = new Map();
|
private readonly tagRegistry: Map<string, IRenderTagInfo> = new Map();
|
||||||
/** 空图片 */
|
/** 空图片 */
|
||||||
private readonly emptyImg: HTMLCanvasElement;
|
private readonly emptyImg: ITexture;
|
||||||
|
|
||||||
constructor(readonly renderer: IRenderTreeRoot) {
|
constructor(readonly renderer: IRenderTreeRoot) {
|
||||||
const emptyImage = document.createElement('canvas');
|
const emptyImage = document.createElement('canvas');
|
||||||
emptyImage.width = 1;
|
emptyImage.width = 1;
|
||||||
emptyImage.height = 1;
|
emptyImage.height = 1;
|
||||||
this.emptyImg = emptyImage;
|
this.emptyImg = new Texture(emptyImage);
|
||||||
|
|
||||||
this.resgiterIntrinsicTags();
|
this.resgiterIntrinsicTags();
|
||||||
}
|
}
|
||||||
@ -52,17 +54,7 @@ export class RenderTagManager implements IRenderTagManager {
|
|||||||
const { text = '', nocache = true, cache = false } = props;
|
const { text = '', nocache = true, cache = false } = props;
|
||||||
return this.renderer.createElement(Text, text, cache && !nocache);
|
return this.renderer.createElement(Text, text, cache && !nocache);
|
||||||
});
|
});
|
||||||
this.registerTag('image', props => {
|
this.registerTag('image', this.createStandardElement(false, Image));
|
||||||
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('shader', this.createNoParamElement(Shader));
|
this.registerTag('shader', this.createNoParamElement(Shader));
|
||||||
this.registerTag('comment', props => {
|
this.registerTag('comment', props => {
|
||||||
if (!props) return this.renderer.createElement(Comment);
|
if (!props) return this.renderer.createElement(Comment);
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import { isNil } from 'lodash-es';
|
|||||||
import { ITexture, ITextureStore } from './types';
|
import { ITexture, ITextureStore } from './types';
|
||||||
import { logger } from '@motajs/common';
|
import { logger } from '@motajs/common';
|
||||||
|
|
||||||
export class TextureStore<T extends ITexture = ITexture>
|
export class TextureStore<
|
||||||
implements ITextureStore<T>
|
T extends ITexture = ITexture
|
||||||
{
|
> implements ITextureStore<T> {
|
||||||
private readonly texMap: Map<number, T> = new Map();
|
private readonly texMap: Map<number, T> = new Map();
|
||||||
private readonly invMap: Map<T, number> = new Map();
|
private readonly invMap: Map<T, number> = new Map();
|
||||||
private readonly aliasMap: Map<string, 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 { CanvasStyle } from '../types';
|
||||||
import { Font } from '../style';
|
import { Font } from '../style';
|
||||||
import { IRenderImage, IRenderText } from './types';
|
import { IRenderImage, IRenderText } from './types';
|
||||||
|
import { ITexture } from '../assets';
|
||||||
|
|
||||||
/** 文字的安全填充,会填充在文字的上侧和下侧,防止削顶和削底 */
|
/** 文字的安全填充,会填充在文字的上侧和下侧,防止削顶和削底 */
|
||||||
const SAFE_PAD = 1;
|
const SAFE_PAD = 1;
|
||||||
@ -147,31 +148,31 @@ export class Text extends RenderItem implements IRenderText {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Image extends RenderItem implements IRenderImage {
|
export class Image extends RenderItem implements IRenderImage {
|
||||||
image: CanvasImageSource;
|
image: ITexture | null;
|
||||||
|
|
||||||
constructor(image: CanvasImageSource, enableCache: boolean = false) {
|
constructor(enableCache: boolean = false) {
|
||||||
super(enableCache);
|
super(enableCache);
|
||||||
this.image = image;
|
this.image = null;
|
||||||
if (image instanceof VideoFrame || image instanceof SVGElement) {
|
|
||||||
this.size(200, 200);
|
|
||||||
} else {
|
|
||||||
this.size(image.width, image.height);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(
|
protected render(
|
||||||
canvas: MotaOffscreenCanvas2D,
|
canvas: MotaOffscreenCanvas2D,
|
||||||
_transform: Transform
|
_transform: Transform
|
||||||
): void {
|
): void {
|
||||||
|
if (!this.image) return;
|
||||||
const ctx = canvas.ctx;
|
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 图片资源
|
* @param image 图片资源
|
||||||
*/
|
*/
|
||||||
setImage(image: CanvasImageSource) {
|
setImage(image: ITexture | null) {
|
||||||
this.image = image;
|
this.image = image;
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
@ -183,7 +184,8 @@ export class Image extends RenderItem implements IRenderImage {
|
|||||||
): boolean {
|
): boolean {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'image':
|
case 'image':
|
||||||
this.setImage(nextValue);
|
if (!nextValue) this.setImage(null);
|
||||||
|
else this.setImage(nextValue);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { DefineComponent, DefineSetupFnComponent } from 'vue';
|
|||||||
import { JSX } from 'vue/jsx-runtime';
|
import { JSX } from 'vue/jsx-runtime';
|
||||||
import EventEmitter from 'eventemitter3';
|
import EventEmitter from 'eventemitter3';
|
||||||
import { SizedCanvasImageSource } from '../types';
|
import { SizedCanvasImageSource } from '../types';
|
||||||
|
import { ITexture } from '../assets';
|
||||||
|
|
||||||
//#region 功能类型
|
//#region 功能类型
|
||||||
|
|
||||||
@ -572,13 +573,13 @@ export interface IRenderText extends IRenderItem {
|
|||||||
|
|
||||||
export interface IRenderImage extends IRenderItem {
|
export interface IRenderImage extends IRenderItem {
|
||||||
/** 当前元素的图片内容 */
|
/** 当前元素的图片内容 */
|
||||||
readonly image: CanvasImageSource;
|
readonly image: ITexture | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置图片资源
|
* 设置图片资源
|
||||||
* @param image 图片资源
|
* @param image 图片资源
|
||||||
*/
|
*/
|
||||||
setImage(image: CanvasImageSource): void;
|
setImage(image: ITexture): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|||||||
@ -122,8 +122,7 @@ var data_comment_c456ea59_6018_45ef_8bcc_211a24c627dc = {
|
|||||||
"_range": "editor.mode.checkUnique(thiseval)",
|
"_range": "editor.mode.checkUnique(thiseval)",
|
||||||
"_directory": "./project/fonts/",
|
"_directory": "./project/fonts/",
|
||||||
"_transform": (function (one) {
|
"_transform": (function (one) {
|
||||||
if (one.endsWith(".ttf")) return one.substring(0, one.lastIndexOf('.'));
|
return one;
|
||||||
return null;
|
|
||||||
}).toString(),
|
}).toString(),
|
||||||
"_docs": "使用字体",
|
"_docs": "使用字体",
|
||||||
"_data": "在此存放所有可能使用的字体 \n 字体名不能使用中文,不能带空格或特殊字符"
|
"_data": "在此存放所有可能使用的字体 \n 字体名不能使用中文,不能带空格或特殊字符"
|
||||||
|
|||||||
@ -2046,9 +2046,6 @@ control.prototype._doSL_load = function (id, callback) {
|
|||||||
core.saves.autosave.now,
|
core.saves.autosave.now,
|
||||||
1
|
1
|
||||||
)[0];
|
)[0];
|
||||||
if (!main.replayChecking) {
|
|
||||||
Mota.require('@motajs/legacy-ui').fixedUi.closeByName('start');
|
|
||||||
}
|
|
||||||
if (core.isPlaying() && !core.status.gameOver) {
|
if (core.isPlaying() && !core.status.gameOver) {
|
||||||
core.control.autosave(0);
|
core.control.autosave(0);
|
||||||
core.saves.autosave.now -= 1;
|
core.saves.autosave.now -= 1;
|
||||||
@ -2063,11 +2060,6 @@ control.prototype._doSL_load = function (id, callback) {
|
|||||||
id == 'autoSave' ? id : 'save' + id,
|
id == 'autoSave' ? id : 'save' + id,
|
||||||
null,
|
null,
|
||||||
function (data) {
|
function (data) {
|
||||||
if (!main.replayChecking && data) {
|
|
||||||
Mota.require('@motajs/legacy-ui').fixedUi.closeByName(
|
|
||||||
'start'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (id == 'autoSave' && data != null) {
|
if (id == 'autoSave' && data != null) {
|
||||||
core.saves.autosave.data = data;
|
core.saves.autosave.data = data;
|
||||||
if (!(core.saves.autosave.data instanceof Array)) {
|
if (!(core.saves.autosave.data instanceof Array)) {
|
||||||
|
|||||||
@ -297,18 +297,6 @@ core.prototype.init = async function (coreData, callback) {
|
|||||||
core._afterLoadResources(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) {
|
for (var i in core.tilesets) {
|
||||||
var imgName = core.tilesets[i];
|
var imgName = core.tilesets[i];
|
||||||
var img = core.material.images.tilesets[imgName];
|
var img = core.material.images.tilesets[imgName];
|
||||||
var width = Math.floor(parseInt(img.getAttribute('_width')) / 32),
|
var width = Math.floor(img.width / 32),
|
||||||
height = Math.floor(parseInt(img.getAttribute('_height')) / 32);
|
height = Math.floor(img.height / 32);
|
||||||
if (id >= startOffset && id < startOffset + width * height) {
|
if (id >= startOffset && id < startOffset + width * height) {
|
||||||
var x = (id - startOffset) % width,
|
var x = (id - startOffset) % width,
|
||||||
y = Math.floor((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 () {
|
loader.prototype._loadMaterials_afterLoad = function () {
|
||||||
|
if (main.mode === 'play') return;
|
||||||
var images = core.splitImage(core.material.images['icons']);
|
var images = core.splitImage(core.material.images['icons']);
|
||||||
for (var key in core.statusBar.icons) {
|
for (var key in core.statusBar.icons) {
|
||||||
if (typeof core.statusBar.icons[key] == 'number') {
|
if (typeof core.statusBar.icons[key] == 'number') {
|
||||||
@ -602,11 +603,11 @@ loader.prototype.freeBgm = function (name) {
|
|||||||
name = core.getMappedName(name);
|
name = core.getMappedName(name);
|
||||||
if (!core.material.bgms[name]) return;
|
if (!core.material.bgms[name]) return;
|
||||||
// 从cachedBgms中删除
|
// 从cachedBgms中删除
|
||||||
core.musicStatus.cachedBgms = core.musicStatus.cachedBgms.filter(function (
|
core.musicStatus.cachedBgms = core.musicStatus.cachedBgms.filter(
|
||||||
t
|
function (t) {
|
||||||
) {
|
|
||||||
return t != name;
|
return t != name;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
// 清掉缓存
|
// 清掉缓存
|
||||||
core.material.bgms[name].removeAttribute('src');
|
core.material.bgms[name].removeAttribute('src');
|
||||||
core.material.bgms[name].load();
|
core.material.bgms[name].load();
|
||||||
|
|||||||
@ -20,6 +20,7 @@ function main() {
|
|||||||
|
|
||||||
this.dom = {
|
this.dom = {
|
||||||
body: document.body,
|
body: document.body,
|
||||||
|
// 这些是给编辑器留的
|
||||||
gameDraw: document.getElementById('game-draw'),
|
gameDraw: document.getElementById('game-draw'),
|
||||||
gameCanvas: document.getElementsByClassName('gameCanvas'),
|
gameCanvas: document.getElementsByClassName('gameCanvas'),
|
||||||
inputDiv: document.getElementById('inputDiv'),
|
inputDiv: document.getElementById('inputDiv'),
|
||||||
@ -64,6 +65,8 @@ function main() {
|
|||||||
'icons'
|
'icons'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 这些也是给编辑器留的
|
||||||
|
this.canvas = {};
|
||||||
this.statusBar = {
|
this.statusBar = {
|
||||||
image: {},
|
image: {},
|
||||||
icons: {
|
icons: {
|
||||||
@ -105,8 +108,8 @@ function main() {
|
|||||||
btn8: 34
|
btn8: 34
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.floors = {};
|
this.floors = {};
|
||||||
this.canvas = {};
|
|
||||||
|
|
||||||
this.__VERSION__ = '2.10.0';
|
this.__VERSION__ = '2.10.0';
|
||||||
this.__VERSION_CODE__ = 610;
|
this.__VERSION_CODE__ = 610;
|
||||||
|
|||||||
@ -9,8 +9,13 @@ import { transformAsync } from '@babel/core';
|
|||||||
import archiver from 'archiver';
|
import archiver from 'archiver';
|
||||||
import { createWriteStream } from 'fs';
|
import { createWriteStream } from 'fs';
|
||||||
import { zip } from 'compressing';
|
import { zip } from 'compressing';
|
||||||
import { RequiredData, RequiredIconsData, ResourceType } from './types';
|
import { RequiredData, RequiredIconsData } from './types';
|
||||||
import { splitResource, SplittedResource } from './build-resource';
|
import {
|
||||||
|
CompressedUsage,
|
||||||
|
LoadDataType,
|
||||||
|
splitResource,
|
||||||
|
SplittedResource
|
||||||
|
} from './build-resource';
|
||||||
import { formatSize } from './utils';
|
import { formatSize } from './utils';
|
||||||
|
|
||||||
/** 打包调试 */
|
/** 打包调试 */
|
||||||
@ -292,9 +297,9 @@ async function getAllChars(client: RollupOutput[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface CompressedLoadListItem {
|
interface CompressedLoadListItem {
|
||||||
type: ResourceType;
|
readonly readAs: LoadDataType;
|
||||||
name: string;
|
readonly name: string;
|
||||||
usage: string;
|
readonly usage: CompressedUsage;
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompressedLoadList = Record<string, CompressedLoadListItem[]>;
|
type CompressedLoadList = Record<string, CompressedLoadListItem[]>;
|
||||||
@ -309,7 +314,7 @@ function generateResourceJSON(resources: SplittedResource[]) {
|
|||||||
const uri = `project/resource/${file.fileName}`;
|
const uri = `project/resource/${file.fileName}`;
|
||||||
file.content.forEach(content => {
|
file.content.forEach(content => {
|
||||||
const item: CompressedLoadListItem = {
|
const item: CompressedLoadListItem = {
|
||||||
type: content.type,
|
readAs: content.readAs,
|
||||||
name: content.name,
|
name: content.name,
|
||||||
usage: content.usage
|
usage: content.usage
|
||||||
};
|
};
|
||||||
@ -468,7 +473,7 @@ async function buildGame() {
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
fonts.map(v => {
|
fonts.map(v => {
|
||||||
const fontmin = new Fontmin();
|
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 dest = resolve(tempDir, 'fonts');
|
||||||
const plugin = Fontmin.glyph({
|
const plugin = Fontmin.glyph({
|
||||||
text: [...chars].join('')
|
text: [...chars].join('')
|
||||||
|
|||||||
@ -1,20 +1,34 @@
|
|||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import {
|
import { RequiredData, RequiredIconsData } from './types';
|
||||||
RequiredData,
|
|
||||||
RequiredIconsData,
|
|
||||||
ResourceType,
|
|
||||||
ResourceUsage
|
|
||||||
} from './types';
|
|
||||||
import { Stats } from 'fs';
|
import { Stats } from 'fs';
|
||||||
import { readdir, readFile, stat } from 'fs/promises';
|
import { readdir, readFile, stat } from 'fs/promises';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import { fileHash } from './utils';
|
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 {
|
export interface ResourceInfo {
|
||||||
name: string;
|
readonly name: string;
|
||||||
type: ResourceType;
|
readonly readAs: LoadDataType;
|
||||||
usage: ResourceUsage;
|
readonly usage: CompressedUsage;
|
||||||
stats: Stats;
|
readonly stats: Stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SplittedResource {
|
export interface SplittedResource {
|
||||||
@ -26,35 +40,52 @@ export interface SplittedResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ResourceContent extends ResourceInfo {
|
interface ResourceContent extends ResourceInfo {
|
||||||
content: string | Buffer | Uint8Array;
|
readonly content: string | Buffer | Uint8Array;
|
||||||
exceed: boolean;
|
readonly exceed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResourcePath {
|
interface ResourcePath {
|
||||||
name: string;
|
readonly name: string;
|
||||||
path: string;
|
readonly path: string;
|
||||||
usage: ResourceUsage;
|
readonly usage: CompressedUsage;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTypeByUsage(usage: ResourceUsage): ResourceType {
|
function getTypeByUsage(usage: CompressedUsage): LoadDataType {
|
||||||
switch (usage) {
|
switch (usage) {
|
||||||
case 'animate':
|
case CompressedUsage.Animate:
|
||||||
return 'text';
|
return LoadDataType.Text;
|
||||||
case 'autotile':
|
case CompressedUsage.Autotile:
|
||||||
case 'image':
|
case CompressedUsage.Image:
|
||||||
case 'tileset':
|
case CompressedUsage.Tileset:
|
||||||
return 'image';
|
case CompressedUsage.Material:
|
||||||
case 'sound':
|
return LoadDataType.Blob;
|
||||||
return 'byte';
|
case CompressedUsage.Font:
|
||||||
case 'font':
|
case CompressedUsage.Sound:
|
||||||
return 'buffer';
|
return LoadDataType.ArrayBuffer;
|
||||||
case 'material':
|
|
||||||
return 'material';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function readFileOfType(path: string, type: ResourceType) {
|
function getZipFolderByUsage(usage: CompressedUsage): string {
|
||||||
if (type === 'text') {
|
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');
|
return readFile(path, 'utf-8');
|
||||||
} else {
|
} else {
|
||||||
return readFile(path);
|
return readFile(path);
|
||||||
@ -64,7 +95,7 @@ function readFileOfType(path: string, type: ResourceType) {
|
|||||||
async function compressFiles(files: ResourceContent[]) {
|
async function compressFiles(files: ResourceContent[]) {
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
files.forEach(v => {
|
files.forEach(v => {
|
||||||
const dir = `${v.type}/${v.name}`;
|
const dir = `${getZipFolderByUsage(v.usage)}/${v.name}`;
|
||||||
zip.file(dir, v.content);
|
zip.file(dir, v.content);
|
||||||
});
|
});
|
||||||
const buffer = await zip.generateAsync({
|
const buffer = await zip.generateAsync({
|
||||||
@ -107,37 +138,37 @@ export async function splitResource(
|
|||||||
...animates.map<ResourcePath>(v => ({
|
...animates.map<ResourcePath>(v => ({
|
||||||
name: `${v}.animate`,
|
name: `${v}.animate`,
|
||||||
path: resolve(base, 'project/animates', `${v}.animate`),
|
path: resolve(base, 'project/animates', `${v}.animate`),
|
||||||
usage: 'animate'
|
usage: CompressedUsage.Animate
|
||||||
})),
|
})),
|
||||||
...fonts.map<ResourcePath>(v => ({
|
...fonts.map<ResourcePath>(v => ({
|
||||||
name: `${v}.ttf`,
|
name: v,
|
||||||
path: resolve(fontsDir, `${v}.ttf`),
|
path: resolve(fontsDir, v),
|
||||||
usage: 'font'
|
usage: CompressedUsage.Font
|
||||||
})),
|
})),
|
||||||
...images.map<ResourcePath>(v => ({
|
...images.map<ResourcePath>(v => ({
|
||||||
name: v,
|
name: v,
|
||||||
path: resolve(base, 'project/images', v),
|
path: resolve(base, 'project/images', v),
|
||||||
usage: 'image'
|
usage: CompressedUsage.Image
|
||||||
})),
|
})),
|
||||||
...sounds.map<ResourcePath>(v => ({
|
...sounds.map<ResourcePath>(v => ({
|
||||||
name: v,
|
name: v,
|
||||||
path: resolve(base, 'project/sounds', v),
|
path: resolve(base, 'project/sounds', v),
|
||||||
usage: 'sound'
|
usage: CompressedUsage.Sound
|
||||||
})),
|
})),
|
||||||
...tilesets.map<ResourcePath>(v => ({
|
...tilesets.map<ResourcePath>(v => ({
|
||||||
name: v,
|
name: v,
|
||||||
path: resolve(base, 'project/tilesets', v),
|
path: resolve(base, 'project/tilesets', v),
|
||||||
usage: 'tileset'
|
usage: CompressedUsage.Tileset
|
||||||
})),
|
})),
|
||||||
...autotiles.map<ResourcePath>(v => ({
|
...autotiles.map<ResourcePath>(v => ({
|
||||||
name: `${v}.png`,
|
name: `${v}.png`,
|
||||||
path: resolve(base, 'project/autotiles', `${v}.png`),
|
path: resolve(base, 'project/autotiles', `${v}.png`),
|
||||||
usage: 'autotile'
|
usage: CompressedUsage.Autotile
|
||||||
})),
|
})),
|
||||||
...materials.map<ResourcePath>(v => ({
|
...materials.map<ResourcePath>(v => ({
|
||||||
name: v,
|
name: v,
|
||||||
path: resolve(base, 'project/materials', 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 type = getTypeByUsage(usage);
|
||||||
const content = await readFileOfType(path, type);
|
const content = await readFileOfType(path, type);
|
||||||
const info: ResourceContent = {
|
const info: ResourceContent = {
|
||||||
type,
|
readAs: type,
|
||||||
name,
|
name,
|
||||||
usage,
|
usage,
|
||||||
stats,
|
stats,
|
||||||
|
|||||||
@ -73,11 +73,11 @@ import fs from 'fs/promises';
|
|||||||
'./src/types/source/data.d.ts',
|
'./src/types/source/data.d.ts',
|
||||||
`
|
`
|
||||||
${floorId}
|
${floorId}
|
||||||
${d.images.length > 0 ? imgs : 'type ImageIds = never\n'}
|
${d.images.length > 0 ? imgs : 'type ImageIds = string;\n'}
|
||||||
${d.animates.length > 0 ? anis : 'type AnimationIds = never\n'}
|
${d.animates.length > 0 ? anis : 'type AnimationIds = string;\n'}
|
||||||
${d.sounds.length > 0 ? sounds : 'type SoundIds = never\n'}
|
${d.sounds.length > 0 ? sounds : 'type SoundIds = string;\n'}
|
||||||
${d.bgms.length > 0 ? bgms : 'type BgmIds = never\n'}
|
${d.bgms.length > 0 ? bgms : 'type BgmIds = string;\n'}
|
||||||
${d.fonts.length > 0 ? fonts : 'type FontIds = never\n'}
|
${d.fonts.length > 0 ? fonts : 'type FontIds = string;\n'}
|
||||||
${names}
|
${names}
|
||||||
`,
|
`,
|
||||||
'utf-8'
|
'utf-8'
|
||||||
|
|||||||
@ -590,11 +590,11 @@ async function doDeclaration(type: string, data: string) {
|
|||||||
'src/types/source/data.d.ts',
|
'src/types/source/data.d.ts',
|
||||||
`
|
`
|
||||||
${floorId}
|
${floorId}
|
||||||
${d.images.length > 0 ? imgs : 'type ImageIds = never\n'}
|
${d.images.length > 0 ? imgs : 'type ImageIds = string;\n'}
|
||||||
${d.animates.length > 0 ? anis : 'type AnimationIds = never\n'}
|
${d.animates.length > 0 ? anis : 'type AnimationIds = string;\n'}
|
||||||
${d.sounds.length > 0 ? sounds : 'type SoundIds = never\n'}
|
${d.sounds.length > 0 ? sounds : 'type SoundIds = string;\n'}
|
||||||
${d.bgms.length > 0 ? bgms : 'type BgmIds = never\n'}
|
${d.bgms.length > 0 ? bgms : 'type BgmIds = string;\n'}
|
||||||
${d.fonts.length > 0 ? fonts : 'type FontIds = never\n'}
|
${d.fonts.length > 0 ? fonts : 'type FontIds = string;\n'}
|
||||||
${names}
|
${names}
|
||||||
`,
|
`,
|
||||||
'utf-8'
|
'utf-8'
|
||||||
|
|||||||
11
src/App.vue
11
src/App.vue
@ -16,21 +16,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import { mainUi, fixedUi } from '@motajs/legacy-ui';
|
import { mainUi } from '@motajs/legacy-ui';
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const { hook } = Mota.require('@user/data-base');
|
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 {
|
interface Material {
|
||||||
@ -694,25 +694,6 @@ interface CoreValues {
|
|||||||
floorChangeTime: number;
|
floorChangeTime: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type CoreStatusBarElements = {
|
|
||||||
/**
|
|
||||||
* @deprecated 已失效,此接口已经不会被使用到\
|
|
||||||
* 状态栏图标信息
|
|
||||||
*/
|
|
||||||
readonly icons: Record<string, HTMLImageElement>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated 已失效,此接口已经不会被使用到\
|
|
||||||
* 状态栏的图标元素
|
|
||||||
*/
|
|
||||||
readonly image: Record<string, HTMLImageElement>;
|
|
||||||
} & {
|
|
||||||
/**
|
|
||||||
* @deprecated 已失效,此接口已经不会被使用到\
|
|
||||||
*/
|
|
||||||
readonly [key: string]: HTMLElement;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Materials = [
|
type Materials = [
|
||||||
'animates',
|
'animates',
|
||||||
'enemys',
|
'enemys',
|
||||||
@ -1094,12 +1075,6 @@ interface Main extends MainData {
|
|||||||
*/
|
*/
|
||||||
readonly bgmRemoteRoot: string;
|
readonly bgmRemoteRoot: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated 已失效,此接口已经不会被使用到\
|
|
||||||
* 所有的系统画布
|
|
||||||
*/
|
|
||||||
readonly canvas: Record<string, CanvasRenderingContext2D>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获得所有楼层的信息,等同于core.floors,但两者不是引用关系
|
* 获得所有楼层的信息,等同于core.floors,但两者不是引用关系
|
||||||
*/
|
*/
|
||||||
@ -1141,11 +1116,6 @@ interface Main extends MainData {
|
|||||||
*/
|
*/
|
||||||
readonly supportBunch: boolean;
|
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 =
|
type BgmIds =
|
||||||
| 'bgm.opus'
|
| 'bgm.opus'
|
||||||
|
|
||||||
type FontIds = never
|
type FontIds = string;
|
||||||
|
|
||||||
interface NameMap {
|
interface NameMap {
|
||||||
'确定': 'confirm.opus';
|
'确定': 'confirm.opus';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user