mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-09-03 22:01:47 +08:00
refactor: 游戏打包
This commit is contained in:
parent
1169db5dfd
commit
7349583bf8
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": [["@babel/preset-env"]],
|
|
||||||
"sourceType": "script",
|
|
||||||
"minified": true,
|
|
||||||
"comments": false
|
|
||||||
}
|
|
7
components.d.ts
vendored
7
components.d.ts
vendored
@ -7,7 +7,6 @@ export {}
|
|||||||
|
|
||||||
declare module '@vue/runtime-core' {
|
declare module '@vue/runtime-core' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AButton: typeof import('ant-design-vue/es')['Button']
|
|
||||||
ADivider: typeof import('ant-design-vue/es')['Divider']
|
ADivider: typeof import('ant-design-vue/es')['Divider']
|
||||||
AInput: typeof import('ant-design-vue/es')['Input']
|
AInput: typeof import('ant-design-vue/es')['Input']
|
||||||
AProgress: typeof import('ant-design-vue/es')['Progress']
|
AProgress: typeof import('ant-design-vue/es')['Progress']
|
||||||
@ -15,11 +14,5 @@ declare module '@vue/runtime-core' {
|
|||||||
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
||||||
ASlider: typeof import('ant-design-vue/es')['Slider']
|
ASlider: typeof import('ant-design-vue/es')['Slider']
|
||||||
ASwitch: typeof import('ant-design-vue/es')['Switch']
|
ASwitch: typeof import('ant-design-vue/es')['Switch']
|
||||||
Box: typeof import('./src/components/box.vue')['default']
|
|
||||||
BoxAnimate: typeof import('./src/components/boxAnimate.vue')['default']
|
|
||||||
Colomn: typeof import('./src/components/colomn.vue')['default']
|
|
||||||
EnemyOne: typeof import('./src/components/enemyOne.vue')['default']
|
|
||||||
Minimap: typeof import('./src/components/minimap.vue')['default']
|
|
||||||
Scroll: typeof import('./src/components/scroll.vue')['default']
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
22
package.json
22
package.json
@ -32,7 +32,7 @@
|
|||||||
"mutate-animate": "^1.4.2",
|
"mutate-animate": "^1.4.2",
|
||||||
"ogg-opus-decoder": "^1.6.14",
|
"ogg-opus-decoder": "^1.6.14",
|
||||||
"opus-decoder": "^0.7.7",
|
"opus-decoder": "^0.7.7",
|
||||||
"vue": "^3.5.17"
|
"vue": "^3.5.20"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.26.4",
|
"@babel/cli": "^7.26.4",
|
||||||
@ -46,16 +46,18 @@
|
|||||||
"@rollup/plugin-replace": "^5.0.7",
|
"@rollup/plugin-replace": "^5.0.7",
|
||||||
"@rollup/plugin-terser": "^0.4.4",
|
"@rollup/plugin-terser": "^0.4.4",
|
||||||
"@rollup/plugin-typescript": "^11.1.6",
|
"@rollup/plugin-typescript": "^11.1.6",
|
||||||
|
"@types/archiver": "^6.0.3",
|
||||||
"@types/babel__core": "^7.20.5",
|
"@types/babel__core": "^7.20.5",
|
||||||
"@types/express": "^5.0.3",
|
"@types/express": "^5.0.3",
|
||||||
"@types/fontmin": "^0.9.5",
|
"@types/fontmin": "^0.9.5",
|
||||||
"@types/fs-extra": "^9.0.13",
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^18.19.80",
|
"@types/node": "^18.19.80",
|
||||||
"@types/ws": "^8.18.0",
|
"@types/ws": "^8.18.0",
|
||||||
"@vitejs/plugin-legacy": "^6.0.2",
|
"@vitejs/plugin-legacy": "^6.0.2",
|
||||||
"@vitejs/plugin-vue": "^5.2.3",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
"@vitejs/plugin-vue-jsx": "^5.1.1",
|
||||||
|
"archiver": "^7.0.1",
|
||||||
"chokidar": "^3.6.0",
|
"chokidar": "^3.6.0",
|
||||||
"compressing": "^1.10.1",
|
"compressing": "^1.10.1",
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.1.2",
|
||||||
@ -64,9 +66,9 @@
|
|||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-vue": "^9.33.0",
|
"eslint-plugin-vue": "^9.33.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"fontmin": "^0.9.9",
|
"fontmin": "^2.0.3",
|
||||||
"form-data": "^4.0.2",
|
"form-data": "^4.0.2",
|
||||||
"fs-extra": "^10.1.0",
|
"fs-extra": "^11.3.1",
|
||||||
"glob": "^11.0.1",
|
"glob": "^11.0.1",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"less": "^4.2.2",
|
"less": "^4.2.2",
|
||||||
@ -75,13 +77,13 @@
|
|||||||
"mermaid": "^11.5.0",
|
"mermaid": "^11.5.0",
|
||||||
"postcss-preset-env": "^9.6.0",
|
"postcss-preset-env": "^9.6.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"rollup": "^3.29.5",
|
"rollup": "^4.49.0",
|
||||||
"terser": "^5.39.0",
|
"terser": "^5.39.0",
|
||||||
"tsx": "^4.19.3",
|
"tsx": "^4.20.5",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.9.2",
|
||||||
"typescript-eslint": "^8.27.0",
|
"typescript-eslint": "^8.27.0",
|
||||||
"unplugin-vue-components": "^0.22.12",
|
"unplugin-vue-components": "^0.22.12",
|
||||||
"vite": "^6.2.2",
|
"vite": "^6.3.5",
|
||||||
"vite-plugin-dts": "^4.5.4",
|
"vite-plugin-dts": "^4.5.4",
|
||||||
"vitepress": "^1.6.3",
|
"vitepress": "^1.6.3",
|
||||||
"vitepress-plugin-mermaid": "^2.0.17",
|
"vitepress-plugin-mermaid": "^2.0.17",
|
||||||
|
@ -48,7 +48,7 @@ export interface IAudioDecodeError {
|
|||||||
|
|
||||||
export interface IAudioDecodeData {
|
export interface IAudioDecodeData {
|
||||||
/** 每个声道的音频信息 */
|
/** 每个声道的音频信息 */
|
||||||
channelData: Float32Array[];
|
channelData: Float32Array<ArrayBuffer>[];
|
||||||
/** 已经被解码的 PCM 采样数 */
|
/** 已经被解码的 PCM 采样数 */
|
||||||
samplesDecoded: number;
|
samplesDecoded: number;
|
||||||
/** 音频采样率 */
|
/** 音频采样率 */
|
||||||
@ -163,15 +163,15 @@ export class VorbisDecoder extends AudioDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async decode(data: Uint8Array): Promise<IAudioDecodeData | undefined> {
|
async decode(data: Uint8Array): Promise<IAudioDecodeData | undefined> {
|
||||||
return this.decoder?.decode(data);
|
return this.decoder?.decode(data) as Promise<IAudioDecodeData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async decodeAll(data: Uint8Array): Promise<IAudioDecodeData | undefined> {
|
async decodeAll(data: Uint8Array): Promise<IAudioDecodeData | undefined> {
|
||||||
return this.decoder?.decodeFile(data);
|
return this.decoder?.decodeFile(data) as Promise<IAudioDecodeData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async flush(): Promise<IAudioDecodeData | undefined> {
|
async flush(): Promise<IAudioDecodeData | undefined> {
|
||||||
return this.decoder?.flush();
|
return this.decoder?.flush() as Promise<IAudioDecodeData>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,7 +179,9 @@ export class OpusDecoder extends AudioDecoder {
|
|||||||
decoder?: OggOpusDecoderWebWorker;
|
decoder?: OggOpusDecoderWebWorker;
|
||||||
|
|
||||||
async create(): Promise<void> {
|
async create(): Promise<void> {
|
||||||
this.decoder = new OggOpusDecoderWebWorker();
|
this.decoder = new OggOpusDecoderWebWorker({
|
||||||
|
speechQualityEnhancement: 'none'
|
||||||
|
});
|
||||||
await this.decoder.ready;
|
await this.decoder.ready;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,14 +190,14 @@ export class OpusDecoder extends AudioDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async decode(data: Uint8Array): Promise<IAudioDecodeData | undefined> {
|
async decode(data: Uint8Array): Promise<IAudioDecodeData | undefined> {
|
||||||
return this.decoder?.decode(data);
|
return this.decoder?.decode(data) as Promise<IAudioDecodeData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async decodeAll(data: Uint8Array): Promise<IAudioDecodeData | undefined> {
|
async decodeAll(data: Uint8Array): Promise<IAudioDecodeData | undefined> {
|
||||||
return this.decoder?.decodeFile(data);
|
return this.decoder?.decodeFile(data) as Promise<IAudioDecodeData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async flush(): Promise<IAudioDecodeData | undefined> {
|
async flush(): Promise<IAudioDecodeData | undefined> {
|
||||||
return await this.decoder?.flush();
|
return this.decoder?.flush() as Promise<IAudioDecodeData>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ export class Image3DEffect
|
|||||||
const matrix = this.program.getMatrix('u_imageTransform');
|
const matrix = this.program.getMatrix('u_imageTransform');
|
||||||
if (!matrix) return;
|
if (!matrix) return;
|
||||||
const trans = this.proj.multiply(this.view).multiply(this.model);
|
const trans = this.proj.multiply(this.view).multiply(this.model);
|
||||||
matrix.set(false, trans.mat);
|
matrix.set(false, Array.from(trans.mat));
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,7 +174,7 @@ function onmove(e: MouseEvent) {
|
|||||||
mat4.rotateX(matrix, matrix, -(dy * 10 * Math.PI) / 180);
|
mat4.rotateX(matrix, matrix, -(dy * 10 * Math.PI) / 180);
|
||||||
mat4.rotateY(matrix, matrix, (dx * 10 * Math.PI) / 180);
|
mat4.rotateY(matrix, matrix, (dx * 10 * Math.PI) / 180);
|
||||||
|
|
||||||
const end = matrix.join(',');
|
const end = Array.from(matrix).join(',');
|
||||||
background.style.transform = `perspective(${
|
background.style.transform = `perspective(${
|
||||||
1000 * core.domStyle.scale
|
1000 * core.domStyle.scale
|
||||||
}px)matrix3d(${end})`;
|
}px)matrix3d(${end})`;
|
||||||
|
4282
pnpm-lock.yaml
4282
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
|||||||
///<reference path="../../types/declaration/core.d.ts" />
|
///<reference path="../../src/types/declaration/core.d.ts" />
|
||||||
|
|
||||||
/*
|
/*
|
||||||
actions.js:用户交互的事件的处理
|
actions.js:用户交互的事件的处理
|
||||||
|
@ -360,7 +360,7 @@ core.prototype._loadGameProcess = async function () {
|
|||||||
if (main.pluginUseCompress) {
|
if (main.pluginUseCompress) {
|
||||||
await main.loadScript(`project/processG.min.js`);
|
await main.loadScript(`project/processG.min.js`);
|
||||||
} else {
|
} else {
|
||||||
await main.loadScript(`esm?name=src/editor.ts`, true);
|
await main.loadScript(`esm?name=src/data.ts`, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
///<reference path="../types/declaration/core.d.ts" />
|
///<reference path="../src/types/declaration/core.d.ts" />
|
||||||
function main() {
|
function main() {
|
||||||
//------------------------ 用户修改内容 ------------------------//
|
//------------------------ 用户修改内容 ------------------------//
|
||||||
|
|
||||||
@ -130,18 +130,6 @@ main.prototype.loadScript = function (src, module) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
main.prototype.init = async function (mode, callback) {
|
main.prototype.init = async function (mode, callback) {
|
||||||
try {
|
|
||||||
const a = {};
|
|
||||||
const b = {};
|
|
||||||
new Proxy(a, b);
|
|
||||||
new Promise(res => res());
|
|
||||||
eval('`${0}`');
|
|
||||||
} catch {
|
|
||||||
alert('浏览器版本过低,无法游玩本塔!');
|
|
||||||
alert('建议使用Edge浏览器或Chrome浏览器游玩!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (main.replayChecking) {
|
if (main.replayChecking) {
|
||||||
main.loadSync(mode, callback);
|
main.loadSync(mode, callback);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,16 +1,31 @@
|
|||||||
import { build, loadConfigFromFile, mergeConfig, UserConfig } from 'vite';
|
import { build, loadConfigFromFile, mergeConfig, UserConfig } from 'vite';
|
||||||
import legacy from '@vitejs/plugin-legacy';
|
import legacy from '@vitejs/plugin-legacy';
|
||||||
import path from 'path';
|
import { resolve } from 'path';
|
||||||
import fs from 'fs-extra';
|
import { copy, emptyDir, ensureDir } from 'fs-extra';
|
||||||
|
import { OutputAsset, OutputChunk, RollupOutput } from 'rollup';
|
||||||
|
import Fontmin from 'fontmin';
|
||||||
|
import { readdir, readFile, rmdir, stat, writeFile } from 'fs/promises';
|
||||||
|
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 { sum } from 'lodash-es';
|
||||||
|
import { formatSize } from './utils';
|
||||||
|
|
||||||
const outputDir = path.resolve('./dist/game');
|
// 资源分离步骤的单包大小,默认 2M,可以自行调整
|
||||||
|
const RESOUCE_SIZE = 2 * 2 ** 20;
|
||||||
|
|
||||||
// 清空 dist/game 目录
|
const distDir = resolve(process.cwd(), 'dist');
|
||||||
fs.emptyDirSync(outputDir);
|
const tempDir = resolve(process.cwd(), '_temp');
|
||||||
|
|
||||||
// 构建游戏
|
/**
|
||||||
async function buildGame() {
|
* 构建游戏代码
|
||||||
const configFile = path.resolve('./vite.config.ts');
|
* @param entry 入口文件
|
||||||
|
*/
|
||||||
|
async function buildClient(outDir: string) {
|
||||||
|
const configFile = resolve(process.cwd(), 'vite.config.ts');
|
||||||
const config = await loadConfigFromFile(
|
const config = await loadConfigFromFile(
|
||||||
{ command: 'build', mode: 'production' },
|
{ command: 'build', mode: 'production' },
|
||||||
configFile
|
configFile
|
||||||
@ -26,14 +41,15 @@ async function buildGame() {
|
|||||||
'Opera >= 43'
|
'Opera >= 43'
|
||||||
],
|
],
|
||||||
polyfills: true,
|
polyfills: true,
|
||||||
modernPolyfills: true
|
modernPolyfills: true,
|
||||||
|
renderModernChunks: false
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
build: {
|
build: {
|
||||||
outDir: outputDir,
|
outDir,
|
||||||
sourcemap: true,
|
copyPublicDir: true,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: path.resolve('./src/main.ts'),
|
external: ['@wasm-audio-decoders/opus-ml'],
|
||||||
output: {
|
output: {
|
||||||
format: 'es',
|
format: 'es',
|
||||||
entryFileNames: '[name].[hash].js',
|
entryFileNames: '[name].[hash].js',
|
||||||
@ -47,7 +63,17 @@ async function buildGame() {
|
|||||||
'lz-string',
|
'lz-string',
|
||||||
'chart.js',
|
'chart.js',
|
||||||
'mutate-animate',
|
'mutate-animate',
|
||||||
'@vueuse/core'
|
'eventemitter3',
|
||||||
|
'gl-matrix',
|
||||||
|
'jszip',
|
||||||
|
'anon-tokyo',
|
||||||
|
'vue'
|
||||||
|
],
|
||||||
|
audio: [
|
||||||
|
'codec-parser',
|
||||||
|
'opus-decoder',
|
||||||
|
'ogg-opus-decoder',
|
||||||
|
'@wasm-audio-decoders/ogg-vorbis'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -55,11 +81,581 @@ async function buildGame() {
|
|||||||
}
|
}
|
||||||
} satisfies UserConfig);
|
} satisfies UserConfig);
|
||||||
|
|
||||||
await build(resolved);
|
return build({
|
||||||
console.log('✅ Game built successfully.');
|
...resolved,
|
||||||
|
configFile: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
buildGame().catch(e => {
|
async function buildData(outDir: string, entry: string) {
|
||||||
console.error(e);
|
const configFile = resolve(process.cwd(), 'vite.config.ts');
|
||||||
process.exit(1);
|
const config = await loadConfigFromFile(
|
||||||
});
|
{ command: 'build', mode: 'production' },
|
||||||
|
configFile
|
||||||
|
);
|
||||||
|
const resolved = mergeConfig(config?.config ?? {}, {
|
||||||
|
build: {
|
||||||
|
outDir,
|
||||||
|
copyPublicDir: false,
|
||||||
|
lib: {
|
||||||
|
entry,
|
||||||
|
name: 'ProcessData',
|
||||||
|
fileName: 'data',
|
||||||
|
formats: ['iife']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} satisfies UserConfig);
|
||||||
|
|
||||||
|
return build({
|
||||||
|
...resolved,
|
||||||
|
configFile: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const enum ProgressStatus {
|
||||||
|
Success,
|
||||||
|
Fail,
|
||||||
|
Working,
|
||||||
|
Warn
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输出构建步骤
|
||||||
|
* @param step 步骤数
|
||||||
|
* @param final 最后一步的状态
|
||||||
|
*/
|
||||||
|
function logProgress(step: number, final: ProgressStatus) {
|
||||||
|
const list = [
|
||||||
|
`1. 构建前准备`,
|
||||||
|
`2. 构建客户端代码`,
|
||||||
|
`3. 构建数据端代码`,
|
||||||
|
`4. 压缩 main.js`,
|
||||||
|
`5. 压缩字体`,
|
||||||
|
`6. 资源分块`,
|
||||||
|
`7. 最后处理`,
|
||||||
|
`8. 压缩为 zip`
|
||||||
|
];
|
||||||
|
|
||||||
|
const str = list.reduce((prev, curr, idx) => {
|
||||||
|
if (idx > step) {
|
||||||
|
return prev;
|
||||||
|
} else if (idx === step) {
|
||||||
|
switch (final) {
|
||||||
|
case ProgressStatus.Fail:
|
||||||
|
return prev + `❌ ${curr}\n错误信息:\n`;
|
||||||
|
case ProgressStatus.Success:
|
||||||
|
return prev + `✅ ${curr}\n`;
|
||||||
|
case ProgressStatus.Working:
|
||||||
|
return prev + `🔄 ${curr}\n`;
|
||||||
|
case ProgressStatus.Warn:
|
||||||
|
return prev + `⚠️ ${curr}\n`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return prev + `✅ ${curr}\n`;
|
||||||
|
}
|
||||||
|
}, '');
|
||||||
|
|
||||||
|
console.clear();
|
||||||
|
console.log(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取规范化文件名
|
||||||
|
* @param output Rollup 输出
|
||||||
|
* @param client 是否是客户端代码
|
||||||
|
*/
|
||||||
|
function getFileName(output: OutputChunk | OutputAsset, client: boolean) {
|
||||||
|
const name = output.fileName;
|
||||||
|
if (name.startsWith('index-legacy') && client) {
|
||||||
|
return 'main';
|
||||||
|
}
|
||||||
|
if (name.startsWith('data') && !client) {
|
||||||
|
return 'main';
|
||||||
|
}
|
||||||
|
if (name.startsWith('index.html') && client) {
|
||||||
|
return 'index';
|
||||||
|
}
|
||||||
|
const index = name.indexOf('-legacy');
|
||||||
|
const unhash = name.slice(0, index);
|
||||||
|
return unhash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件大小
|
||||||
|
* @param output Rollup 输出
|
||||||
|
*/
|
||||||
|
function getFileSize(output: OutputChunk | OutputAsset) {
|
||||||
|
if (output.type === 'asset') {
|
||||||
|
if (typeof output.source === 'string') {
|
||||||
|
return Buffer.byteLength(output.source);
|
||||||
|
} else {
|
||||||
|
return output.source.byteLength;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Buffer.byteLength(output.code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const enum ClientDataLevel {
|
||||||
|
Error,
|
||||||
|
Suspect,
|
||||||
|
Pass
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查客户端与数据端文件大小,并给出数据端引用客户端的可能性
|
||||||
|
* @param client 客户端文件大小
|
||||||
|
* @param data 数据端文件大小
|
||||||
|
*/
|
||||||
|
function checkClientData(
|
||||||
|
client: Map<string, number>,
|
||||||
|
data: Map<string, number>
|
||||||
|
) {
|
||||||
|
let error = false;
|
||||||
|
let warn = false;
|
||||||
|
|
||||||
|
const clientMain = client.get('main');
|
||||||
|
const dataMain = data.get('main');
|
||||||
|
|
||||||
|
if (clientMain && dataMain) {
|
||||||
|
if (clientMain <= dataMain) {
|
||||||
|
error = true;
|
||||||
|
} else if (clientMain / 2 < dataMain) {
|
||||||
|
warn = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let clientTotal = 0;
|
||||||
|
let dataTotal = 0;
|
||||||
|
|
||||||
|
client.forEach(v => {
|
||||||
|
clientTotal += v;
|
||||||
|
});
|
||||||
|
|
||||||
|
data.forEach(v => {
|
||||||
|
dataTotal += v;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (clientTotal <= dataTotal) {
|
||||||
|
error = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientTotal / 4 < dataTotal) {
|
||||||
|
warn = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) return ClientDataLevel.Error;
|
||||||
|
else if (warn) return ClientDataLevel.Suspect;
|
||||||
|
else return ClientDataLevel.Pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAllChars(client: RollupOutput[]) {
|
||||||
|
const chars = new Set<string>();
|
||||||
|
|
||||||
|
// 1. 客户端构建结果
|
||||||
|
client.forEach(v => {
|
||||||
|
v.output.forEach(v => {
|
||||||
|
if (v.type === 'chunk' && v.fileName.startsWith('index')) {
|
||||||
|
const set = new Set(v.code);
|
||||||
|
set.forEach(v => chars.add(v));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 样板内容
|
||||||
|
const files: string[] = [];
|
||||||
|
|
||||||
|
files.push(resolve(tempDir, 'client/main.js'));
|
||||||
|
files.push(resolve(tempDir, 'client/project/data.js'));
|
||||||
|
files.push(resolve(tempDir, 'client/project/enemys.js'));
|
||||||
|
files.push(resolve(tempDir, 'client/project/events.js'));
|
||||||
|
files.push(resolve(tempDir, 'client/project/functions.js'));
|
||||||
|
files.push(resolve(tempDir, 'client/project/icons.js'));
|
||||||
|
files.push(resolve(tempDir, 'client/project/items.js'));
|
||||||
|
files.push(resolve(tempDir, 'client/project/maps.js'));
|
||||||
|
|
||||||
|
const floors = await readdir(resolve(tempDir, 'client/project/floors'));
|
||||||
|
const ids = floors.map(v => resolve(tempDir, 'client/project/floors', v));
|
||||||
|
files.push(...ids);
|
||||||
|
|
||||||
|
const libs = await readdir(resolve(tempDir, 'client/libs'));
|
||||||
|
files.push(...libs.map(v => resolve(tempDir, 'client/libs', v)));
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
files.map(async v => {
|
||||||
|
const stats = await stat(v);
|
||||||
|
if (!stats.isFile()) return;
|
||||||
|
const file = await readFile(v, 'utf-8');
|
||||||
|
const set = new Set(file);
|
||||||
|
set.forEach(v => chars.add(v));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return chars;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CompressedLoadListItem {
|
||||||
|
type: ResourceType;
|
||||||
|
name: string;
|
||||||
|
usage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompressedLoadList = Record<string, CompressedLoadListItem[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成资源地图 json 文件
|
||||||
|
*/
|
||||||
|
function generateResourceJSON(resources: SplittedResource[]) {
|
||||||
|
const list: CompressedLoadList = {};
|
||||||
|
|
||||||
|
resources.forEach(file => {
|
||||||
|
const uri = `project/resource/${file.fileName}`;
|
||||||
|
file.content.forEach(content => {
|
||||||
|
const item: CompressedLoadListItem = {
|
||||||
|
type: content.type,
|
||||||
|
name: content.name,
|
||||||
|
usage: content.usage
|
||||||
|
};
|
||||||
|
list[uri] ??= [];
|
||||||
|
list[uri].push(item);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return JSON.stringify(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildGame() {
|
||||||
|
logProgress(0, ProgressStatus.Working);
|
||||||
|
|
||||||
|
//#region 准备步骤
|
||||||
|
try {
|
||||||
|
await ensureDir(distDir);
|
||||||
|
await ensureDir(tempDir);
|
||||||
|
await emptyDir(distDir);
|
||||||
|
await emptyDir(tempDir);
|
||||||
|
await ensureDir(resolve(tempDir, 'fonts'));
|
||||||
|
await ensureDir(resolve(tempDir, 'common'));
|
||||||
|
await ensureDir(resolve(tempDir, 'resource'));
|
||||||
|
} catch (e) {
|
||||||
|
logProgress(0, ProgressStatus.Fail);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
logProgress(1, ProgressStatus.Working);
|
||||||
|
|
||||||
|
//#region 构建客户端
|
||||||
|
const clientPack = await buildClient(resolve(tempDir, 'client')).catch(
|
||||||
|
reason => {
|
||||||
|
logProgress(1, ProgressStatus.Fail);
|
||||||
|
console.error(reason);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
logProgress(2, ProgressStatus.Working);
|
||||||
|
|
||||||
|
//#region 构建数据端
|
||||||
|
const dataPack = await buildData(
|
||||||
|
resolve(tempDir, 'data'),
|
||||||
|
resolve(process.cwd(), 'src/data.ts')
|
||||||
|
).catch(reason => {
|
||||||
|
logProgress(2, ProgressStatus.Fail);
|
||||||
|
console.error(reason);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientSize = new Map<string, number>();
|
||||||
|
const dataSize = new Map<string, number>();
|
||||||
|
|
||||||
|
const clientPackArr = [];
|
||||||
|
const dataPackArr = [];
|
||||||
|
|
||||||
|
// 判断客户端与数据端的构建包大小,从而推断是否出现了数据端引用客户端的问题
|
||||||
|
|
||||||
|
if (clientPack instanceof Array) {
|
||||||
|
clientPackArr.push(...clientPack);
|
||||||
|
} else if ('close' in clientPack) {
|
||||||
|
// pass.
|
||||||
|
} else {
|
||||||
|
clientPackArr.push(clientPack);
|
||||||
|
}
|
||||||
|
if (dataPack instanceof Array) {
|
||||||
|
dataPackArr.push(...dataPack);
|
||||||
|
} else if ('close' in dataPack) {
|
||||||
|
// pass.
|
||||||
|
} else {
|
||||||
|
dataPackArr.push(dataPack);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取每个 chunk 的大小
|
||||||
|
clientPackArr.forEach(v => {
|
||||||
|
v.output.forEach(v => {
|
||||||
|
const name = getFileName(v, true);
|
||||||
|
const size = getFileSize(v);
|
||||||
|
clientSize.set(name, size);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
dataPackArr.forEach(v => {
|
||||||
|
v.output.forEach(v => {
|
||||||
|
const name = getFileName(v, false);
|
||||||
|
const size = getFileSize(v);
|
||||||
|
dataSize.set(name, size);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const level = checkClientData(clientSize, dataSize);
|
||||||
|
|
||||||
|
if (level === ClientDataLevel.Error) {
|
||||||
|
logProgress(2, ProgressStatus.Fail);
|
||||||
|
console.error(`客户端似乎引用了数据端内容,请仔细检查后再构建!`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
logProgress(3, ProgressStatus.Working);
|
||||||
|
|
||||||
|
// 解析全塔属性
|
||||||
|
const dataFile = await readFile(
|
||||||
|
resolve(process.cwd(), 'public/project/data.js'),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
const dataObject: RequiredData = JSON.parse(
|
||||||
|
dataFile.split('\n').slice(1).join('\n')
|
||||||
|
);
|
||||||
|
const mainData = dataObject.main;
|
||||||
|
|
||||||
|
logProgress(3, ProgressStatus.Working);
|
||||||
|
|
||||||
|
//#region 压缩 main
|
||||||
|
try {
|
||||||
|
const main = await readFile(
|
||||||
|
resolve(tempDir, 'client/main.js'),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
const [head, tail] = main.split('// >>>> body end');
|
||||||
|
const transformed = await transformAsync(tail, {
|
||||||
|
presets: [['@babel/preset-env']],
|
||||||
|
sourceType: 'script',
|
||||||
|
minified: true,
|
||||||
|
comments: false
|
||||||
|
});
|
||||||
|
if (!transformed || !transformed.code) {
|
||||||
|
throw new ReferenceError(
|
||||||
|
`Cannot write main.js since transform result is empty.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const code = transformed.code;
|
||||||
|
await writeFile(
|
||||||
|
resolve(tempDir, 'common/main.js'),
|
||||||
|
head + '\n// >>>> body end\n' + code,
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
logProgress(3, ProgressStatus.Fail);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
//#region 压缩字体
|
||||||
|
const chars = await getAllChars(clientPackArr).catch(reason => {
|
||||||
|
logProgress(4, ProgressStatus.Fail);
|
||||||
|
console.error(reason);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
const { fonts } = mainData;
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
fonts.map(v => {
|
||||||
|
const fontmin = new Fontmin();
|
||||||
|
const src = resolve(tempDir, 'client/project/fonts', `${v}.ttf`);
|
||||||
|
const dest = resolve(tempDir, 'fonts');
|
||||||
|
const plugin = Fontmin.glyph({
|
||||||
|
text: [...chars].join('')
|
||||||
|
});
|
||||||
|
fontmin.src(src).dest(dest).use(plugin);
|
||||||
|
return fontmin.runAsync();
|
||||||
|
})
|
||||||
|
).catch(reason => {
|
||||||
|
logProgress(4, ProgressStatus.Fail);
|
||||||
|
console.error(reason);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
logProgress(5, ProgressStatus.Working);
|
||||||
|
|
||||||
|
//#region 资源分块
|
||||||
|
const iconsFile = await readFile(
|
||||||
|
resolve(process.cwd(), 'public/project/icons.js'),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
const iconsObject: RequiredIconsData = JSON.parse(
|
||||||
|
iconsFile.split('\n').slice(1).join('\n')
|
||||||
|
);
|
||||||
|
const resources = await splitResource(
|
||||||
|
dataObject,
|
||||||
|
iconsObject,
|
||||||
|
resolve(tempDir, 'client'),
|
||||||
|
resolve(tempDir, 'fonts'),
|
||||||
|
RESOUCE_SIZE
|
||||||
|
).catch(reason => {
|
||||||
|
logProgress(5, ProgressStatus.Fail);
|
||||||
|
console.error(reason);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
resources.map(v => {
|
||||||
|
return writeFile(
|
||||||
|
resolve(tempDir, 'resource', v.fileName),
|
||||||
|
v.buffer
|
||||||
|
);
|
||||||
|
})
|
||||||
|
).catch(reason => {
|
||||||
|
logProgress(5, ProgressStatus.Fail);
|
||||||
|
console.error(reason);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
logProgress(6, ProgressStatus.Working);
|
||||||
|
|
||||||
|
//#region 最后处理
|
||||||
|
|
||||||
|
const toCopy = [
|
||||||
|
'libs',
|
||||||
|
'_server',
|
||||||
|
'extensions',
|
||||||
|
'index.html',
|
||||||
|
'editor.html',
|
||||||
|
'styles.css',
|
||||||
|
'logo.png',
|
||||||
|
'project/floors',
|
||||||
|
'project/data.js',
|
||||||
|
'project/enemys.js',
|
||||||
|
'project/events.js',
|
||||||
|
'project/functions.js',
|
||||||
|
'project/icons.js',
|
||||||
|
'project/items.js',
|
||||||
|
'project/maps.js',
|
||||||
|
'project/plugins.js'
|
||||||
|
];
|
||||||
|
|
||||||
|
clientPackArr.forEach(v => {
|
||||||
|
v.output.forEach(v => {
|
||||||
|
toCopy.push(v.fileName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
toCopy.map(v =>
|
||||||
|
copy(resolve(tempDir, 'client', v), resolve(distDir, v))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await copy(
|
||||||
|
resolve(tempDir, 'resource'),
|
||||||
|
resolve(distDir, 'project/resource')
|
||||||
|
);
|
||||||
|
await copy(
|
||||||
|
resolve(tempDir, 'common/main.js'),
|
||||||
|
resolve(distDir, 'main.js')
|
||||||
|
);
|
||||||
|
await copy(
|
||||||
|
resolve(tempDir, 'data/data.iife.js'),
|
||||||
|
resolve(distDir, 'data.process.js')
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
dataObject.main.bgms.map(v =>
|
||||||
|
copy(
|
||||||
|
resolve(tempDir, 'client/project/bgms', v),
|
||||||
|
resolve(distDir, 'project/bgms', v)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const scripts = archiver('zip');
|
||||||
|
scripts.directory('packages/', resolve(process.cwd(), 'packages'));
|
||||||
|
scripts.directory(
|
||||||
|
'packages-user/',
|
||||||
|
resolve(process.cwd(), 'packages-user')
|
||||||
|
);
|
||||||
|
scripts.directory('src/', resolve(process.cwd(), 'src'));
|
||||||
|
|
||||||
|
const output = createWriteStream(resolve(distDir, 'source-code.zip'));
|
||||||
|
scripts.pipe(output);
|
||||||
|
|
||||||
|
output.on('error', err => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise<void>(res => {
|
||||||
|
output.on('finish', () => res());
|
||||||
|
scripts.finalize();
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = generateResourceJSON(resources);
|
||||||
|
await writeFile(resolve(distDir, 'loadList.json'), json, 'utf-8');
|
||||||
|
|
||||||
|
await copy(
|
||||||
|
resolve(process.cwd(), 'LICENSE'),
|
||||||
|
resolve(distDir, 'LICENSE')
|
||||||
|
);
|
||||||
|
|
||||||
|
await copy(
|
||||||
|
resolve(process.cwd(), 'script/template/启动服务.exe'),
|
||||||
|
resolve(distDir, '启动服务.exe')
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
logProgress(6, ProgressStatus.Fail);
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
logProgress(7, ProgressStatus.Working);
|
||||||
|
|
||||||
|
//#region 压缩游戏
|
||||||
|
|
||||||
|
try {
|
||||||
|
await zip.compressDir(
|
||||||
|
resolve(distDir),
|
||||||
|
resolve(process.cwd(), 'dist.zip')
|
||||||
|
);
|
||||||
|
|
||||||
|
await emptyDir(tempDir);
|
||||||
|
await rmdir(tempDir);
|
||||||
|
} catch (e) {
|
||||||
|
logProgress(7, ProgressStatus.Fail);
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
//#region 输出构建信息
|
||||||
|
|
||||||
|
const sourceStats = await stat(resolve(distDir, 'source-code.zip'));
|
||||||
|
const sourceSize = sourceStats.size;
|
||||||
|
const zipStats = await stat(resolve(process.cwd(), 'dist.zip'));
|
||||||
|
const zipSize = zipStats.size;
|
||||||
|
const resourceSize = resources.reduce(
|
||||||
|
(prev, curr) => prev + curr.byteLength,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
console.clear();
|
||||||
|
console.log(`✅ 构建已完成!`);
|
||||||
|
if (zipSize > 100 * 2 ** 20) {
|
||||||
|
console.log(
|
||||||
|
`⚠️ 压缩包大于 100M,可能导致发塔困难,请考虑降低塔的大小,`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log(`压缩包大小:${formatSize(zipSize)}`);
|
||||||
|
console.log(`源码大小:${formatSize(sourceSize)}`);
|
||||||
|
console.log(`资源大小:${formatSize(resourceSize)}`);
|
||||||
|
resources.forEach(v => {
|
||||||
|
console.log(
|
||||||
|
`--> ${v.fileName} ${formatSize(v.byteLength)} | ${v.content.length} 个资源`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
(() => {
|
||||||
|
buildGame();
|
||||||
|
})();
|
||||||
|
188
script/build-resource.ts
Normal file
188
script/build-resource.ts
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
import JSZip from 'jszip';
|
||||||
|
import {
|
||||||
|
RequiredData,
|
||||||
|
RequiredIconsData,
|
||||||
|
ResourceType,
|
||||||
|
ResourceUsage
|
||||||
|
} from './types';
|
||||||
|
import { Stats } from 'fs';
|
||||||
|
import { readdir, readFile, stat } from 'fs/promises';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import { fileHash } from './utils';
|
||||||
|
|
||||||
|
export interface ResourceInfo {
|
||||||
|
name: string;
|
||||||
|
type: ResourceType;
|
||||||
|
usage: ResourceUsage;
|
||||||
|
stats: Stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SplittedResource {
|
||||||
|
readonly byteLength: number;
|
||||||
|
readonly resource: JSZip;
|
||||||
|
readonly buffer: Uint8Array;
|
||||||
|
readonly fileName: string;
|
||||||
|
readonly content: Readonly<ResourceInfo>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResourceContent extends ResourceInfo {
|
||||||
|
content: string | Buffer | Uint8Array;
|
||||||
|
exceed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResourcePath {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
usage: ResourceUsage;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTypeByUsage(usage: ResourceUsage): ResourceType {
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFileOfType(path: string, type: ResourceType) {
|
||||||
|
if (type === 'text') {
|
||||||
|
return readFile(path, 'utf-8');
|
||||||
|
} else {
|
||||||
|
return readFile(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function compressFiles(files: ResourceContent[]) {
|
||||||
|
const zip = new JSZip();
|
||||||
|
files.forEach(v => {
|
||||||
|
const dir = `${v.type}/${v.name}`;
|
||||||
|
zip.file(dir, v.content);
|
||||||
|
});
|
||||||
|
const buffer = await zip.generateAsync({ type: 'uint8array' });
|
||||||
|
|
||||||
|
const hash = fileHash(buffer);
|
||||||
|
const name = `resource.${hash}.h5data`;
|
||||||
|
|
||||||
|
const resource: SplittedResource = {
|
||||||
|
byteLength: buffer.byteLength,
|
||||||
|
buffer: buffer,
|
||||||
|
resource: zip,
|
||||||
|
fileName: name,
|
||||||
|
content: files
|
||||||
|
};
|
||||||
|
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function splitResource(
|
||||||
|
data: RequiredData,
|
||||||
|
icons: RequiredIconsData,
|
||||||
|
base: string,
|
||||||
|
fontsDir: string,
|
||||||
|
limit: number
|
||||||
|
) {
|
||||||
|
const result: SplittedResource[] = [];
|
||||||
|
|
||||||
|
// 获取所有需要分块的资源
|
||||||
|
const { animates, fonts, images, sounds, tilesets } = data.main;
|
||||||
|
const autotiles = Object.keys(icons.autotile);
|
||||||
|
const materials = await readdir(resolve(base, 'project/materials'));
|
||||||
|
|
||||||
|
const paths: ResourcePath[] = [
|
||||||
|
...animates.map<ResourcePath>(v => ({
|
||||||
|
name: `${v}.animate`,
|
||||||
|
path: resolve(base, 'project/animates', `${v}.animate`),
|
||||||
|
usage: 'animate'
|
||||||
|
})),
|
||||||
|
...fonts.map<ResourcePath>(v => ({
|
||||||
|
name: `${v}.ttf`,
|
||||||
|
path: resolve(fontsDir, `${v}.ttf`),
|
||||||
|
usage: 'font'
|
||||||
|
})),
|
||||||
|
...images.map<ResourcePath>(v => ({
|
||||||
|
name: v,
|
||||||
|
path: resolve(base, 'project/images', v),
|
||||||
|
usage: 'image'
|
||||||
|
})),
|
||||||
|
...sounds.map<ResourcePath>(v => ({
|
||||||
|
name: v,
|
||||||
|
path: resolve(base, 'project/sounds', v),
|
||||||
|
usage: 'sound'
|
||||||
|
})),
|
||||||
|
...tilesets.map<ResourcePath>(v => ({
|
||||||
|
name: v,
|
||||||
|
path: resolve(base, 'project/tilesets', v),
|
||||||
|
usage: 'tileset'
|
||||||
|
})),
|
||||||
|
...autotiles.map<ResourcePath>(v => ({
|
||||||
|
name: `${v}.png`,
|
||||||
|
path: resolve(base, 'project/autotiles', `${v}.png`),
|
||||||
|
usage: 'autotile'
|
||||||
|
})),
|
||||||
|
...materials.map<ResourcePath>(v => ({
|
||||||
|
name: v,
|
||||||
|
path: resolve(base, 'project/materials', v),
|
||||||
|
usage: 'material'
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
|
||||||
|
const files = await Promise.all(
|
||||||
|
paths.map(async ({ path, usage, name }) => {
|
||||||
|
const stats = await stat(path);
|
||||||
|
if (!stats.isFile()) {
|
||||||
|
return Promise.reject(
|
||||||
|
new ReferenceError(
|
||||||
|
`Expected resource is a file, but get directory.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const type = getTypeByUsage(usage);
|
||||||
|
const content = await readFileOfType(path, type);
|
||||||
|
const info: ResourceContent = {
|
||||||
|
type,
|
||||||
|
name,
|
||||||
|
usage,
|
||||||
|
stats,
|
||||||
|
content,
|
||||||
|
exceed: stats.size > limit
|
||||||
|
};
|
||||||
|
return info;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 从小到大排序,这样的话可以尽量减小资源分块文件数量
|
||||||
|
files.sort((a, b) => a.stats.size - b.stats.size);
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
while (index < files.length) {
|
||||||
|
let total = 0;
|
||||||
|
const start = index;
|
||||||
|
for (let i = index; i < files.length; i++) {
|
||||||
|
const file = files[i];
|
||||||
|
if (file.exceed) {
|
||||||
|
if (i === index) i = index + 1;
|
||||||
|
index = i;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
total += file.stats.size;
|
||||||
|
}
|
||||||
|
if (total > limit) {
|
||||||
|
index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const toZip = files.slice(start, index);
|
||||||
|
result.push(await compressFiles(toZip));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
209
script/build.ts
209
script/build.ts
@ -1,209 +0,0 @@
|
|||||||
import fss from 'fs';
|
|
||||||
import fs from 'fs-extra';
|
|
||||||
import Fontmin from 'fontmin';
|
|
||||||
import * as babel from '@babel/core';
|
|
||||||
import * as rollup from 'rollup';
|
|
||||||
import typescript from '@rollup/plugin-typescript';
|
|
||||||
import rollupBabel from '@rollup/plugin-babel';
|
|
||||||
import terser from '@rollup/plugin-terser';
|
|
||||||
import resolve from '@rollup/plugin-node-resolve';
|
|
||||||
import commonjs from '@rollup/plugin-commonjs';
|
|
||||||
import { splitResource } from './resource.js';
|
|
||||||
import compressing from 'compressing';
|
|
||||||
import json from '@rollup/plugin-json';
|
|
||||||
|
|
||||||
const type = process.argv[2];
|
|
||||||
const map = false;
|
|
||||||
const resorce = true;
|
|
||||||
const compress = type === 'dist';
|
|
||||||
|
|
||||||
(async function () {
|
|
||||||
const timestamp = Date.now();
|
|
||||||
// 1. 去除未使用的文件
|
|
||||||
const data = (() => {
|
|
||||||
const data = fss.readFileSync('./public/project/data.js', 'utf-8');
|
|
||||||
const json = JSON.parse(
|
|
||||||
data
|
|
||||||
.split(/(\n|\r\n)/)
|
|
||||||
.slice(1)
|
|
||||||
.join('\n')
|
|
||||||
);
|
|
||||||
return json;
|
|
||||||
})() as { main: Record<string, string[]> };
|
|
||||||
const main = data.main;
|
|
||||||
try {
|
|
||||||
const data = [
|
|
||||||
['./dist/project/floors', '.js', 'floorIds'],
|
|
||||||
['./dist/project/bgms', '', 'bgms'],
|
|
||||||
['./dist/project/sounds', '', 'sounds'],
|
|
||||||
['./dist/project/images', '', 'images'],
|
|
||||||
['./dist/project/animates', '.animate', 'animates'],
|
|
||||||
['./dist/project/tilesets', '', 'tilesets'],
|
|
||||||
['./dist/project/fonts', '.ttf', 'fonts']
|
|
||||||
];
|
|
||||||
await Promise.all(
|
|
||||||
data.map(async v => {
|
|
||||||
const all = await fs.readdir(`${v[0]}`);
|
|
||||||
const data = main[v[2]].map(vv => vv + v[1]);
|
|
||||||
all.forEach(async vv => {
|
|
||||||
if (!data.includes(vv)) {
|
|
||||||
await fs.rm(`${v[0]}/${vv}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
if (!map) await fs.remove('./dist/maps/');
|
|
||||||
// 在线查看什么都看不到,这编辑器难道还需要留着吗?
|
|
||||||
await fs.remove('./dist/_server');
|
|
||||||
await fs.remove('./dist/editor.html');
|
|
||||||
await fs.remove('./dist/server.cjs');
|
|
||||||
|
|
||||||
await fs.remove('./dist/project/materials/airwall.png');
|
|
||||||
await fs.remove('./dist/project/materials/ground.png');
|
|
||||||
await fs.remove('./dist/project/materials/icons_old.png');
|
|
||||||
} catch (e) {
|
|
||||||
console.log('去除未使用的文件失败!');
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 压缩字体
|
|
||||||
try {
|
|
||||||
// 获取要压缩的文字列表,libs & projects下的所有js文件
|
|
||||||
let texts = ``;
|
|
||||||
const exclude = `\n \t`;
|
|
||||||
const libs = await fs.readdir('./public/libs');
|
|
||||||
const project = await fs.readdir('./public/project');
|
|
||||||
const floors = await fs.readdir('./public/project/floors');
|
|
||||||
const assets = await fs.readdir('./dist/assets/');
|
|
||||||
const all = [
|
|
||||||
...libs.map(v => `./public/libs/${v}`),
|
|
||||||
...project.map(v => `./public/project/${v}`),
|
|
||||||
...floors.map(v => `./public/project/floors/${v}`),
|
|
||||||
...assets.map(v => `./dist/assets/${v}`)
|
|
||||||
];
|
|
||||||
for await (const dir of all) {
|
|
||||||
const stat = await fs.stat(dir);
|
|
||||||
if (!stat.isFile()) continue;
|
|
||||||
if (dir.endsWith('.ttf')) continue;
|
|
||||||
const file = await fs.readFile(dir, 'utf-8');
|
|
||||||
for (let i = 0; i < file.length; i++) {
|
|
||||||
const char = file[i];
|
|
||||||
if (!texts.includes(char) && !exclude.includes(char))
|
|
||||||
texts += char;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取所有字体(直接压缩字体会报错
|
|
||||||
const fonts = main.fonts;
|
|
||||||
await Promise.all([
|
|
||||||
...fonts.map(v =>
|
|
||||||
(async () => {
|
|
||||||
const fontmin = new Fontmin();
|
|
||||||
fontmin
|
|
||||||
.src<string>(`./public/project/fonts/${v}.ttf`)
|
|
||||||
.dest('./dist/project/fonts')
|
|
||||||
.use(
|
|
||||||
Fontmin.glyph({
|
|
||||||
text: texts
|
|
||||||
})
|
|
||||||
);
|
|
||||||
await new Promise(res => {
|
|
||||||
fontmin.run(err => {
|
|
||||||
if (err) throw err;
|
|
||||||
res('');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})()
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
} catch (e) {
|
|
||||||
console.log('字体压缩失败');
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 压缩js插件
|
|
||||||
try {
|
|
||||||
await fs.remove('./dist/project/plugin.min.js');
|
|
||||||
|
|
||||||
const build = await rollup.rollup({
|
|
||||||
input: 'src/game/index.ts',
|
|
||||||
plugins: [
|
|
||||||
typescript({
|
|
||||||
sourceMap: false
|
|
||||||
}),
|
|
||||||
rollupBabel({
|
|
||||||
// todo: 是否需要添加 polyfill?
|
|
||||||
babelHelpers: 'bundled',
|
|
||||||
sourceType: 'module'
|
|
||||||
}),
|
|
||||||
resolve(),
|
|
||||||
commonjs(),
|
|
||||||
json()
|
|
||||||
]
|
|
||||||
});
|
|
||||||
await build.write({
|
|
||||||
format: 'iife',
|
|
||||||
name: 'CorePlugin',
|
|
||||||
file: './dist/project/plugin.min.js'
|
|
||||||
});
|
|
||||||
|
|
||||||
await fs.remove('./dist/project/plugin/');
|
|
||||||
} catch (e) {
|
|
||||||
console.log('压缩插件失败');
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 压缩main.js
|
|
||||||
try {
|
|
||||||
// 先获取不能压缩的部分
|
|
||||||
const main = await fs.readFile('./public/main.js', 'utf-8');
|
|
||||||
|
|
||||||
const endIndex = main.indexOf('// >>>> body end');
|
|
||||||
const nonCompress = main.slice(0, endIndex);
|
|
||||||
const needCompress = main.slice(endIndex + 17);
|
|
||||||
const compressed = babel.transformSync(needCompress)?.code;
|
|
||||||
await fs.writeFile('./dist/main.js', nonCompress + compressed, 'utf-8');
|
|
||||||
} catch (e) {
|
|
||||||
console.log('main.js压缩失败');
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 杂项
|
|
||||||
try {
|
|
||||||
await fs.copy('./LICENSE', './dist/LICENSE');
|
|
||||||
} catch (e) {
|
|
||||||
console.log('添加杂项失败');
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 资源分离
|
|
||||||
if (resorce) {
|
|
||||||
await splitResource();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!compress) {
|
|
||||||
await fs.copy('./script/template/启动服务.exe', './dist/启动服务.exe');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. 压缩
|
|
||||||
if (compress) {
|
|
||||||
try {
|
|
||||||
await fs.ensureDir('./out');
|
|
||||||
await compressing.zip.compressDir('./dist', './out/dist.zip');
|
|
||||||
|
|
||||||
// 压缩资源
|
|
||||||
if (resorce) {
|
|
||||||
const resources = await fs.readdir('./dist-resource');
|
|
||||||
for await (const index of resources) {
|
|
||||||
await compressing.zip.compressDir(
|
|
||||||
`./dist-resource/${index}`,
|
|
||||||
`./out/${index}.zip`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log('压缩为zip失败!');
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
@ -1,237 +0,0 @@
|
|||||||
import fs, { Stats } from 'fs-extra';
|
|
||||||
import JSZip from 'jszip';
|
|
||||||
import { formatSize, uniqueSymbol } from './utils.js';
|
|
||||||
|
|
||||||
// 资源拆分模块,可以加快在线加载速度
|
|
||||||
|
|
||||||
type ResourceType =
|
|
||||||
| 'text'
|
|
||||||
| 'buffer'
|
|
||||||
| 'image'
|
|
||||||
| 'material'
|
|
||||||
| 'audio'
|
|
||||||
| 'json'
|
|
||||||
| 'zip'
|
|
||||||
| 'byte';
|
|
||||||
interface CompressedLoadListItem {
|
|
||||||
type: ResourceType;
|
|
||||||
name: string;
|
|
||||||
usage: string;
|
|
||||||
}
|
|
||||||
type CompressedLoadList = Record<string, CompressedLoadListItem[]>;
|
|
||||||
|
|
||||||
interface MainData {
|
|
||||||
main: {
|
|
||||||
images: string[];
|
|
||||||
tilesets: string[];
|
|
||||||
animates: string[];
|
|
||||||
sounds: string[];
|
|
||||||
fonts: string[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 单包大小,2M */
|
|
||||||
const SPLIT_SIZE = 2 ** 20 * 2;
|
|
||||||
|
|
||||||
export async function splitResource() {
|
|
||||||
const splitResult: CompressedLoadList = {};
|
|
||||||
let now: CompressedLoadListItem[] = [];
|
|
||||||
let totalSize: number = 0;
|
|
||||||
let nowZip: JSZip = new JSZip();
|
|
||||||
|
|
||||||
await fs.ensureDir('./dist/resource/');
|
|
||||||
await fs.emptyDir('./dist/resource');
|
|
||||||
|
|
||||||
const pushItem = async (
|
|
||||||
type: ResourceType,
|
|
||||||
name: string,
|
|
||||||
usage: string,
|
|
||||||
file: Stats,
|
|
||||||
content: any
|
|
||||||
) => {
|
|
||||||
totalSize += file.size;
|
|
||||||
|
|
||||||
if (totalSize > SPLIT_SIZE) {
|
|
||||||
if (file.size > SPLIT_SIZE) {
|
|
||||||
const symbol = uniqueSymbol() + `.h5data`;
|
|
||||||
console.warn(
|
|
||||||
`file ${type}/${name}(${formatSize(
|
|
||||||
file.size
|
|
||||||
)}) is larger than split limit (${formatSize(
|
|
||||||
SPLIT_SIZE
|
|
||||||
)}), single zip will be generated.`
|
|
||||||
);
|
|
||||||
splitResult['resource/' + symbol] = [{ type, name, usage }];
|
|
||||||
const zip = new JSZip();
|
|
||||||
addZippedFile(zip, type, name, content);
|
|
||||||
await writeZip(zip, `./dist/resource/${symbol}`, file.size);
|
|
||||||
totalSize -= file.size;
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
const symbol = uniqueSymbol() + `.h5data`;
|
|
||||||
splitResult['resource/' + symbol] = now;
|
|
||||||
await writeZip(
|
|
||||||
nowZip,
|
|
||||||
`./dist/resource/${symbol}`,
|
|
||||||
totalSize - file.size
|
|
||||||
);
|
|
||||||
nowZip = new JSZip();
|
|
||||||
totalSize = 0;
|
|
||||||
now = [];
|
|
||||||
await pushItem(type, name, usage, file, content);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
now.push({ type, name, usage });
|
|
||||||
addZippedFile(nowZip, type, name, content);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const writeZip = (zip: JSZip, name: string, size: number) => {
|
|
||||||
return new Promise<void>(res => {
|
|
||||||
zip.generateNodeStream({ type: 'nodebuffer', streamFiles: true })
|
|
||||||
.pipe(fs.createWriteStream(name))
|
|
||||||
.once('finish', function () {
|
|
||||||
console.log(
|
|
||||||
`Generated ${name}. Unzipped size: ${formatSize(size)}`
|
|
||||||
);
|
|
||||||
res();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const addZippedFile = (
|
|
||||||
zip: JSZip,
|
|
||||||
type: ResourceType,
|
|
||||||
name: string,
|
|
||||||
content: any
|
|
||||||
) => {
|
|
||||||
zip.file(`${type}/${name}`, content);
|
|
||||||
};
|
|
||||||
|
|
||||||
const file = await fs.readFile('./dist/project/data.js', 'utf-8');
|
|
||||||
const data = JSON.parse(file.split('\n').slice(1).join('')) as MainData;
|
|
||||||
|
|
||||||
// images
|
|
||||||
for (const image of data.main.images) {
|
|
||||||
const path = `./dist/project/images/${image}`;
|
|
||||||
const stat = await fs.stat(path);
|
|
||||||
await pushItem('image', image, 'image', stat, await fs.readFile(path));
|
|
||||||
}
|
|
||||||
|
|
||||||
// tileset
|
|
||||||
for (const tileset of data.main.tilesets) {
|
|
||||||
const path = `./dist/project/tilesets/${tileset}`;
|
|
||||||
const stat = await fs.stat(path);
|
|
||||||
await pushItem(
|
|
||||||
'image',
|
|
||||||
tileset,
|
|
||||||
'tileset',
|
|
||||||
stat,
|
|
||||||
await fs.readFile(path)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// animates
|
|
||||||
for (const ani of data.main.animates) {
|
|
||||||
const path = `./dist/project/animates/${ani}.animate`;
|
|
||||||
const stat = await fs.stat(path);
|
|
||||||
await pushItem(
|
|
||||||
'text',
|
|
||||||
ani + '.animate',
|
|
||||||
'animate',
|
|
||||||
stat,
|
|
||||||
await fs.readFile(path, 'utf-8')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// sounds
|
|
||||||
for (const sound of data.main.sounds) {
|
|
||||||
const path = `./dist/project/sounds/${sound}`;
|
|
||||||
const stat = await fs.stat(path);
|
|
||||||
await pushItem('byte', sound, 'sound', stat, await fs.readFile(path));
|
|
||||||
}
|
|
||||||
|
|
||||||
// fonts
|
|
||||||
for (const font of data.main.fonts) {
|
|
||||||
const path = `./dist/project/fonts/${font}.ttf`;
|
|
||||||
const stat = await fs.stat(path);
|
|
||||||
await pushItem(
|
|
||||||
'buffer',
|
|
||||||
font + '.ttf',
|
|
||||||
'font',
|
|
||||||
stat,
|
|
||||||
await fs.readFile(path)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// autotiles
|
|
||||||
const autotiles = await fs.readdir('./dist/project/autotiles');
|
|
||||||
for (const a of autotiles) {
|
|
||||||
const path = `./dist/project/autotiles/${a}`;
|
|
||||||
const stat = await fs.stat(path);
|
|
||||||
await pushItem('image', a, 'autotile', stat, await fs.readFile(path));
|
|
||||||
}
|
|
||||||
|
|
||||||
// materials
|
|
||||||
const materials = await fs.readdir('./dist/project/materials');
|
|
||||||
for (const m of materials) {
|
|
||||||
const path = `./dist/project/materials/${m}`;
|
|
||||||
const stat = await fs.stat(path);
|
|
||||||
await pushItem(
|
|
||||||
'material',
|
|
||||||
m,
|
|
||||||
'material',
|
|
||||||
stat,
|
|
||||||
await fs.readFile(path)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const symbol = uniqueSymbol() + `.h5data`;
|
|
||||||
splitResult['resource/' + symbol] = now;
|
|
||||||
await writeZip(nowZip, `./dist/resource/${symbol}`, totalSize);
|
|
||||||
|
|
||||||
// 添加资源映射
|
|
||||||
await fs.writeFile(
|
|
||||||
'./dist/loadList.json',
|
|
||||||
JSON.stringify(splitResult),
|
|
||||||
'utf-8'
|
|
||||||
);
|
|
||||||
|
|
||||||
// 删除原资源
|
|
||||||
await fs.emptyDir('./dist/project/images');
|
|
||||||
await fs.emptyDir('./dist/project/tilesets');
|
|
||||||
await fs.emptyDir('./dist/project/animates');
|
|
||||||
await fs.emptyDir('./dist/project/fonts');
|
|
||||||
await fs.emptyDir('./dist/project/materials');
|
|
||||||
await fs.emptyDir('./dist/project/sounds');
|
|
||||||
await fs.emptyDir('./dist/project/autotiles');
|
|
||||||
// 然后加入填充内容
|
|
||||||
await fs.copy(
|
|
||||||
'./script/template/.h5data',
|
|
||||||
'./dist/project/images/images.h5data'
|
|
||||||
);
|
|
||||||
await fs.copy(
|
|
||||||
'./script/template/.h5data',
|
|
||||||
'./dist/project/tilesets/tilesets.h5data'
|
|
||||||
);
|
|
||||||
await fs.copy(
|
|
||||||
'./script/template/.h5data',
|
|
||||||
'./dist/project/animates/animates.h5data'
|
|
||||||
);
|
|
||||||
await fs.copy(
|
|
||||||
'./script/template/.h5data',
|
|
||||||
'./dist/project/fonts/fonts.h5data'
|
|
||||||
);
|
|
||||||
await fs.copy(
|
|
||||||
'./script/template/.h5data',
|
|
||||||
'./dist/project/materials/materials.h5data'
|
|
||||||
);
|
|
||||||
await fs.copy(
|
|
||||||
'./script/template/.h5data',
|
|
||||||
'./dist/project/sounds/sounds.h5data'
|
|
||||||
);
|
|
||||||
await fs.copy(
|
|
||||||
'./script/template/.h5data',
|
|
||||||
'./dist/project/autotiles/autotiles.h5data'
|
|
||||||
);
|
|
||||||
}
|
|
39
script/types.ts
Normal file
39
script/types.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
export interface RequiredData {
|
||||||
|
main: {
|
||||||
|
floorIds: string[];
|
||||||
|
images: string[];
|
||||||
|
tilesets: string[];
|
||||||
|
animates: string[];
|
||||||
|
bgms: string[];
|
||||||
|
sounds: string[];
|
||||||
|
fonts: string[];
|
||||||
|
};
|
||||||
|
firstData: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequiredIconsData {
|
||||||
|
autotile: {
|
||||||
|
[x: string]: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResourceUsage =
|
||||||
|
| 'image'
|
||||||
|
| 'tileset'
|
||||||
|
| 'animate'
|
||||||
|
| 'sound'
|
||||||
|
| 'font'
|
||||||
|
| 'autotile'
|
||||||
|
| 'material';
|
||||||
|
|
||||||
|
export type ResourceType =
|
||||||
|
| 'text'
|
||||||
|
| 'buffer'
|
||||||
|
| 'image'
|
||||||
|
| 'material'
|
||||||
|
| 'audio'
|
||||||
|
| 'json'
|
||||||
|
| 'zip'
|
||||||
|
| 'byte';
|
@ -1,13 +1,24 @@
|
|||||||
|
import { createHash } from 'crypto';
|
||||||
|
|
||||||
export function uniqueSymbol() {
|
export function uniqueSymbol() {
|
||||||
return Math.ceil(Math.random() * 0xefffffff + 0x10000000).toString(16);
|
return Math.ceil(Math.random() * 0xefffffff + 0x10000000).toString(16);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatSize(size: number) {
|
export function fileHash(
|
||||||
return size < 1 << 10
|
content: string | Buffer | Uint8Array,
|
||||||
? `${size.toFixed(2)}B`
|
length: number = 8
|
||||||
: size < 1 << 20
|
) {
|
||||||
? `${(size / (1 << 10)).toFixed(2)}KB`
|
return createHash('sha256').update(content).digest('hex').slice(0, length);
|
||||||
: size < 1 << 30
|
}
|
||||||
? `${(size / (1 << 20)).toFixed(2)}MB`
|
|
||||||
: `${(size / (1 << 30)).toFixed(2)}GB`;
|
export function formatSize(size: number) {
|
||||||
|
if (size < 1 << 10) {
|
||||||
|
return `${size.toFixed(2)}B`;
|
||||||
|
} else if (size < 1 << 20) {
|
||||||
|
return `${(size / (1 << 10)).toFixed(2)}KB`;
|
||||||
|
} else if (size < 1 << 30) {
|
||||||
|
return `${(size / (1 << 20)).toFixed(2)}MB`;
|
||||||
|
} else {
|
||||||
|
return `${(size / (1 << 30)).toFixed(2)}GB`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
12
src/App.vue
12
src/App.vue
@ -2,12 +2,16 @@
|
|||||||
<div id="ui">
|
<div id="ui">
|
||||||
<div id="ui-main">
|
<div id="ui-main">
|
||||||
<div id="ui-list">
|
<div id="ui-list">
|
||||||
<div class="ui-one" v-for="(ui, index) of mainUi.stack">
|
<div
|
||||||
|
v-for="(ui, index) of mainUi.stack"
|
||||||
|
:key="index"
|
||||||
|
class="ui-one"
|
||||||
|
>
|
||||||
<component
|
<component
|
||||||
v-if="show(index)"
|
|
||||||
:is="ui.ui.component"
|
:is="ui.ui.component"
|
||||||
v-on="ui.vOn ?? {}"
|
v-if="show(index)"
|
||||||
v-bind="ui.vBind ?? {}"
|
v-bind="ui.vBind ?? {}"
|
||||||
|
v-on="ui.vOn ?? {}"
|
||||||
></component>
|
></component>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -16,8 +20,8 @@
|
|||||||
<template v-for="ui of fixedUi.stack" :key="ui.num">
|
<template v-for="ui of fixedUi.stack" :key="ui.num">
|
||||||
<component
|
<component
|
||||||
:is="ui.ui.component"
|
:is="ui.ui.component"
|
||||||
v-on="ui.vOn ?? {}"
|
|
||||||
v-bind="ui.vBind ?? {}"
|
v-bind="ui.vBind ?? {}"
|
||||||
|
v-on="ui.vOn ?? {}"
|
||||||
></component>
|
></component>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
import './styles.less';
|
import './styles.less';
|
||||||
import { createGame } from '@user/entry-client';
|
import { createGame } from '@user/entry-client';
|
||||||
|
import App from './App.vue';
|
||||||
|
|
||||||
// 创建游戏实例
|
// 创建游戏实例
|
||||||
createGame();
|
createGame();
|
||||||
|
createApp(App).mount('#root');
|
||||||
(async () => {
|
|
||||||
const App = (await import('./App.vue')).default;
|
|
||||||
createApp(App).mount('#root');
|
|
||||||
})();
|
|
||||||
|
|
||||||
main.init('play');
|
main.init('play');
|
||||||
main.listen();
|
main.listen();
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import vue from '@vitejs/plugin-vue';
|
import vue from '@vitejs/plugin-vue';
|
||||||
import components from 'unplugin-vue-components/vite';
|
import components from 'unplugin-vue-components/vite';
|
||||||
import vuejsx from '@vitejs/plugin-vue-jsx';
|
|
||||||
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
|
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
|
||||||
|
import vuejsx from '@vitejs/plugin-vue-jsx';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import postcssPresetEnv from 'postcss-preset-env';
|
import postcssPresetEnv from 'postcss-preset-env';
|
||||||
import * as glob from 'glob';
|
import * as glob from 'glob';
|
||||||
@ -34,11 +34,9 @@ const aliasesUser = glob.sync('packages-user/*/src').map((srcPath) => {
|
|||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
vue({
|
vue(),
|
||||||
customElement: custom
|
|
||||||
}),
|
|
||||||
vuejsx({
|
vuejsx({
|
||||||
isCustomElement: (tag) => {
|
isCustomElement: tag => {
|
||||||
return custom.includes(tag) || tag.startsWith('g-');
|
return custom.includes(tag) || tag.startsWith('g-');
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
Loading…
Reference in New Issue
Block a user