refactor: 客户端加载

This commit is contained in:
unanmed 2026-03-20 16:08:43 +08:00
parent fdeee99d40
commit 4fc6db3e79
55 changed files with 1699 additions and 1234 deletions

View File

@ -1,9 +1,16 @@
import { createMaterial } from './material';
import { loading } from '@user/data-base';
import { createMaterial, fallbackLoad } from './material';
import { materials } from './ins';
export function create() {
createMaterial();
loading.once('loaded', () => {
fallbackLoad(materials);
loading.emit('assetBuilt');
});
}
export * from './load';
export * from './material';
export * from './ins';

View File

@ -1,4 +1,16 @@
import { BGMPlayer, MotaAudioContext, SoundPlayer } from '@motajs/audio';
import {
AudioType,
BGMPlayer,
MotaAudioContext,
OpusDecoder,
SoundPlayer,
VorbisDecoder
} from '@motajs/audio';
import { MotaAssetsLoader } from './load/loader';
import { AutotileProcessor, MaterialManager } from './material';
import { loadProgress } from '@user/data-base';
//#region 音频实例
/** 游戏全局音频上下文 */
export const audioContext = new MotaAudioContext();
@ -6,3 +18,29 @@ export const audioContext = new MotaAudioContext();
export const soundPlayer = new SoundPlayer(audioContext);
/** 音乐播放器 */
export const bgmPlayer = new BGMPlayer(audioContext);
audioContext.registerDecoder(AudioType.Opus, () => new OpusDecoder());
audioContext.registerDecoder(AudioType.Ogg, () => new VorbisDecoder());
//#endregion
//#region 素材实例
/** 素材管理器 */
export const materials = new MaterialManager();
/** 自动元件处理器 */
export const autotile = new AutotileProcessor(materials);
//#endregion
//#region 加载实例
/** 全局加载实例 */
export const loader = new MotaAssetsLoader(
loadProgress,
audioContext,
soundPlayer,
materials
);
//#endregion

View 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'
];

View File

@ -0,0 +1 @@
export * from './processor';

View 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
}

View 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);
}
}

View 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[]>;
}

View File

@ -1,6 +1,9 @@
import { ITexture } from '@motajs/render';
import { materials } from './ins';
import { IBlockIdentifier, IIndexedIdentifier } from './types';
import {
IBlockIdentifier,
IIndexedIdentifier,
IMaterialManager
} from './types';
import { isNil } from 'lodash-es';
function extractClsBlocks<C extends Exclude<Cls, 'tileset'>>(
@ -47,7 +50,7 @@ function addAutotile(set: Set<number>, map?: readonly (readonly number[])[]) {
/**
*
*/
export function fallbackLoad() {
export function fallbackLoad(materials: IMaterialManager) {
// 基本素材
const icons = core.icons.icons;
const images = core.material.images;
@ -102,12 +105,6 @@ export function fallbackLoad() {
materials.addTileset(img, identifier);
});
// Images
core.images.forEach((v, i) => {
const img = core.material.images.images[v];
materials.addImage(img, { index: i, alias: v });
});
// 地图上出现过的 tileset
const tilesetSet = new Set<number>();
const autotileSet = new Set<number>();

View File

@ -1,19 +1,12 @@
import { loading } from '@user/data-base';
import { fallbackLoad } from './fallback';
import { createAutotile } from './autotile';
export function createMaterial() {
createAutotile();
loading.once('loaded', () => {
fallbackLoad();
loading.emit('assetBuilt');
});
}
export * from './autotile';
export * from './builder';
export * from './fallback';
export * from './ins';
export * from './manager';
export * from './types';
export * from './utils';

View File

@ -1,5 +0,0 @@
import { AutotileProcessor } from './autotile';
import { MaterialManager } from './manager';
export const materials = new MaterialManager();
export const autotile = new AutotileProcessor(materials);

View File

@ -102,8 +102,7 @@ export interface IMaterialFramedData {
}
export interface IMaterialAsset
extends IDirtyTracker<boolean>,
IDirtyMarker<void> {
extends IDirtyTracker<boolean>, IDirtyMarker<void> {
/** 图集的贴图数据 */
readonly data: ITextureComposedData;
}
@ -290,8 +289,7 @@ export interface IMaterialAliasGetter {
}
export interface IMaterialManager
extends IMaterialGetter,
IMaterialAliasGetter {
extends IMaterialGetter, IMaterialAliasGetter {
/** 贴图存储,把 terrains 等内容单独分开存储 */
readonly tileStore: ITextureStore;
/** tilesets 贴图存储,每个 tileset 是一个贴图对象 */
@ -331,7 +329,6 @@ export interface IMaterialManager
addRowAnimate(
source: SizedCanvasImageSource,
map: ArrayLike<IBlockIdentifier>,
frames: number,
height: number
): Iterable<IMaterialData>;

View File

@ -5,7 +5,7 @@ import { MotaOffscreenCanvas2D, SizedCanvasImageSource } from '@motajs/render';
// 得出结论ImageBitmap和Canvas的绘制性能不如Image于是直接画Image就行所以缓存基本上就是存Image
type ImageMapKeys = Exclude<Cls, 'tileset' | 'autotile'>;
type ImageMap = Record<ImageMapKeys, HTMLImageElement>;
type ImageMap = Record<ImageMapKeys, ImageBitmap>;
const i = (img: ImageMapKeys) => {
return core.material.images[img];
@ -21,10 +21,10 @@ interface AutotileCache {
type AutotileCaches = Record<AllNumbersOf<'autotile'>, AutotileCache>;
interface TextureRequire {
tileset: Record<string, HTMLImageElement>;
material: Record<ImageMapKeys, HTMLImageElement>;
tileset: Record<string, ImageBitmap>;
material: Record<ImageMapKeys, ImageBitmap>;
autotile: AutotileCaches;
images: Record<ImageIds, HTMLImageElement>;
images: Record<ImageIds, ImageBitmap>;
}
interface RenderableDataBase {
@ -49,10 +49,10 @@ export interface AutotileRenderable extends RenderableDataBase {
}
class TextureCache {
tileset!: Record<string, HTMLImageElement>;
material: Record<ImageMapKeys, HTMLImageElement>;
tileset!: Record<string, ImageBitmap>;
material: Record<ImageMapKeys, ImageBitmap>;
autotile!: AutotileCaches;
images!: Record<ImageIds, HTMLImageElement>;
images!: Record<ImageIds, ImageBitmap>;
idNumberMap!: IdToNumber;
@ -76,7 +76,7 @@ class TextureCache {
characterTurn2: Dir2[] = ['leftup', 'rightup', 'rightdown', 'leftdown'];
constructor() {
this.material = imageMap as Record<ImageMapKeys, HTMLImageElement>;
this.material = imageMap as Record<ImageMapKeys, ImageBitmap>;
}
init() {

View File

@ -11,6 +11,7 @@ import { GameTitleUI } from './ui/title';
import { createWeather } from './weather';
import { createMainExtension } from './commonIns';
import { createApp } from './renderer';
import { LoadSceneUI } from './ui/load';
export function createGameRenderer() {
const App = defineComponent(_props => {
@ -23,6 +24,9 @@ export function createGameRenderer() {
mainRenderer.hide();
createApp(App).mount(mainRenderer);
sceneController.open(LoadSceneUI, {});
mainRenderer.show();
}
export function createRender() {
@ -31,11 +35,6 @@ export function createRender() {
createAction();
createWeather();
loading.once('loaded', () => {
sceneController.open(GameTitleUI, {});
mainRenderer.show();
});
loading.once('assetBuilt', () => {
createMainExtension();
});

View File

@ -54,6 +54,8 @@ export const MOVING_TOLERANCE = 60;
/** 开关门动画的动画时长 */
export const DOOR_ANIMATE_INTERVAL = 50;
//#endregion
//#region 状态栏
/** 状态栏像素宽度 */
@ -69,6 +71,8 @@ export const STATUS_BAR_COUNT = ENABLE_RIGHT_STATUS_BAR ? 2 : 1;
/** 状态栏宽度的一半 */
export const HALF_STATUS_WIDTH = STATUS_BAR_WIDTH / 2;
//#endregion
//#region 游戏画面
/** 游戏画面像素宽度,宽=地图宽度+状态栏宽度*状态栏数量 */
@ -91,6 +95,8 @@ export const CENTER_LOC: ElementLocator = [
0.5
];
//#endregion
//#region 通用配置
/** 弹框的宽度,使用在内置 UI 与组件中,包括确认框、选择框、等待框等 */
@ -98,6 +104,31 @@ export const POP_BOX_WIDTH = MAP_WIDTH / 2;
/** 默认字体 */
export const DEFAULT_FONT = new Font('Verdana', 16);
//#endregion
//#region 加载界面
/** 加载界面的任务进度条半径 */
export const LOAD_TASK_RADIUS = Math.min(MAIN_WIDTH, MAIN_HEIGHT) / 6;
/** 加载界面的字节进度条纵轴位置 */
export const LOAD_BYTE_HEIGHT = MAIN_HEIGHT / 2 + MAIN_HEIGHT / 4;
/** 加载界面任务进度条的纵轴位置 */
export const LOAD_TASK_CENTER_HEIGHT = MAIN_HEIGHT / 2 - MAIN_HEIGHT / 8;
/** 加载界面字节进度条的长度 */
export const LOAD_BYTE_LENGTH = MAIN_WIDTH - MAIN_WIDTH / 12;
/** 加载界面任务进度条的粗细 */
export const LOAD_TASK_LINE_WIDTH = 6;
/** 加载界面字节进度条的粗细 */
export const LOAD_BYTE_LINE_WIDTH = 6;
/** 已加载部分进度条的颜色 */
export const LOAD_LOADED_COLOR = '#57ff78';
/** 未加载部分进度条的颜色 */
export const LOAD_UNLOADED_COLOR = '#ccc';
/** 加载界面的文字颜色 */
export const LOAD_FONT_COLOR = '#fff';
//#endregion
//#region 存档界面
/** 存档缩略图尺寸 */
@ -115,8 +146,13 @@ export const SAVE_DOWN_PAD = 30;
/** 存档页码数,调高并不会影响性能,但是如果玩家存档太多的话会导致存档体积很大 */
export const SAVE_PAGES = 1000;
//#endregion
//#region 标题界面
/** 标题图 */
export const TITLE_BACKGROUND_IMAGE = 'bg.jpg';
/** 标题文字中心横坐标 */
export const TITLE_X = HALF_WIDTH;
/** 标题文字中心纵坐标 */
@ -136,3 +172,5 @@ export const BUTTONS_HEIGHT = 200;
export const BUTTONS_X = HALF_WIDTH;
/** 标题界面按钮左上角纵坐标 */
export const BUTTONS_Y = MAIN_HEIGHT - BUTTONS_HEIGHT;
//#endregion

View 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);

View File

@ -1,7 +1,7 @@
import { GameUI, SetupComponentOptions } from '@motajs/system';
import { computed, ComputedRef, defineComponent, shallowReactive } from 'vue';
import { TextContent } from '../components';
import { ElementLocator, Font, SizedCanvasImageSource } from '@motajs/render';
import { ElementLocator, Font, ITexture } from '@motajs/render';
import { MixedToolbar, ReplayingStatus } from './toolbar';
import { openViewMap } from './viewmap';
import { mainUIController } from './controller';
@ -12,6 +12,7 @@ import {
STATUS_BAR_WIDTH
} from '../shared';
import { DefaultProps } from '@motajs/render-vue';
import { materials } from '@user/client-base';
export interface ILeftHeroStatus {
/** 楼层 id */
@ -69,27 +70,27 @@ export interface IRightHeroStatus {
interface StatusInfo {
/** 图标 */
icon: SizedCanvasImageSource;
readonly icon: ITexture | null;
/** 属性值,经过格式化 */
value: ComputedRef<string>;
readonly value: ComputedRef<string>;
/** 字体 */
font: Font;
readonly font: Font;
/** 文字颜色 */
color: CanvasStyle;
readonly color: CanvasStyle;
}
interface KeyLikeItem {
/** 属性值,经过格式化 */
value: ComputedRef<string>;
readonly value: ComputedRef<string>;
/** 字体 */
font: Font;
readonly font: Font;
/** 文字颜色 */
color: CanvasStyle;
readonly color: CanvasStyle;
}
interface KeyLikeInfo {
/** 这一行包含的内容 */
items: KeyLikeItem[];
readonly items: KeyLikeItem[];
}
interface StatusBarProps<T> extends DefaultProps {
@ -115,15 +116,15 @@ export const LeftStatusBar = defineComponent<StatusBarProps<ILeftHeroStatus>>(
/** 状态属性的开始纵坐标 */
const STATUS_Y = TITLE_HEIGHT + STATUS_PAD;
// 可以换成 core.material.images.images['xxx.png'] 来使用全塔属性注册的图片
const hpIcon = core.statusBar.icons.hp;
const atkIcon = core.statusBar.icons.atk;
const defIcon = core.statusBar.icons.def;
const mdefIcon = core.statusBar.icons.mdef;
const moneyIcon = core.statusBar.icons.money;
const expIcon = core.statusBar.icons.exp;
const manaIcon = core.statusBar.icons.mana;
const lvIcon = core.statusBar.icons.lv;
// 可以换成 materials.getImageByAlias('xxx.png') 来使用全塔属性注册的图片
const hpIcon = materials.getImageByAlias('icon-hp');
const atkIcon = materials.getImageByAlias('icon-atk');
const defIcon = materials.getImageByAlias('icon-def');
const mdefIcon = materials.getImageByAlias('icon-mdef');
const moneyIcon = materials.getImageByAlias('icon-money');
const expIcon = materials.getImageByAlias('icon-exp');
const manaIcon = materials.getImageByAlias('icon-mana');
const lvIcon = materials.getImageByAlias('icon-lv');
const s = p.status;

View File

@ -14,6 +14,7 @@ import {
HALF_WIDTH,
MAIN_HEIGHT,
MAIN_WIDTH,
TITLE_BACKGROUND_IMAGE,
TITLE_FILL,
TITLE_STROKE,
TITLE_STROKE_WIDTH,
@ -34,6 +35,7 @@ import { MainSceneUI } from './main';
import { adjustCover } from '../utils';
import { cosh, CurveMode, linear } from '@motajs/animate';
import { sleep } from '@motajs/common';
import { materials } from '@user/client-base';
const enum TitleButton {
StartGame,
@ -62,12 +64,12 @@ const gameTitleProps = {
} satisfies SetupComponentOptions<GameTitleProps>;
export const GameTitle = defineComponent<GameTitleProps>(props => {
const bg = core.material.images.images['bg.jpg'];
const bg = materials.getImageByAlias(TITLE_BACKGROUND_IMAGE);
//#region 计算背景图
const [width, height] = adjustCover(
bg.width,
bg.height,
bg?.width ?? MAIN_WIDTH,
bg?.height ?? MAIN_HEIGHT,
MAIN_WIDTH,
MAIN_HEIGHT
);

View File

@ -25,6 +25,7 @@ import { MAIN_HEIGHT, FULL_LOC, POP_BOX_WIDTH, CENTER_LOC } from '../shared';
import { openReplay, openSettings } from './settings';
import { openViewMap } from './viewmap';
import { DefaultProps } from '@motajs/render-vue';
import { materials } from '@user/client-base';
interface ToolbarProps extends DefaultProps {
loc?: ElementLocator;
@ -73,15 +74,15 @@ export const PlayingToolbar = defineComponent<
ToolbarEmits,
keyof ToolbarEmits
>((props, { emit }) => {
const bookIcon = core.statusBar.icons.book;
const flyIcon = core.statusBar.icons.fly;
const toolIcon = core.statusBar.icons.toolbox;
const equipIcon = core.statusBar.icons.equipbox;
const keyIcon = core.statusBar.icons.keyboard;
const shopIcon = core.statusBar.icons.shop;
const saveIcon = core.statusBar.icons.save;
const loadIcon = core.statusBar.icons.load;
const setIcon = core.statusBar.icons.settings;
const bookIcon = materials.getImageByAlias('icon-book');
const flyIcon = materials.getImageByAlias('icon-fly');
const toolIcon = materials.getImageByAlias('icon-toolbox');
const equipIcon = materials.getImageByAlias('icon-equipbox');
const keyIcon = materials.getImageByAlias('icon-keyboard');
const shopIcon = materials.getImageByAlias('icon-shop');
const saveIcon = materials.getImageByAlias('icon-save');
const loadIcon = materials.getImageByAlias('icon-load');
const setIcon = materials.getImageByAlias('icon-settings');
const iconFont = new Font('Verdana', 12);
@ -170,8 +171,8 @@ const replayingProps = {
export const ReplayingToolbar = defineComponent<ReplayingProps>(props => {
const status = props.status;
const bookIcon = core.statusBar.icons.book;
const saveIcon = core.statusBar.icons.save;
const bookIcon = materials.getImageByAlias('icon-book');
const saveIcon = materials.getImageByAlias('icon-save');
const font1 = Font.defaults({ size: 16 });
const font2 = new Font('Verdana', 12);

View File

@ -4,7 +4,7 @@ import { clamp } from 'lodash-es';
export class SunWeather extends Weather<CustomRenderItem> {
/** 阳光图片 */
private image: HTMLImageElement | null = null;
private image: ImageBitmap | null = null;
/** 阳光图片的不透明度 */
private alpha: number = 0;
/** 阳光的最大不透明度 */

View File

@ -49,7 +49,7 @@ class GameLoading extends EventEmitter<GameLoadEvent> {
* @param autotiles
*/
onAutotileLoaded(
autotiles: Partial<Record<AllIdsOf<'autotile'>, HTMLImageElement>>
autotiles: Partial<Record<AllIdsOf<'autotile'>, ImageBitmap>>
) {
if (this.autotileListened) return;
this.autotileListened = true;

View File

@ -3,8 +3,7 @@
export function initUI() {
if (main.mode === 'editor') return;
if (!main.replayChecking) {
const { mainUi, fixedUi, mainSetting } =
Mota.require('@motajs/legacy-ui');
const { mainUi } = Mota.require('@motajs/legacy-ui');
ui.prototype.drawBook = function () {
if (!core.isReplaying()) return mainUi.open('book');
@ -25,11 +24,6 @@ export function initUI() {
control.prototype.showStatusBar = function () {
if (main.mode === 'editor') return;
core.removeFlag('hideStatusBar');
if (mainSetting.getValue('ui.tips')) {
if (!fixedUi.hasName('tips')) {
fixedUi.open('tips');
}
}
core.updateStatusBar();
};
@ -39,8 +33,6 @@ export function initUI() {
// 如果原本就是隐藏的,则先显示
if (!core.domStyle.showStatusBar) this.showStatusBar();
if (core.isReplaying()) showToolbox = true;
fixedUi.closeByName('tips');
core.setFlag('hideStatusBar', true);
core.setFlag('showToolbox', showToolbox || null);
core.updateStatusBar();

View File

@ -1,9 +1,11 @@
import { logger } from '@motajs/common';
import { IAudioVolumeEffect, IMotaAudioContext } from './types';
import { IAudioVolumeEffect, IMotaAudioContext, ISoundPlayer } from './types';
type LocationArray = [number, number, number];
export class SoundPlayer<T extends string = SoundIds> {
export class SoundPlayer<
T extends string = SoundIds
> implements ISoundPlayer<T> {
/** 每个音效的唯一标识符 */
private num: number = 0;
@ -50,13 +52,17 @@ export class SoundPlayer<T extends string = SoundIds> {
* @param id
* @param data Uint8Array数据
*/
async add(id: T, data: Uint8Array) {
const buffer = await this.ac.decodeToAudioBuffer(data);
if (!buffer) {
logger.warn(51, id);
return;
async add(id: T, data: Uint8Array | AudioBuffer) {
if (data instanceof Uint8Array) {
const buffer = await this.ac.decodeToAudioBuffer(data);
if (!buffer) {
logger.warn(51, id);
return;
}
this.buffer.set(id, buffer);
} else {
this.buffer.set(id, data);
}
this.buffer.set(id, buffer);
}
/**

View File

@ -123,6 +123,12 @@ export class AudioStreamSource
this.controller = controller;
}
unpiped(controller: IStreamController): void {
if (this.controller === controller) {
this.controller = null;
}
}
async pump(data: Uint8Array | undefined, done: boolean): Promise<void> {
if (!data || this.errored) return;
if (!this.headerRecieved) {

View File

@ -611,7 +611,7 @@ export interface ISoundPlayer<T extends string> {
* @param id
* @param data Uint8Array数据
*/
add(id: T, data: Uint8Array): Promise<void>;
add(id: T, data: Uint8Array | AudioBuffer): Promise<void>;
/**
*

View File

@ -149,6 +149,7 @@
"92": "Followers can only be added when the last follower is not moving.",
"93": "Followers can only be removed when the last follower is not moving.",
"94": "Expecting an excitation binding when using '$1'",
"95": "Task adding is required before start loading.",
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency."
}
}

View File

@ -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;
}
}

View File

@ -1,5 +1,3 @@
export * from './patch';
export * from './disposable';
export * from './eventEmitter';
export * from './resource';
export * from './utils';

View File

@ -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>;
/**
* URIURL
*/
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);
}
})
);
});
});
}

View File

@ -4,7 +4,7 @@ import { isMobile } from '../use';
import { MotaSetting } from '../setting';
import { triggerFullscreen } from '../utils';
import settingsText from '../data/settings.json';
import { fixedUi, mainUi } from './uiIns';
import { mainUi } from './uiIns';
import { mainSetting } from './settingIns';
//#region legacy-ui
@ -13,8 +13,6 @@ export function createUI() {
const { hook } = Mota.require('@user/data-base');
hook.once('mounted', () => {
const ui = document.getElementById('ui-main')!;
const fixed = document.getElementById('ui-fixed')!;
const blur = mainSetting.getSetting('screen.blur');
mainUi.on('start', () => {
@ -34,12 +32,6 @@ export function createUI() {
core.closePanel();
}
});
fixedUi.on('start', () => {
fixed.style.display = 'block';
});
fixedUi.on('end', () => {
fixed.style.display = 'none';
});
});
}
@ -123,21 +115,11 @@ function handleAudioSetting<T extends number | boolean>(
}
}
function handleUiSetting<T extends number | boolean>(key: string, n: T, _o: T) {
if (key === 'danmaku') {
if (n) {
fixedUi.open('danmaku');
} else {
fixedUi.closeByName('danmaku');
}
} else if (key === 'tips') {
if (n && core.isPlaying()) {
fixedUi.open('tips');
} else {
fixedUi.closeByName('tips');
}
}
}
function handleUiSetting<T extends number | boolean>(
_key: string,
_n: T,
_o: T
) {}
// ----- 游戏的所有设置项
mainSetting

View File

@ -14,7 +14,3 @@ mainUi.register(
new GameUi('virtualKey', VirtualKey)
);
mainUi.showAll();
export const fixedUi = new UiController(true);
fixedUi.register(new GameUi('load', UI.Load));
fixedUi.showAll();

View File

@ -6,4 +6,3 @@ export { default as Settings } from './settings.vue';
export { default as Shop } from './shop.vue';
export { default as Toolbox } from './toolbox.vue';
export { default as Hotkey } from './hotkey.vue';
export { default as Load } from './load.vue';

View File

@ -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>

View File

@ -1 +1,4 @@
export * from './progress';
export * from './stream';
export * from './task';
export * from './types';

View 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);
}
}

View File

@ -1,76 +1,15 @@
import { logger } from '@motajs/common';
import EventEmitter from 'eventemitter3';
import { IStreamLoader, IStreamReader } from './types';
export interface IStreamController<T = void> {
readonly loading: boolean;
/**
*
*/
start(): Promise<T>;
/**
*
* @param reason
*/
cancel(reason?: string): void;
}
export interface IStreamReader<T = any> {
/**
*
* @param data
* @param done
*/
pump(
data: Uint8Array | undefined,
done: boolean,
response: Response
): Promise<void>;
/**
*
* @param controller
*/
piped(controller: IStreamController<T>): void;
/**
*
* @param stream
* @param controller
*/
start(
stream: ReadableStream,
controller: IStreamController<T>,
response: Response
): Promise<void>;
/**
*
* @param done false
* @param reason
*/
end(done: boolean, reason?: string): void;
}
interface StreamLoaderEvent {
data: [data: Uint8Array | undefined, done: boolean];
}
export class StreamLoader
extends EventEmitter<StreamLoaderEvent>
implements IStreamController<void>
{
export class StreamLoader implements IStreamLoader {
/** 传输目标 */
private target: Set<IStreamReader> = new Set();
/** 读取流对象 */
private stream?: ReadableStream;
private stream: ReadableStream | null = null;
loading: boolean = false;
constructor(public readonly url: string) {
super();
}
constructor(public readonly url: string) {}
/**
*
@ -83,7 +22,14 @@ export class StreamLoader
}
this.target.add(reader);
reader.piped(this);
return this;
}
unpipe(reader: IStreamReader): void {
if (this.loading) {
logger.warn(46);
return;
}
this.target.delete(reader);
}
async start() {

158
packages/loader/src/task.ts Normal file
View 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;
}
}

View 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

View File

@ -16,8 +16,10 @@ import {
LineParams,
QuadParams,
RectRCircleParams,
RectREllipseParams
RectREllipseParams,
ITexture
} from '@motajs/render';
import { Ref } from 'vue';
export interface BaseProps {
/** 元素的横坐标 */
@ -81,6 +83,8 @@ export interface BaseProps {
export interface CustomProps extends BaseProps {
/** 自定义的渲染函数 */
render?: CustomRenderFunction;
/** 更新绑定,当数组中的任意一项更新时将会自动更新此元素的渲染 */
bindings?: Ref<any>[];
}
export interface ContainerProps extends BaseProps {}
@ -111,7 +115,7 @@ export interface TextProps extends BaseProps {
export interface ImageProps extends BaseProps {
/** 图片对象 */
image: CanvasImageSource;
image?: ITexture | null;
}
export interface CommentProps extends BaseProps {

View File

@ -9,13 +9,15 @@ import {
Image,
IRenderItem,
IRenderTreeRoot,
ITexture,
Line,
Path,
QuadraticCurve,
Rect,
RectR,
Shader,
Text
Text,
Texture
} from '@motajs/render';
import { IRenderTagInfo, IRenderTagManager, TagCreateFunction } from './types';
import { logger } from '@motajs/common';
@ -24,13 +26,13 @@ export class RenderTagManager implements IRenderTagManager {
/** 标签注册映射 */
private readonly tagRegistry: Map<string, IRenderTagInfo> = new Map();
/** 空图片 */
private readonly emptyImg: HTMLCanvasElement;
private readonly emptyImg: ITexture;
constructor(readonly renderer: IRenderTreeRoot) {
const emptyImage = document.createElement('canvas');
emptyImage.width = 1;
emptyImage.height = 1;
this.emptyImg = emptyImage;
this.emptyImg = new Texture(emptyImage);
this.resgiterIntrinsicTags();
}
@ -52,17 +54,7 @@ export class RenderTagManager implements IRenderTagManager {
const { text = '', nocache = true, cache = false } = props;
return this.renderer.createElement(Text, text, cache && !nocache);
});
this.registerTag('image', props => {
if (!props) {
return this.renderer.createElement(Image, this.emptyImg, false);
}
const {
image = this.emptyImg,
nocache = true,
cache = false
} = props;
return this.renderer.createElement(Image, image, cache && !nocache);
});
this.registerTag('image', this.createStandardElement(false, Image));
this.registerTag('shader', this.createNoParamElement(Shader));
this.registerTag('comment', props => {
if (!props) return this.renderer.createElement(Comment);

View File

@ -2,9 +2,9 @@ import { isNil } from 'lodash-es';
import { ITexture, ITextureStore } from './types';
import { logger } from '@motajs/common';
export class TextureStore<T extends ITexture = ITexture>
implements ITextureStore<T>
{
export class TextureStore<
T extends ITexture = ITexture
> implements ITextureStore<T> {
private readonly texMap: Map<number, T> = new Map();
private readonly invMap: Map<T, number> = new Map();
private readonly aliasMap: Map<string, number> = new Map();

View File

@ -2,6 +2,7 @@ import { RenderItem, Transform, MotaOffscreenCanvas2D } from '.';
import { CanvasStyle } from '../types';
import { Font } from '../style';
import { IRenderImage, IRenderText } from './types';
import { ITexture } from '../assets';
/** 文字的安全填充,会填充在文字的上侧和下侧,防止削顶和削底 */
const SAFE_PAD = 1;
@ -147,31 +148,31 @@ export class Text extends RenderItem implements IRenderText {
}
export class Image extends RenderItem implements IRenderImage {
image: CanvasImageSource;
image: ITexture | null;
constructor(image: CanvasImageSource, enableCache: boolean = false) {
constructor(enableCache: boolean = false) {
super(enableCache);
this.image = image;
if (image instanceof VideoFrame || image instanceof SVGElement) {
this.size(200, 200);
} else {
this.size(image.width, image.height);
}
this.image = null;
}
protected render(
canvas: MotaOffscreenCanvas2D,
_transform: Transform
): void {
if (!this.image) return;
const ctx = canvas.ctx;
ctx.drawImage(this.image, 0, 0, this.width, this.height);
const {
source,
rect: { x, y, w, h }
} = this.image.render();
ctx.drawImage(source, x, y, w, h, 0, 0, this.width, this.height);
}
/**
*
* @param image
*/
setImage(image: CanvasImageSource) {
setImage(image: ITexture | null) {
this.image = image;
this.update();
}
@ -183,7 +184,8 @@ export class Image extends RenderItem implements IRenderImage {
): boolean {
switch (key) {
case 'image':
this.setImage(nextValue);
if (!nextValue) this.setImage(null);
else this.setImage(nextValue);
return true;
}
return false;

View File

@ -12,6 +12,7 @@ import { DefineComponent, DefineSetupFnComponent } from 'vue';
import { JSX } from 'vue/jsx-runtime';
import EventEmitter from 'eventemitter3';
import { SizedCanvasImageSource } from '../types';
import { ITexture } from '../assets';
//#region 功能类型
@ -572,13 +573,13 @@ export interface IRenderText extends IRenderItem {
export interface IRenderImage extends IRenderItem {
/** 当前元素的图片内容 */
readonly image: CanvasImageSource;
readonly image: ITexture | null;
/**
*
* @param image
*/
setImage(image: CanvasImageSource): void;
setImage(image: ITexture): void;
}
//#endregion

View File

@ -122,8 +122,7 @@ var data_comment_c456ea59_6018_45ef_8bcc_211a24c627dc = {
"_range": "editor.mode.checkUnique(thiseval)",
"_directory": "./project/fonts/",
"_transform": (function (one) {
if (one.endsWith(".ttf")) return one.substring(0, one.lastIndexOf('.'));
return null;
return one;
}).toString(),
"_docs": "使用字体",
"_data": "在此存放所有可能使用的字体 \n 字体名不能使用中文,不能带空格或特殊字符"

View File

@ -2046,9 +2046,6 @@ control.prototype._doSL_load = function (id, callback) {
core.saves.autosave.now,
1
)[0];
if (!main.replayChecking) {
Mota.require('@motajs/legacy-ui').fixedUi.closeByName('start');
}
if (core.isPlaying() && !core.status.gameOver) {
core.control.autosave(0);
core.saves.autosave.now -= 1;
@ -2063,11 +2060,6 @@ control.prototype._doSL_load = function (id, callback) {
id == 'autoSave' ? id : 'save' + id,
null,
function (data) {
if (!main.replayChecking && data) {
Mota.require('@motajs/legacy-ui').fixedUi.closeByName(
'start'
);
}
if (id == 'autoSave' && data != null) {
core.saves.autosave.data = data;
if (!(core.saves.autosave.data instanceof Array)) {

View File

@ -297,18 +297,6 @@ core.prototype.init = async function (coreData, callback) {
core._afterLoadResources(callback);
});
}
} else {
if (main.renderLoaded)
Mota.require('@motajs/legacy-ui').fixedUi.open('load', {
callback
});
else {
Mota.require('@user/data-base').hook.once('renderLoaded', () => {
Mota.require('@motajs/legacy-ui').fixedUi.open('load', {
callback
});
});
}
}
};

View File

@ -64,8 +64,8 @@ icons.prototype.getTilesetOffset = function (id) {
for (var i in core.tilesets) {
var imgName = core.tilesets[i];
var img = core.material.images.tilesets[imgName];
var width = Math.floor(parseInt(img.getAttribute('_width')) / 32),
height = Math.floor(parseInt(img.getAttribute('_height')) / 32);
var width = Math.floor(img.width / 32),
height = Math.floor(img.height / 32);
if (id >= startOffset && id < startOffset + width * height) {
var x = (id - startOffset) % width,
y = Math.floor((id - startOffset) / width);

View File

@ -139,6 +139,7 @@ loader.prototype._loadMaterials_async = function (onprogress, onfinished) {
};
loader.prototype._loadMaterials_afterLoad = function () {
if (main.mode === 'play') return;
var images = core.splitImage(core.material.images['icons']);
for (var key in core.statusBar.icons) {
if (typeof core.statusBar.icons[key] == 'number') {
@ -602,11 +603,11 @@ loader.prototype.freeBgm = function (name) {
name = core.getMappedName(name);
if (!core.material.bgms[name]) return;
// 从cachedBgms中删除
core.musicStatus.cachedBgms = core.musicStatus.cachedBgms.filter(function (
t
) {
return t != name;
});
core.musicStatus.cachedBgms = core.musicStatus.cachedBgms.filter(
function (t) {
return t != name;
}
);
// 清掉缓存
core.material.bgms[name].removeAttribute('src');
core.material.bgms[name].load();

View File

@ -20,6 +20,7 @@ function main() {
this.dom = {
body: document.body,
// 这些是给编辑器留的
gameDraw: document.getElementById('game-draw'),
gameCanvas: document.getElementsByClassName('gameCanvas'),
inputDiv: document.getElementById('inputDiv'),
@ -64,6 +65,8 @@ function main() {
'icons'
];
// 这些也是给编辑器留的
this.canvas = {};
this.statusBar = {
image: {},
icons: {
@ -105,8 +108,8 @@ function main() {
btn8: 34
}
};
this.floors = {};
this.canvas = {};
this.__VERSION__ = '2.10.0';
this.__VERSION_CODE__ = 610;

View File

@ -9,8 +9,13 @@ import { transformAsync } from '@babel/core';
import archiver from 'archiver';
import { createWriteStream } from 'fs';
import { zip } from 'compressing';
import { RequiredData, RequiredIconsData, ResourceType } from './types';
import { splitResource, SplittedResource } from './build-resource';
import { RequiredData, RequiredIconsData } from './types';
import {
CompressedUsage,
LoadDataType,
splitResource,
SplittedResource
} from './build-resource';
import { formatSize } from './utils';
/** 打包调试 */
@ -292,9 +297,9 @@ async function getAllChars(client: RollupOutput[]) {
}
interface CompressedLoadListItem {
type: ResourceType;
name: string;
usage: string;
readonly readAs: LoadDataType;
readonly name: string;
readonly usage: CompressedUsage;
}
type CompressedLoadList = Record<string, CompressedLoadListItem[]>;
@ -309,7 +314,7 @@ function generateResourceJSON(resources: SplittedResource[]) {
const uri = `project/resource/${file.fileName}`;
file.content.forEach(content => {
const item: CompressedLoadListItem = {
type: content.type,
readAs: content.readAs,
name: content.name,
usage: content.usage
};
@ -468,7 +473,7 @@ async function buildGame() {
await Promise.all(
fonts.map(v => {
const fontmin = new Fontmin();
const src = resolve(tempDir, 'client/project/fonts', `${v}.ttf`);
const src = resolve(tempDir, 'client/project/fonts', v);
const dest = resolve(tempDir, 'fonts');
const plugin = Fontmin.glyph({
text: [...chars].join('')

View File

@ -1,20 +1,34 @@
import JSZip from 'jszip';
import {
RequiredData,
RequiredIconsData,
ResourceType,
ResourceUsage
} from './types';
import { RequiredData, RequiredIconsData } from './types';
import { Stats } from 'fs';
import { readdir, readFile, stat } from 'fs/promises';
import { resolve } from 'path';
import { fileHash } from './utils';
export const enum CompressedUsage {
// ---- 系统加载内容,不可更改
Font,
Image,
Sound,
Tileset,
Autotile,
Material,
Animate
}
export const enum LoadDataType {
ArrayBuffer,
Uint8Array,
Blob,
Text,
JSON
}
export interface ResourceInfo {
name: string;
type: ResourceType;
usage: ResourceUsage;
stats: Stats;
readonly name: string;
readonly readAs: LoadDataType;
readonly usage: CompressedUsage;
readonly stats: Stats;
}
export interface SplittedResource {
@ -26,35 +40,52 @@ export interface SplittedResource {
}
interface ResourceContent extends ResourceInfo {
content: string | Buffer | Uint8Array;
exceed: boolean;
readonly content: string | Buffer | Uint8Array;
readonly exceed: boolean;
}
interface ResourcePath {
name: string;
path: string;
usage: ResourceUsage;
readonly name: string;
readonly path: string;
readonly usage: CompressedUsage;
}
function getTypeByUsage(usage: ResourceUsage): ResourceType {
function getTypeByUsage(usage: CompressedUsage): LoadDataType {
switch (usage) {
case 'animate':
return 'text';
case 'autotile':
case 'image':
case 'tileset':
return 'image';
case 'sound':
return 'byte';
case 'font':
return 'buffer';
case 'material':
return 'material';
case CompressedUsage.Animate:
return LoadDataType.Text;
case CompressedUsage.Autotile:
case CompressedUsage.Image:
case CompressedUsage.Tileset:
case CompressedUsage.Material:
return LoadDataType.Blob;
case CompressedUsage.Font:
case CompressedUsage.Sound:
return LoadDataType.ArrayBuffer;
}
}
function readFileOfType(path: string, type: ResourceType) {
if (type === 'text') {
function getZipFolderByUsage(usage: CompressedUsage): string {
switch (usage) {
case CompressedUsage.Image:
return 'image';
case CompressedUsage.Tileset:
return 'tileset';
case CompressedUsage.Autotile:
return 'autotile';
case CompressedUsage.Material:
return 'material';
case CompressedUsage.Font:
return 'font';
case CompressedUsage.Sound:
return 'sound';
case CompressedUsage.Animate:
return 'animate';
}
}
function readFileOfType(path: string, type: LoadDataType) {
if (type === LoadDataType.Text) {
return readFile(path, 'utf-8');
} else {
return readFile(path);
@ -64,7 +95,7 @@ function readFileOfType(path: string, type: ResourceType) {
async function compressFiles(files: ResourceContent[]) {
const zip = new JSZip();
files.forEach(v => {
const dir = `${v.type}/${v.name}`;
const dir = `${getZipFolderByUsage(v.usage)}/${v.name}`;
zip.file(dir, v.content);
});
const buffer = await zip.generateAsync({
@ -107,37 +138,37 @@ export async function splitResource(
...animates.map<ResourcePath>(v => ({
name: `${v}.animate`,
path: resolve(base, 'project/animates', `${v}.animate`),
usage: 'animate'
usage: CompressedUsage.Animate
})),
...fonts.map<ResourcePath>(v => ({
name: `${v}.ttf`,
path: resolve(fontsDir, `${v}.ttf`),
usage: 'font'
name: v,
path: resolve(fontsDir, v),
usage: CompressedUsage.Font
})),
...images.map<ResourcePath>(v => ({
name: v,
path: resolve(base, 'project/images', v),
usage: 'image'
usage: CompressedUsage.Image
})),
...sounds.map<ResourcePath>(v => ({
name: v,
path: resolve(base, 'project/sounds', v),
usage: 'sound'
usage: CompressedUsage.Sound
})),
...tilesets.map<ResourcePath>(v => ({
name: v,
path: resolve(base, 'project/tilesets', v),
usage: 'tileset'
usage: CompressedUsage.Tileset
})),
...autotiles.map<ResourcePath>(v => ({
name: `${v}.png`,
path: resolve(base, 'project/autotiles', `${v}.png`),
usage: 'autotile'
usage: CompressedUsage.Autotile
})),
...materials.map<ResourcePath>(v => ({
name: v,
path: resolve(base, 'project/materials', v),
usage: 'material'
usage: CompressedUsage.Material
}))
];
@ -154,7 +185,7 @@ export async function splitResource(
const type = getTypeByUsage(usage);
const content = await readFileOfType(path, type);
const info: ResourceContent = {
type,
readAs: type,
name,
usage,
stats,

View File

@ -73,11 +73,11 @@ import fs from 'fs/promises';
'./src/types/source/data.d.ts',
`
${floorId}
${d.images.length > 0 ? imgs : 'type ImageIds = never\n'}
${d.animates.length > 0 ? anis : 'type AnimationIds = never\n'}
${d.sounds.length > 0 ? sounds : 'type SoundIds = never\n'}
${d.bgms.length > 0 ? bgms : 'type BgmIds = never\n'}
${d.fonts.length > 0 ? fonts : 'type FontIds = never\n'}
${d.images.length > 0 ? imgs : 'type ImageIds = string;\n'}
${d.animates.length > 0 ? anis : 'type AnimationIds = string;\n'}
${d.sounds.length > 0 ? sounds : 'type SoundIds = string;\n'}
${d.bgms.length > 0 ? bgms : 'type BgmIds = string;\n'}
${d.fonts.length > 0 ? fonts : 'type FontIds = string;\n'}
${names}
`,
'utf-8'

View File

@ -590,11 +590,11 @@ async function doDeclaration(type: string, data: string) {
'src/types/source/data.d.ts',
`
${floorId}
${d.images.length > 0 ? imgs : 'type ImageIds = never\n'}
${d.animates.length > 0 ? anis : 'type AnimationIds = never\n'}
${d.sounds.length > 0 ? sounds : 'type SoundIds = never\n'}
${d.bgms.length > 0 ? bgms : 'type BgmIds = never\n'}
${d.fonts.length > 0 ? fonts : 'type FontIds = never\n'}
${d.images.length > 0 ? imgs : 'type ImageIds = string;\n'}
${d.animates.length > 0 ? anis : 'type AnimationIds = string;\n'}
${d.sounds.length > 0 ? sounds : 'type SoundIds = string;\n'}
${d.bgms.length > 0 ? bgms : 'type BgmIds = string;\n'}
${d.fonts.length > 0 ? fonts : 'type FontIds = string;\n'}
${names}
`,
'utf-8'

View File

@ -16,21 +16,12 @@
</div>
</div>
</div>
<div id="ui-fixed">
<template v-for="ui of fixedUi.stack" :key="ui.num">
<component
:is="ui.ui.component"
v-bind="ui.vBind ?? {}"
v-on="ui.vOn ?? {}"
></component>
</template>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import { mainUi, fixedUi } from '@motajs/legacy-ui';
import { mainUi } from '@motajs/legacy-ui';
onMounted(() => {
const { hook } = Mota.require('@user/data-base');

View File

@ -76,33 +76,33 @@ type MaterialImages = {
/**
*
*/
[C in Exclude<Cls, 'tileset' | 'autotile'>]: HTMLImageElement;
[C in Exclude<Cls, 'tileset' | 'autotile'>]: ImageBitmap;
} & {
/**
*
*/
airwall: HTMLImageElement;
airwall: ImageBitmap;
/**
*
*/
autotile: Record<AllIdsOf<'autotile'>, HTMLImageElement>;
autotile: Record<AllIdsOf<'autotile'>, ImageBitmap>;
/**
*
*/
images: Record<ImageIds, HTMLImageElement>;
images: Record<ImageIds, ImageBitmap>;
/**
*
*/
tilesets: Record<string, HTMLImageElement>;
tilesets: Record<string, ImageBitmap>;
keyboard: HTMLImageElement;
keyboard: ImageBitmap;
hero: HTMLImageElement;
hero: ImageBitmap;
icons: HTMLImageElement;
icons: ImageBitmap;
};
interface Material {
@ -694,25 +694,6 @@ interface CoreValues {
floorChangeTime: number;
}
type CoreStatusBarElements = {
/**
* @deprecated 使\
*
*/
readonly icons: Record<string, HTMLImageElement>;
/**
* @deprecated 使\
*
*/
readonly image: Record<string, HTMLImageElement>;
} & {
/**
* @deprecated 使\
*/
readonly [key: string]: HTMLElement;
};
type Materials = [
'animates',
'enemys',
@ -1094,12 +1075,6 @@ interface Main extends MainData {
*/
readonly bgmRemoteRoot: string;
/**
* @deprecated 使\
*
*/
readonly canvas: Record<string, CanvasRenderingContext2D>;
/**
* core.floors
*/
@ -1141,11 +1116,6 @@ interface Main extends MainData {
*/
readonly supportBunch: boolean;
/**
*
*/
readonly statusBar: CoreStatusBarElements;
/**
*
*/

View File

@ -48,7 +48,7 @@ type SoundIds =
type BgmIds =
| 'bgm.opus'
type FontIds = never
type FontIds = string;
interface NameMap {
'确定': 'confirm.opus';