mirror of
				https://github.com/unanmed/HumanBreak.git
				synced 2025-11-04 07:02:58 +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' {
 | 
			
		||||
  export interface GlobalComponents {
 | 
			
		||||
    AButton: typeof import('ant-design-vue/es')['Button']
 | 
			
		||||
    ADivider: typeof import('ant-design-vue/es')['Divider']
 | 
			
		||||
    AInput: typeof import('ant-design-vue/es')['Input']
 | 
			
		||||
    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']
 | 
			
		||||
    ASlider: typeof import('ant-design-vue/es')['Slider']
 | 
			
		||||
    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",
 | 
			
		||||
        "ogg-opus-decoder": "^1.6.14",
 | 
			
		||||
        "opus-decoder": "^0.7.7",
 | 
			
		||||
        "vue": "^3.5.17"
 | 
			
		||||
        "vue": "^3.5.20"
 | 
			
		||||
    },
 | 
			
		||||
    "devDependencies": {
 | 
			
		||||
        "@babel/cli": "^7.26.4",
 | 
			
		||||
@ -46,16 +46,18 @@
 | 
			
		||||
        "@rollup/plugin-replace": "^5.0.7",
 | 
			
		||||
        "@rollup/plugin-terser": "^0.4.4",
 | 
			
		||||
        "@rollup/plugin-typescript": "^11.1.6",
 | 
			
		||||
        "@types/archiver": "^6.0.3",
 | 
			
		||||
        "@types/babel__core": "^7.20.5",
 | 
			
		||||
        "@types/express": "^5.0.3",
 | 
			
		||||
        "@types/fontmin": "^0.9.5",
 | 
			
		||||
        "@types/fs-extra": "^9.0.13",
 | 
			
		||||
        "@types/fs-extra": "^11.0.4",
 | 
			
		||||
        "@types/lodash-es": "^4.17.12",
 | 
			
		||||
        "@types/node": "^18.19.80",
 | 
			
		||||
        "@types/ws": "^8.18.0",
 | 
			
		||||
        "@vitejs/plugin-legacy": "^6.0.2",
 | 
			
		||||
        "@vitejs/plugin-vue": "^5.2.3",
 | 
			
		||||
        "@vitejs/plugin-vue-jsx": "^4.1.2",
 | 
			
		||||
        "@vitejs/plugin-vue": "^6.0.1",
 | 
			
		||||
        "@vitejs/plugin-vue-jsx": "^5.1.1",
 | 
			
		||||
        "archiver": "^7.0.1",
 | 
			
		||||
        "chokidar": "^3.6.0",
 | 
			
		||||
        "compressing": "^1.10.1",
 | 
			
		||||
        "concurrently": "^9.1.2",
 | 
			
		||||
@ -64,9 +66,9 @@
 | 
			
		||||
        "eslint-plugin-react": "^7.37.5",
 | 
			
		||||
        "eslint-plugin-vue": "^9.33.0",
 | 
			
		||||
        "express": "^5.1.0",
 | 
			
		||||
        "fontmin": "^0.9.9",
 | 
			
		||||
        "fontmin": "^2.0.3",
 | 
			
		||||
        "form-data": "^4.0.2",
 | 
			
		||||
        "fs-extra": "^10.1.0",
 | 
			
		||||
        "fs-extra": "^11.3.1",
 | 
			
		||||
        "glob": "^11.0.1",
 | 
			
		||||
        "globals": "^15.15.0",
 | 
			
		||||
        "less": "^4.2.2",
 | 
			
		||||
@ -75,13 +77,13 @@
 | 
			
		||||
        "mermaid": "^11.5.0",
 | 
			
		||||
        "postcss-preset-env": "^9.6.0",
 | 
			
		||||
        "prettier": "^3.6.2",
 | 
			
		||||
        "rollup": "^3.29.5",
 | 
			
		||||
        "rollup": "^4.49.0",
 | 
			
		||||
        "terser": "^5.39.0",
 | 
			
		||||
        "tsx": "^4.19.3",
 | 
			
		||||
        "typescript": "^5.8.2",
 | 
			
		||||
        "tsx": "^4.20.5",
 | 
			
		||||
        "typescript": "^5.9.2",
 | 
			
		||||
        "typescript-eslint": "^8.27.0",
 | 
			
		||||
        "unplugin-vue-components": "^0.22.12",
 | 
			
		||||
        "vite": "^6.2.2",
 | 
			
		||||
        "vite": "^6.3.5",
 | 
			
		||||
        "vite-plugin-dts": "^4.5.4",
 | 
			
		||||
        "vitepress": "^1.6.3",
 | 
			
		||||
        "vitepress-plugin-mermaid": "^2.0.17",
 | 
			
		||||
 | 
			
		||||
@ -48,7 +48,7 @@ export interface IAudioDecodeError {
 | 
			
		||||
 | 
			
		||||
export interface IAudioDecodeData {
 | 
			
		||||
    /** 每个声道的音频信息 */
 | 
			
		||||
    channelData: Float32Array[];
 | 
			
		||||
    channelData: Float32Array<ArrayBuffer>[];
 | 
			
		||||
    /** 已经被解码的 PCM 采样数 */
 | 
			
		||||
    samplesDecoded: number;
 | 
			
		||||
    /** 音频采样率 */
 | 
			
		||||
@ -163,15 +163,15 @@ export class VorbisDecoder extends AudioDecoder {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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> {
 | 
			
		||||
        return this.decoder?.decodeFile(data);
 | 
			
		||||
        return this.decoder?.decodeFile(data) as Promise<IAudioDecodeData>;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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;
 | 
			
		||||
 | 
			
		||||
    async create(): Promise<void> {
 | 
			
		||||
        this.decoder = new OggOpusDecoderWebWorker();
 | 
			
		||||
        this.decoder = new OggOpusDecoderWebWorker({
 | 
			
		||||
            speechQualityEnhancement: 'none'
 | 
			
		||||
        });
 | 
			
		||||
        await this.decoder.ready;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -188,14 +190,14 @@ export class OpusDecoder extends AudioDecoder {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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> {
 | 
			
		||||
        return this.decoder?.decodeFile(data);
 | 
			
		||||
        return this.decoder?.decodeFile(data) as Promise<IAudioDecodeData>;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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');
 | 
			
		||||
        if (!matrix) return;
 | 
			
		||||
        const trans = this.proj.multiply(this.view).multiply(this.model);
 | 
			
		||||
        matrix.set(false, trans.mat);
 | 
			
		||||
        matrix.set(false, Array.from(trans.mat));
 | 
			
		||||
        this.requestUpdate();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -174,7 +174,7 @@ function onmove(e: MouseEvent) {
 | 
			
		||||
    mat4.rotateX(matrix, matrix, -(dy * 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(${
 | 
			
		||||
        1000 * core.domStyle.scale
 | 
			
		||||
    }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:用户交互的事件的处理
 | 
			
		||||
 | 
			
		||||
@ -360,7 +360,7 @@ core.prototype._loadGameProcess = async function () {
 | 
			
		||||
            if (main.pluginUseCompress) {
 | 
			
		||||
                await main.loadScript(`project/processG.min.js`);
 | 
			
		||||
            } 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() {
 | 
			
		||||
    //------------------------ 用户修改内容 ------------------------//
 | 
			
		||||
 | 
			
		||||
@ -130,18 +130,6 @@ main.prototype.loadScript = function (src, module) {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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) {
 | 
			
		||||
        main.loadSync(mode, callback);
 | 
			
		||||
    } else {
 | 
			
		||||
 | 
			
		||||
@ -1,16 +1,31 @@
 | 
			
		||||
import { build, loadConfigFromFile, mergeConfig, UserConfig } from 'vite';
 | 
			
		||||
import legacy from '@vitejs/plugin-legacy';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
import fs from 'fs-extra';
 | 
			
		||||
import { resolve } from 'path';
 | 
			
		||||
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 目录
 | 
			
		||||
fs.emptyDirSync(outputDir);
 | 
			
		||||
const distDir = resolve(process.cwd(), 'dist');
 | 
			
		||||
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(
 | 
			
		||||
        { command: 'build', mode: 'production' },
 | 
			
		||||
        configFile
 | 
			
		||||
@ -26,14 +41,15 @@ async function buildGame() {
 | 
			
		||||
                    'Opera >= 43'
 | 
			
		||||
                ],
 | 
			
		||||
                polyfills: true,
 | 
			
		||||
                modernPolyfills: true
 | 
			
		||||
                modernPolyfills: true,
 | 
			
		||||
                renderModernChunks: false
 | 
			
		||||
            })
 | 
			
		||||
        ],
 | 
			
		||||
        build: {
 | 
			
		||||
            outDir: outputDir,
 | 
			
		||||
            sourcemap: true,
 | 
			
		||||
            outDir,
 | 
			
		||||
            copyPublicDir: true,
 | 
			
		||||
            rollupOptions: {
 | 
			
		||||
                input: path.resolve('./src/main.ts'),
 | 
			
		||||
                external: ['@wasm-audio-decoders/opus-ml'],
 | 
			
		||||
                output: {
 | 
			
		||||
                    format: 'es',
 | 
			
		||||
                    entryFileNames: '[name].[hash].js',
 | 
			
		||||
@ -47,7 +63,17 @@ async function buildGame() {
 | 
			
		||||
                            'lz-string',
 | 
			
		||||
                            'chart.js',
 | 
			
		||||
                            '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);
 | 
			
		||||
 | 
			
		||||
    await build(resolved);
 | 
			
		||||
    console.log('✅ Game built successfully.');
 | 
			
		||||
    return build({
 | 
			
		||||
        ...resolved,
 | 
			
		||||
        configFile: false
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
buildGame().catch(e => {
 | 
			
		||||
    console.error(e);
 | 
			
		||||
    process.exit(1);
 | 
			
		||||
});
 | 
			
		||||
async function buildData(outDir: string, entry: string) {
 | 
			
		||||
    const configFile = resolve(process.cwd(), 'vite.config.ts');
 | 
			
		||||
    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() {
 | 
			
		||||
    return Math.ceil(Math.random() * 0xefffffff + 0x10000000).toString(16);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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`;
 | 
			
		||||
export function fileHash(
 | 
			
		||||
    content: string | Buffer | Uint8Array,
 | 
			
		||||
    length: number = 8
 | 
			
		||||
) {
 | 
			
		||||
    return createHash('sha256').update(content).digest('hex').slice(0, length);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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-main">
 | 
			
		||||
            <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
 | 
			
		||||
                        v-if="show(index)"
 | 
			
		||||
                        :is="ui.ui.component"
 | 
			
		||||
                        v-on="ui.vOn ?? {}"
 | 
			
		||||
                        v-if="show(index)"
 | 
			
		||||
                        v-bind="ui.vBind ?? {}"
 | 
			
		||||
                        v-on="ui.vOn ?? {}"
 | 
			
		||||
                    ></component>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
@ -16,8 +20,8 @@
 | 
			
		||||
            <template v-for="ui of fixedUi.stack" :key="ui.num">
 | 
			
		||||
                <component
 | 
			
		||||
                    :is="ui.ui.component"
 | 
			
		||||
                    v-on="ui.vOn ?? {}"
 | 
			
		||||
                    v-bind="ui.vBind ?? {}"
 | 
			
		||||
                    v-on="ui.vOn ?? {}"
 | 
			
		||||
                ></component>
 | 
			
		||||
            </template>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,11 @@
 | 
			
		||||
import { createApp } from 'vue';
 | 
			
		||||
import './styles.less';
 | 
			
		||||
import { createGame } from '@user/entry-client';
 | 
			
		||||
import App from './App.vue';
 | 
			
		||||
 | 
			
		||||
// 创建游戏实例
 | 
			
		||||
createGame();
 | 
			
		||||
 | 
			
		||||
(async () => {
 | 
			
		||||
    const App = (await import('./App.vue')).default;
 | 
			
		||||
    createApp(App).mount('#root');
 | 
			
		||||
})();
 | 
			
		||||
createApp(App).mount('#root');
 | 
			
		||||
 | 
			
		||||
main.init('play');
 | 
			
		||||
main.listen();
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import { defineConfig } from 'vite';
 | 
			
		||||
import vue from '@vitejs/plugin-vue';
 | 
			
		||||
import components from 'unplugin-vue-components/vite';
 | 
			
		||||
import vuejsx from '@vitejs/plugin-vue-jsx';
 | 
			
		||||
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
 | 
			
		||||
import vuejsx from '@vitejs/plugin-vue-jsx';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
import postcssPresetEnv from 'postcss-preset-env';
 | 
			
		||||
import * as glob from 'glob';
 | 
			
		||||
@ -34,11 +34,9 @@ const aliasesUser = glob.sync('packages-user/*/src').map((srcPath) => {
 | 
			
		||||
// https://vitejs.dev/config/
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
    plugins: [
 | 
			
		||||
        vue({
 | 
			
		||||
            customElement: custom
 | 
			
		||||
        }),
 | 
			
		||||
        vue(),
 | 
			
		||||
        vuejsx({
 | 
			
		||||
            isCustomElement: (tag) => {
 | 
			
		||||
            isCustomElement: tag => {
 | 
			
		||||
                return custom.includes(tag) || tag.startsWith('g-');
 | 
			
		||||
            }
 | 
			
		||||
        }),
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user