From 80a67d519783bf83505521c123a475b37fde9670 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Sun, 21 Apr 2024 13:37:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E7=9A=84=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/libs/core.js | 16 +- script/dev.ts | 17 +- src/core/common/resource.ts | 566 ++++++++++++++++++++++++++++++++++++ src/core/index.ts | 3 - src/core/loader/load.ts | 46 --- src/core/loader/resource.ts | 383 ------------------------ src/core/main/init/ui.ts | 13 +- src/game/system.ts | 16 +- src/plugin/utils.ts | 10 + src/types/core.d.ts | 4 + src/ui/load.vue | 96 +++++- src/ui/start.vue | 30 +- 12 files changed, 715 insertions(+), 485 deletions(-) create mode 100644 src/core/common/resource.ts delete mode 100644 src/core/loader/load.ts delete mode 100644 src/core/loader/resource.ts diff --git a/public/libs/core.js b/public/libs/core.js index fc31b0e..2b45e11 100644 --- a/public/libs/core.js +++ b/public/libs/core.js @@ -297,9 +297,19 @@ core.prototype.init = async function (coreData, callback) { } } - core.loader._load(function () { - core._afterLoadResources(callback); - }); + if (main.replayChecking || main.mode === 'editor') { + core.loader._load(function () { + core._afterLoadResources(callback); + }); + } else { + if (main.renderLoaded) + Mota.require('var', 'fixedUi').open('load', { callback }); + else { + Mota.require('var', 'hook').once('renderLoaded', () => { + Mota.require('var', 'fixedUi').open('load', { callback }); + }); + } + } }; core.prototype.initSync = function (coreData, callback) { diff --git a/script/dev.ts b/script/dev.ts index 5d89931..d54232b 100644 --- a/script/dev.ts +++ b/script/dev.ts @@ -316,6 +316,7 @@ async function writeMultiFiles(req: Request, res: Response) { } async function writeDevResource(data: string) { + return; try { const buf = Buffer.from(data, 'base64'); data = buf.toString('utf-8'); @@ -324,12 +325,14 @@ async function writeDevResource(data: string) { const icons = await fs.readFile('./public/project/icons.js', 'utf-8'); const iconData = JSON.parse(icons.split('\n').slice(1).join('')); res.push( - ...info.main.bgms.map((v: any) => `bgms.${v}`), - ...info.main.fonts.map((v: any) => `fonts.${v}.ttf`), - ...info.main.images.map((v: any) => `images.${v}`), - ...info.main.sounds.map((v: any) => `sounds.${v}`), - ...info.main.tilesets.map((v: any) => `tilesets.${v}`), - ...Object.keys(iconData.autotile).map(v => `autotiles.${v}.png`), + ...info.main.bgms.map((v: any) => `audio/${v}`), + ...info.main.fonts.map((v: any) => `buffer/project/fonts/${v}.ttf`), + ...info.main.images.map((v: any) => `image/project/images/${v}`), + ...info.main.sounds.map((v: any) => `buffer/${v}`), + ...info.main.tilesets.map((v: any) => `image/project/tilesets${v}`), + ...Object.keys(iconData.autotile).map( + v => `image/project/autotiles/${v}.png` + ), ...[ 'animates', 'cloud', @@ -343,7 +346,7 @@ async function writeDevResource(data: string) { 'npcs', 'sun', 'terrains' - ].map(v => `materials.${v}.png`) + ].map(v => `material/${v}.png`) ); const text = JSON.stringify(res, void 0, 4); await fs.writeFile('./src/data/resource-dev.json', text, 'utf-8'); diff --git a/src/core/common/resource.ts b/src/core/common/resource.ts new file mode 100644 index 0000000..c568029 --- /dev/null +++ b/src/core/common/resource.ts @@ -0,0 +1,566 @@ +import axios, { AxiosRequestConfig, ResponseType } from 'axios'; +import { Disposable } from './disposable'; +import { logger } from './logger'; +import JSZip from 'jszip'; +import { EmitableEvent, EventEmitter } from './eventEmitter'; + +type ProgressFn = (now: number, total: number) => void; + +interface ResourceType { + text: string; + buffer: ArrayBuffer; + image: HTMLImageElement; + material: HTMLImageElement; + audio: HTMLAudioElement; + json: any; + zip: JSZip; +} + +interface ResourceMap { + text: TextResource; + buffer: BufferResource; + image: ImageResource; + material: MaterialResource; + audio: AudioResource; + json: JSONResource; + zip: ZipResource; +} + +export abstract class Resource extends Disposable { + 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, `Resource with type of 'none' is loaded.`); + } + } + + /** + * 加载这个资源,需要被子类override + */ + abstract load(onProgress?: ProgressFn): Promise; + + /** + * 解析资源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 { + /** + * 创建一个图片资源 + * @param uri 图片资源的URI,格式为 image/file,例如 'image/project/images/hero.png' + */ + constructor(uri: string) { + super(uri, 'image'); + } + + load(onProgress?: ProgressFn): Promise { + const img = new Image(); + img.src = this.resolveURI(); + this.resource = img; + return new Promise(res => { + img.addEventListener('load', () => { + this.loaded = true; + img.setAttribute('_width', img.width.toString()); + img.setAttribute('_height', img.height.toString()); + res(img); + }); + }); + } + + resolveURI(): string { + return `/${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 `/project/materials/${findURL(this.uri)}`; + } +} + +export class TextResource extends Resource { + /** + * 创建一个文字资源 + * @param uri 文字资源的URI,格式为 text/file,例如 'text/myText.txt' + * 这样的话会加载塔根目录下的 myText.txt 文件 + */ + constructor(uri: string) { + super(uri, 'text'); + } + + load(onProgress?: ProgressFn): Promise { + return new Promise(res => { + createAxiosLoader( + this.resolveURI(), + 'text', + onProgress + ).then(value => { + this.resource = value.data; + this.loaded = true; + res(value.data); + }); + }); + } + + resolveURI(): string { + return `/${findURL(this.uri)}`; + } +} + +export class BufferResource extends Resource { + /** + * 创建一个二进制缓冲区资源 + * @param uri 资源的URI,格式为 buffer/file,例如 'buffer/myBuffer.mp3' + */ + constructor(uri: string) { + super(uri, 'buffer'); + } + + load(onProgress?: ProgressFn): Promise { + return new Promise(res => { + createAxiosLoader( + this.resolveURI(), + 'arraybuffer', + onProgress + ).then(value => { + this.resource = value.data; + this.loaded = true; + res(value.data); + }); + }); + } + + resolveURI(): string { + return `/${findURL(this.uri)}`; + } +} + +export class JSONResource extends Resource { + /** + * 创建一个JSON对象资源 + * @param uri 资源的URI,格式为 json/file,例如 'buffer/myJSON.json' + */ + constructor(uri: string) { + super(uri, 'json'); + } + + load(onProgress?: ProgressFn): Promise { + return new Promise(res => { + createAxiosLoader(this.resolveURI(), 'json', onProgress).then( + value => { + this.resource = value.data; + this.loaded = true; + res(value.data); + } + ); + }); + } + + resolveURI(): string { + return `/${findURL(this.uri)}`; + } +} + +export class AudioResource extends Resource { + /** + * 创建一个音乐资源 + * @param uri 音乐资源的URI,格式为 audio/file,例如 'audio/bgm.mp3' + * 注意这个资源类型为 bgm 等只在播放时才开始流式加载的音乐资源类型, + * 对于需要一次性加载完毕的需要使用 BufferResource 进行加载, + * 并可以通过 AudioPlayer 类进行解析播放 + */ + constructor(uri: string) { + super(uri, 'audio'); + } + + load(onProgress?: ProgressFn): Promise { + const audio = new Audio(); + audio.src = this.resolveURI(); + this.resource = audio; + return new Promise(res => { + this.loaded = true; + res(audio); + }); + } + + resolveURI(): string { + return `/project/bgms/${findURL(this.uri)}`; + } +} + +export class ZipResource extends Resource { + /** + * 创建一个zip压缩资源 + * @param uri 资源的URI,格式为 zip/file,例如 'zip/myZip.h5data' + * 注意后缀名不要是zip,不然有的浏览器会触发下载,而不是加载 + */ + constructor(uri: string) { + super(uri); + this.type = 'zip'; + } + + async load(onProgress?: ProgressFn): Promise { + const data = await new Promise(res => { + createAxiosLoader( + 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 `/${findURL(this.uri)}`; + } +} + +function createAxiosLoader( + url: string, + responseType: ResponseType, + onProgress?: (now: number, total: number) => void +) { + const config: AxiosRequestConfig = {}; + config.responseType = responseType; + if (onProgress) { + config.onDownloadProgress = e => { + onProgress(e.loaded, e.total ?? 0); + }; + } + return axios.get(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 +}; + +interface LoadEvent extends EmitableEvent { + progress: ( + type: keyof ResourceType, + uri: string, + now: number, + total: number + ) => void; + load: (resource: ResourceMap[T]) => void; + 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> { + static totalByte: number = 0; + static loadedByte: number = 0; + static totalTask: number = 0; + static loadedTask: number = 0; + static errorTask: number = 0; + + /** 所有的资源,包括没有添加到加载任务里面的 */ + static store: Map = new Map(); + static taskList: Set = new Set(); + static loadedTaskList: Set = new Set(); + + private static progress: TaskProgressFn; + private static caledTask: Set = 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 + */ + load(): Promise { + if (this.loadingStarted) { + logger.warn( + 2, + `Repeat load of resource '${this.resource.type}/${this.resource.uri}'` + ); + return new Promise(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(reason => { + LoadTask.errorTask++; + logger.error( + 2, + `Unexpected loading error in loading resource '${this.resource.type}/${this.resource.uri}'. Error info: ${reason}` + ); + }); + this.emit('loadStart', this.resource); + load.then(() => { + // @ts-ignore + LoadTask.loadedTaskList.add(this); + this.loaded = totalByte; + LoadTask.loadedTask++; + this.emit('load', this.resource); + }); + return load; + } + + /** + * 新建一个加载任务,同时将任务加入加载列表 + * @param type 任务类型 + * @param uri 加载内容的URI + */ + static add( + type: T, + uri: string + ): LoadTask { + const task = new LoadTask(type, uri); + // @ts-ignore + 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('buffer', `buffer/project/sounds/${v}`); + Mota.r(() => { + res.once('load', res => { + Mota.require('var', 'sound').add(`sounds.${v}`, res.resource!); + }); + }); + }); + // tilseset + 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, 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('var', 'loading'); + 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-ignore + core.material.images[ + v.slice(0, -4) as SelectKey< + MaterialImages, + HTMLImageElement + > + ] = res.resource; + }); + }); + const weathers: (keyof Weather)[] = ['fog', 'cloud', 'sun']; + weathers.forEach(v => { + const res = LoadTask.add('material', `material/${v}.png`); + res.once('load', res => { + // @ts-ignore + core.animateFrame.weather[v] = 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 function loadCompressedResource() {} diff --git a/src/core/index.ts b/src/core/index.ts index 2aa3880..ee9b341 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -57,7 +57,6 @@ import EnemyTarget from '@/panel/enemyTarget.vue'; import KeyboardPanel from '@/panel/keyboard.vue'; import { MCGenerator } from './main/layout'; import { ResourceController } from './loader/controller'; -import { readyAllResource } from './loader/load'; import { logger } from './common/logger'; // ----- 类注册 @@ -133,5 +132,3 @@ Mota.register('module', 'MCGenerator', MCGenerator); main.renderLoaded = true; Mota.require('var', 'hook').emit('renderLoaded'); - -readyAllResource(); diff --git a/src/core/loader/load.ts b/src/core/loader/load.ts deleted file mode 100644 index 9484a58..0000000 --- a/src/core/loader/load.ts +++ /dev/null @@ -1,46 +0,0 @@ -import resource from '@/data/resource.json'; -import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; -import { - Resource, - getTypeByResource, - zipResource, - resource as res -} from './resource'; - -const info = resource; - -/** - * 构建游戏包后的加载 - */ -export function readyAllResource() { - /* @__PURE__ */ if (main.RESOURCE_TYPE === 'dev') return readyDevResource(); - info.resource.forEach(v => { - const type = getTypeByResource(v); - if (type === 'zip') { - zipResource.set(v, new Resource(v, 'zip')); - } else { - res.set(v, new Resource(v, type)); - } - }); -} - -/** - * 开发时的加载 - */ -/* @__PURE__ */ async function readyDevResource() { - const loading = Mota.require('var', 'loading'); - const loadData = (await import('../../data/resource-dev.json')).default; - - loadData.forEach(v => { - const type = getTypeByResource(v); - if (type !== 'zip') { - res.set(v, new Resource(v, type)); - } - }); - res.forEach(v => v.active()); - loading.once('coreInit', () => { - const animates = new Resource('__all_animates__', 'text'); - res.set('__all_animates__', animates); - animates.active(); - }); -} diff --git a/src/core/loader/resource.ts b/src/core/loader/resource.ts deleted file mode 100644 index 98ca1af..0000000 --- a/src/core/loader/resource.ts +++ /dev/null @@ -1,383 +0,0 @@ -import axios, { AxiosResponse } from 'axios'; -import { Disposable } from '../common/disposable'; -import { ensureArray } from '@/plugin/utils'; -import { has } from '@/plugin/utils'; -import JSZip from 'jszip'; -import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; -import { bgm } from '../audio/bgm'; - -// todo: 应当用register去注册资源类型,然后进行分块处理 - -interface ResourceData { - image: HTMLImageElement; - arraybuffer: ArrayBuffer; - text: string; - json: any; - zip: ZippedResource; - bgm: HTMLAudioElement; -} - -export type ResourceType = keyof ResourceData; -export type NonZipResource = Exclude; - -const autotiles: Partial, HTMLImageElement>> = {}; - -export class Resource< - T extends ResourceType = ResourceType -> extends Disposable { - format: T; - request?: Promise< - AxiosResponse | '@imageLoaded' | '@bgmLoaded' - >; - loaded: boolean = false; - uri: string; - - type!: string; - name!: string; - ext!: string; - - /** 资源数据 */ - resource?: ResourceData[T]; - - constructor(resource: string, format: T) { - super(resource); - this.data = this.resolveUrl(resource); - - this.format = format; - this.uri = resource; - - this.once('active', () => this.load()); - this.once('load', v => this.onLoad(v)); - this.once('loadstart', v => this.onLoadStart(v)); - } - - protected onLoadStart(v?: ResourceData[T]) { - if (this.format === 'bgm') { - // bgm 单独处理,因为它可以边播放边加载 - bgm.add(this.uri, v!); - } - } - - protected onLoad(v: ResourceData[T]) { - const loading = Mota.require('var', 'loading'); - // 资源类型处理 - if (this.type === 'fonts') { - document.fonts.add(new FontFace(this.name, v as ArrayBuffer)); - } else if (this.type === 'sounds') { - Mota.require('var', 'sound').add(this.uri, v as ArrayBuffer); - } else if (this.type === 'images') { - const name = `${this.name}${this.ext}` as ImageIds; - loading.on( - 'coreLoaded', - () => { - core.material.images.images[name] = v as HTMLImageElement; - }, - { immediate: true } - ); - } else if (this.type === 'materials') { - const name = this.name as SelectKey< - MaterialImages, - HTMLImageElement - >; - - loading.on( - 'coreLoaded', - () => { - core.material.images[name] = v; - }, - { immediate: true } - ); - loading.addMaterialLoaded(); - } else if (this.type === 'autotiles') { - const name = this.name as AllIdsOf<'autotile'>; - autotiles[name] = v; - loading.addAutotileLoaded(); - loading.onAutotileLoaded(autotiles); - } else if (this.type === 'tilesets') { - const name = `${this.name}${this.ext}`; - loading.on( - 'coreLoaded', - () => { - core.material.images.tilesets[name] = v; - }, - { immediate: true } - ); - } - - // 资源加载类型处理 - if (this.format === 'zip') { - (this.resource as ZippedResource).once('ready', data => { - data.forEach((path, file) => { - const [base, name] = path.split(/(\/|\\)/); - const id = `${base}.${name}`; - const type = getTypeByResource(id) as NonZipResource; - const format = getZipFormatByType(type); - resource.set( - id, - new Resource(id, type).setData(file.async(format)) - ); - }); - }); - } else if (this.format === 'image') { - const img = v as HTMLImageElement; - img.setAttribute('_width', img.width.toString()); - img.setAttribute('_height', img.height.toString()); - } - - if (this.name === '__all_animates__') { - if (this.format !== 'text') { - throw new Error( - `Unexpected mismatch of '__all_animates__' response type.` + - ` Expected: text. Meet: ${this.format}` - ); - } - const data = (v as string).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); - }); - } - } - - /** - * 解析资源url - * @param resource 资源字符串 - * @returns 解析出的资源url - */ - protected resolveUrl(resource: string) { - if (resource === '__all_animates__') { - this.type = 'animates'; - this.name = '__all_animates__'; - this.ext = '.animate'; - - return `/all/__all_animates__?v=${ - main.version - }&id=${main.animates.join(',')}`; - } - const resolve = resource.split('.'); - const type = (this.type = resolve[0]); - const name = (this.name = resolve.slice(1, -1).join('.')); - const ext = (this.ext = '.' + resolve.at(-1)); - - const distBase = import.meta.env.BASE_URL; - - const base = main.RESOURCE_URL; - const indexes = main.RESOURCE_INDEX; - const symbol = main.RESOURCE_SYMBOL; - const t = main.RESOURCE_TYPE; - - if (t === 'dist') { - if (has(indexes[`${type}.*`])) { - const i = indexes[`${type}.*`]; - if (i !== 'dist') { - return `${base}${i}/${type}/${name}-${symbol}${ext}`; - } else { - return `${distBase}resource/${type}/${name}-${symbol}${ext}`; - } - } else { - const i = indexes[`${type}.${name}${ext}`]; - const index = has(i) ? i : '0'; - if (i !== 'dist') { - return `${base}${index}/${type}/${name}-${symbol}${ext}`; - } else { - return `${distBase}resource/${type}/${name}-${symbol}${ext}`; - } - } - } else if (t === 'gh' || t === 'local') { - return `${distBase}resource/${type}/${name}-${symbol}${ext}`; - } else { - return `${distBase}project/${type}/${name}${ext}`; - } - } - - /** - * 加载资源 - */ - protected load() { - if (this.loaded) { - throw new Error(`Cannot load one resource twice.`); - } - const data = this.data; - if (!data) { - throw new Error(`Unexpected null of url in loading resource.`); - } - if (this.format === 'image') { - this.request = new Promise(res => { - const img = new Image(); - img.src = data; - this.emit('loadstart', img); - img.addEventListener('load', () => { - this.resource = img; - this.loaded = true; - this.emit('load', img); - res('@imageLoaded'); - }); - }); - } else if (this.format === 'bgm') { - this.request = new Promise(res => { - const audio = new Audio(); - audio.src = data; - this.emit('loadstart', audio); - audio.addEventListener('load', () => { - this.resource = audio; - this.loaded = true; - this.emit('load', audio); - res('@bgmLoaded'); - }); - }); - } else if ( - this.format === 'json' || - this.format === 'text' || - this.format === 'arraybuffer' - ) { - this.emit('loadstart'); - this.request = axios - .get(data, { - responseType: this.format, - onDownloadProgress: e => { - this.emit('progress', e); - } - }) - .then(v => { - this.resource = v.data; - this.loaded = true; - this.emit('load', v.data); - return v; - }); - } else if (this.format === 'zip') { - this.emit('loadstart'); - this.request = axios - .get(data, { - responseType: 'arraybuffer', - onDownloadProgress: e => { - this.emit('progress', e); - } - }) - .then(v => { - this.resource = new ZippedResource(v.data); - this.loaded = true; - this.emit('load', this.resource); - return v; - }); - } - } - - /** - * 获取资源,如果还没加载会等待加载完毕再获取 - */ - async getData(): Promise { - if (!this.activated) return null; - if (this.loaded) return this.resource ?? null; - else { - if (!this.request) this.load(); - await this.request; - return this.resource ?? null; - } - } - - /** - * 设置资源数据,不再需要加载 - * @param data 数据 - */ - protected setData(data: ResourceData[T] | Promise) { - if (data instanceof Promise) { - data.then(v => { - this.loaded = true; - this.resource = v; - this.emit('load', v); - }); - } else { - this.loaded = true; - this.resource = data; - this.emit('load', data); - } - return this; - } -} - -interface ZippedEvent extends EmitableEvent { - ready: (data: JSZip) => void; -} - -export class ZippedResource extends EventEmitter { - zip: Promise; - data?: JSZip; - - constructor(buffer: ArrayBuffer) { - super(); - this.zip = JSZip.loadAsync(buffer).then(v => { - this.emit('ready', v); - this.data = v; - return v; - }); - } -} - -export class ResourceStore extends Map< - string, - Resource -> { - active(key: string[] | string) { - const keys = ensureArray(key); - keys.forEach(v => this.get(v)?.active()); - } - - dispose(key: string[] | string) { - const keys = ensureArray(key); - keys.forEach(v => this.get(v)?.dispose()); - } - - destroy(key: string[] | string) { - const keys = ensureArray(key); - keys.forEach(v => this.get(v)?.destroy()); - } - - push(data: [string, Resource][] | Record>): void { - if (data instanceof Array) { - for (const [key, res] of data) { - if (this.has(key)) { - console.warn(`Resource already exists: '${key}'.`); - } - this.set(key, res); - } - } else { - return this.push(Object.entries(data)); - } - } - - async getData( - key: string - ): Promise { - return this.get(key)?.getData() ?? null; - } - - getDataSync( - key: string - ): ResourceData[T] | null { - return this.get(key)?.resource ?? null; - } -} - -export function getTypeByResource(resource: string): ResourceType { - const type = resource.split('.')[0]; - - if (type === 'zip') return 'zip'; - else if (type === 'bgms') return 'bgm'; - else if (['images', 'autotiles', 'materials', 'tilesets'].includes(type)) { - return 'image'; - } else if (['sounds', 'fonts'].includes(type)) return 'arraybuffer'; - else if (type === 'animates') return 'json'; - - return 'arraybuffer'; -} - -export function getZipFormatByType(type: ResourceType): 'arraybuffer' | 'text' { - if (type === 'text' || type === 'json') return 'text'; - else return 'arraybuffer'; -} - -export const resource = new ResourceStore(); -export const zipResource = new ResourceStore(); diff --git a/src/core/main/init/ui.ts b/src/core/main/init/ui.ts index f2021ed..f3f9b49 100644 --- a/src/core/main/init/ui.ts +++ b/src/core/main/init/ui.ts @@ -32,7 +32,8 @@ fixedUi.register( new GameUi('chapter', UI.Chapter), new GameUi('completeAchi', UI.CompleteAchi), new GameUi('start', UI.Start), - new GameUi('toolbar', UI.Toolbar) + new GameUi('toolbar', UI.Toolbar), + new GameUi('load', UI.Load) ); fixedUi.showAll(); @@ -72,15 +73,5 @@ hook.once('mounted', () => { fixed.style.display = 'none'; }); - if (loaded && !mounted) { - fixedUi.open('start'); - } mounted = true; }); -hook.once('load', () => { - if (mounted) { - // todo: 暂时先这么搞,之后重写加载界面,需要改成先显示加载界面,加载完毕后再打开这个界面 - fixedUi.open('start'); - } - loaded = true; -}); diff --git a/src/game/system.ts b/src/game/system.ts index 759226b..9bb32b2 100644 --- a/src/game/system.ts +++ b/src/game/system.ts @@ -7,12 +7,6 @@ import type { IndexedEventEmitter } from '@/core/common/eventEmitter'; import type { loading } from './game'; -import type { - Resource, - ResourceStore, - ResourceType, - ZippedResource -} from '@/core/loader/resource'; import type { Hotkey } from '@/core/main/custom/hotkey'; import type { Keyboard } from '@/core/main/custom/keyboard'; import type { CustomToolbar } from '@/core/main/custom/toolbar'; @@ -38,9 +32,6 @@ interface ClassInterface { GameStorage: typeof GameStorage; MotaSetting: typeof MotaSetting; SettingDisplayer: typeof SettingDisplayer; - Resource: typeof Resource; - ZippedResource: typeof ZippedResource; - ResourceStore: typeof ResourceStore; Focus: typeof Focus; GameUi: typeof GameUi; UiController: typeof UiController; @@ -82,8 +73,6 @@ interface VariableInterface { // isMobile: boolean; bgm: BgmController; sound: SoundController; - resource: ResourceStore>; - zipResource: ResourceStore<'zip'>; settingStorage: GameStorage; status: Ref; // 定义于游戏进程,渲染进程依然可用 @@ -529,7 +518,8 @@ function r( fn: (this: T, packages: PackageInterface) => void, thisArg?: T ) { - if (!main.replayChecking) fn.call(thisArg as T, MPackage.requireAll()); + if (!main.replayChecking && main.mode === 'play') + fn.call(thisArg as T, MPackage.requireAll()); } /** @@ -548,7 +538,7 @@ function rf any, T>( thisArg?: T ): (this: T, ...params: Parameters) => ReturnType | undefined { // @ts-ignore - if (main.replayChecking) return () => {}; + if (main.replayChecking || main.mode === 'editor') return () => {}; else { return (...params) => { return fn.call(thisArg, ...params); diff --git a/src/plugin/utils.ts b/src/plugin/utils.ts index f66476b..ac29e19 100644 --- a/src/plugin/utils.ts +++ b/src/plugin/utils.ts @@ -386,3 +386,13 @@ export function getVitualKeyOnce( }); }); } + +export function formatSize(size: number) { + return size < 1 << 10 + ? `${size.toFixed(2)}B` + : size < 1 << 20 + ? `${(size / (1 << 10)).toFixed(2)}KB` + : size < 1 << 30 + ? `${(size / (1 << 20)).toFixed(2)}MB` + : `${(size / (1 << 30)).toFixed(2)}GB`; +} diff --git a/src/types/core.d.ts b/src/types/core.d.ts index ab1c906..e920eab 100644 --- a/src/types/core.d.ts +++ b/src/types/core.d.ts @@ -98,6 +98,8 @@ type MaterialImages = { */ tilesets: Record; + keyboard: HTMLImageElement; + hero: HTMLImageElement; }; @@ -1081,6 +1083,8 @@ interface Core extends Pick { _this: any, ...params: Parameters ): ReturnType; + + _afterLoadResources(callback?: () => void): void; } type CoreMixin = Core & diff --git a/src/ui/load.vue b/src/ui/load.vue index 4fac9bb..059a60e 100644 --- a/src/ui/load.vue +++ b/src/ui/load.vue @@ -1,13 +1,105 @@ - + diff --git a/src/ui/start.vue b/src/ui/start.vue index 5b5f40f..d6acb67 100644 --- a/src/ui/start.vue +++ b/src/ui/start.vue @@ -310,25 +310,21 @@ onMounted(async () => { start = document.getElementById('start') as HTMLDivElement; background = document.getElementById('background') as HTMLImageElement; - const loading = Mota.require('var', 'loading'); + window.addEventListener('resize', resize); + resize(); - loading.once('coreInit', async () => { - window.addEventListener('resize', resize); - resize(); + soundChecked.value = mainSetting.getValue('audio.bgmEnabled', true); + mainBgm.changeTo('title.mp3'); - soundChecked.value = mainSetting.getValue('audio.bgmEnabled', true); - mainBgm.changeTo('title.mp3'); - - start.style.opacity = '1'; - if (played) { - text.value = text2; - hard.splice(1, 0, '挑战'); - } - setButtonAnimate().then(() => (showed.value = true)); - await sleep(1000); - showCursor(); - await sleep(1200); - }); + start.style.opacity = '1'; + if (played) { + text.value = text2; + hard.splice(1, 0, '挑战'); + } + setButtonAnimate().then(() => (showed.value = true)); + await sleep(1000); + showCursor(); + await sleep(1200); CustomToolbar.closeAll(); });