@coze-arch/cli 0.0.35 → 0.0.36-alpha.1b252b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +11 -0
  2. package/docs/deploy.md +164 -0
  3. package/lib/__templates__/nextjs/template.config.js +3 -1
  4. package/lib/__templates__/phaser/.coze +13 -0
  5. package/lib/__templates__/phaser/README.md +37 -0
  6. package/lib/__templates__/phaser/_gitignore +23 -0
  7. package/lib/__templates__/phaser/_npmrc +14 -0
  8. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0001.png +0 -0
  9. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0002.png +0 -0
  10. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0003.png +0 -0
  11. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0004.png +0 -0
  12. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0005.png +0 -0
  13. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0006.png +0 -0
  14. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0007.png +0 -0
  15. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0008.png +0 -0
  16. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0009.png +0 -0
  17. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0010.png +0 -0
  18. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0011.png +0 -0
  19. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0012.png +0 -0
  20. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0013.png +0 -0
  21. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0014.png +0 -0
  22. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0015.png +0 -0
  23. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0016.png +0 -0
  24. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0017.png +0 -0
  25. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0018.png +0 -0
  26. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0019.png +0 -0
  27. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0020.png +0 -0
  28. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0021.png +0 -0
  29. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0022.png +0 -0
  30. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0023.png +0 -0
  31. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0024.png +0 -0
  32. package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/manifest.json +11 -0
  33. package/lib/__templates__/phaser/debug-runtime/flow-graph.json +20 -0
  34. package/lib/__templates__/phaser/index.html +12 -0
  35. package/lib/__templates__/phaser/package.json +31 -0
  36. package/lib/__templates__/phaser/pnpm-lock.yaml +661 -0
  37. package/lib/__templates__/phaser/scripts/build.sh +8 -0
  38. package/lib/__templates__/phaser/scripts/dev.sh +82 -0
  39. package/lib/__templates__/phaser/scripts/prepare.sh +7 -0
  40. package/lib/__templates__/phaser/scripts/spawn-detached.cjs +29 -0
  41. package/lib/__templates__/phaser/scripts/start.sh +82 -0
  42. package/lib/__templates__/phaser/scripts/validate.sh +7 -0
  43. package/lib/__templates__/phaser/src/assets.ts +15 -0
  44. package/lib/__templates__/phaser/src/global.d.ts +36 -0
  45. package/lib/__templates__/phaser/src/main.ts +28 -0
  46. package/lib/__templates__/phaser/src/scene/BootScene.ts +209 -0
  47. package/lib/__templates__/phaser/src/scene/DevLoadingScene.ts +66 -0
  48. package/lib/__templates__/phaser/src/style.css +28 -0
  49. package/lib/__templates__/phaser/src/types.ts +32 -0
  50. package/lib/__templates__/phaser/src/utils/asset-loader.ts +319 -0
  51. package/lib/__templates__/phaser/src/utils/index.ts +2 -0
  52. package/lib/__templates__/phaser/src/utils/mark-editable.ts +6 -0
  53. package/lib/__templates__/phaser/template.config.js +43 -0
  54. package/lib/__templates__/phaser/tsconfig.json +23 -0
  55. package/lib/__templates__/phaser/vite.config.ts +96 -0
  56. package/lib/__templates__/templates.json +25 -0
  57. package/lib/__templates__/vite/template.config.js +3 -1
  58. package/lib/cli.js +3164 -201
  59. package/lib/deploy/package-project.js +220 -0
  60. package/lib/deploy/package-project.py +165 -0
  61. package/package.json +5 -4
@@ -0,0 +1,82 @@
1
+ #!/bin/bash
2
+ set -Eeuo pipefail
3
+
4
+ COZE_WORKSPACE_PATH="${COZE_WORKSPACE_PATH:-$(pwd)}"
5
+ PORT="${PORT:-<%= port %>}"
6
+ DEPLOY_RUN_PORT="${DEPLOY_RUN_PORT:-${PORT}}"
7
+ cd "${COZE_WORKSPACE_PATH}"
8
+
9
+ get_listening_pids() {
10
+ local port="$1"
11
+
12
+ if command -v lsof >/dev/null 2>&1; then
13
+ lsof -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null || true
14
+ elif command -v ss >/dev/null 2>&1; then
15
+ ss -H -lntp 2>/dev/null \
16
+ | awk -v port="${port}" '$4 ~ ":"port"$"' \
17
+ | grep -o 'pid=[0-9]*' \
18
+ | cut -d= -f2 \
19
+ | sort -u || true
20
+ else
21
+ echo "Warning: neither lsof nor ss found, cannot check port ${port}." >&2
22
+ fi
23
+ }
24
+
25
+ kill_port_if_listening() {
26
+ local port="$1"
27
+ local pids
28
+
29
+ pids="$(get_listening_pids "${port}")"
30
+ if [[ -z "${pids}" ]]; then
31
+ echo "Port ${port} is free."
32
+ return
33
+ fi
34
+
35
+ echo "Port ${port} is in use by PIDs: ${pids//$'\n'/ } (SIGKILL)"
36
+ echo "${pids}" | xargs kill -9 2>/dev/null || true
37
+ sleep 1
38
+
39
+ pids="$(get_listening_pids "${port}")"
40
+ if [[ -n "${pids}" ]]; then
41
+ echo "Failed to clear port ${port}; remaining PIDs: ${pids//$'\n'/ }." >&2
42
+ return 1
43
+ fi
44
+
45
+ echo "Port ${port} cleared."
46
+ }
47
+
48
+ echo "Clearing port ${DEPLOY_RUN_PORT} before start."
49
+ kill_port_if_listening "${DEPLOY_RUN_PORT}"
50
+
51
+ <% if (process.env.NODE_ENV === 'test') { %>
52
+ # Keep the process attached in tests so the test runner can collect logs and stop it.
53
+ exec env COZE_PHASER_GAME_ENV=DEV pnpm vite \
54
+ --port "${DEPLOY_RUN_PORT}" \
55
+ --host 127.0.0.1 \
56
+ --strictPort
57
+ <% } else { %>
58
+ LOG_DIR="${COZE_WORKSPACE_PATH}/logs"
59
+ LOG_FILE="${LOG_DIR}/phaser-dev.log"
60
+ PID_FILE="${LOG_DIR}/phaser-dev.pid"
61
+ mkdir -p "${LOG_DIR}"
62
+
63
+ echo "Starting Phaser dev server on port ${DEPLOY_RUN_PORT}..."
64
+ server_pid="$(COZE_PHASER_GAME_ENV=DEV node scripts/spawn-detached.cjs \
65
+ "${LOG_FILE}" pnpm vite \
66
+ --port "${DEPLOY_RUN_PORT}" \
67
+ --host 127.0.0.1 \
68
+ --strictPort)"
69
+ echo "${server_pid}" > "${PID_FILE}"
70
+
71
+ sleep 1
72
+ if ! kill -0 "${server_pid}" 2>/dev/null; then
73
+ echo "Phaser dev server failed to start. See ${LOG_FILE}." >&2
74
+ tail -n 20 "${LOG_FILE}" >&2 || true
75
+ rm -f "${PID_FILE}"
76
+ exit 1
77
+ fi
78
+
79
+ echo "Phaser dev server started (PID: ${server_pid})."
80
+ echo "Log file: ${LOG_FILE}"
81
+ echo "PID file: ${PID_FILE}"
82
+ <% } %>
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -Eeuo pipefail
3
+
4
+ COZE_WORKSPACE_PATH="${COZE_WORKSPACE_PATH:-$(pwd)}"
5
+ cd "${COZE_WORKSPACE_PATH}"
6
+
7
+ pnpm install --prefer-frozen-lockfile --prefer-offline --reporter=append-only
@@ -0,0 +1,29 @@
1
+ const fs = require('node:fs');
2
+ const { spawn } = require('node:child_process');
3
+
4
+ const [logFile, command, ...args] = process.argv.slice(2);
5
+
6
+ if (!logFile || !command) {
7
+ process.stderr.write(
8
+ 'Usage: node scripts/spawn-detached.cjs <log-file> <command> [...args]\n',
9
+ );
10
+ process.exit(1);
11
+ }
12
+
13
+ const logFd = fs.openSync(logFile, 'a');
14
+ const child = spawn(command, args, {
15
+ cwd: process.cwd(),
16
+ detached: process.platform !== 'win32',
17
+ env: process.env,
18
+ stdio: ['ignore', logFd, logFd],
19
+ });
20
+
21
+ fs.closeSync(logFd);
22
+ child.unref();
23
+
24
+ if (!child.pid) {
25
+ process.stderr.write('Failed to start detached process.\n');
26
+ process.exit(1);
27
+ }
28
+
29
+ process.stdout.write(String(child.pid));
@@ -0,0 +1,82 @@
1
+ #!/bin/bash
2
+ set -Eeuo pipefail
3
+
4
+ COZE_WORKSPACE_PATH="${COZE_WORKSPACE_PATH:-$(pwd)}"
5
+ PORT="${PORT:-<%= port %>}"
6
+ DEPLOY_RUN_PORT="${DEPLOY_RUN_PORT:-${PORT}}"
7
+ cd "${COZE_WORKSPACE_PATH}"
8
+
9
+ get_listening_pids() {
10
+ local port="$1"
11
+
12
+ if command -v lsof >/dev/null 2>&1; then
13
+ lsof -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null || true
14
+ elif command -v ss >/dev/null 2>&1; then
15
+ ss -H -lntp 2>/dev/null \
16
+ | awk -v port="${port}" '$4 ~ ":"port"$"' \
17
+ | grep -o 'pid=[0-9]*' \
18
+ | cut -d= -f2 \
19
+ | sort -u || true
20
+ else
21
+ echo "Warning: neither lsof nor ss found, cannot check port ${port}." >&2
22
+ fi
23
+ }
24
+
25
+ kill_port_if_listening() {
26
+ local port="$1"
27
+ local pids
28
+
29
+ pids="$(get_listening_pids "${port}")"
30
+ if [[ -z "${pids}" ]]; then
31
+ echo "Port ${port} is free."
32
+ return
33
+ fi
34
+
35
+ echo "Port ${port} is in use by PIDs: ${pids//$'\n'/ } (SIGKILL)"
36
+ echo "${pids}" | xargs kill -9 2>/dev/null || true
37
+ sleep 1
38
+
39
+ pids="$(get_listening_pids "${port}")"
40
+ if [[ -n "${pids}" ]]; then
41
+ echo "Failed to clear port ${port}; remaining PIDs: ${pids//$'\n'/ }." >&2
42
+ return 1
43
+ fi
44
+
45
+ echo "Port ${port} cleared."
46
+ }
47
+
48
+ echo "Clearing port ${DEPLOY_RUN_PORT} before start."
49
+ kill_port_if_listening "${DEPLOY_RUN_PORT}"
50
+
51
+ <% if (process.env.NODE_ENV === 'test') { %>
52
+ # Keep the process attached in tests so the test runner can collect logs and stop it.
53
+ exec env COZE_PHASER_GAME_ENV=PROD pnpm vite preview \
54
+ --port "${DEPLOY_RUN_PORT}" \
55
+ --host 127.0.0.1 \
56
+ --strictPort
57
+ <% } else { %>
58
+ LOG_DIR="${COZE_WORKSPACE_PATH}/logs"
59
+ LOG_FILE="${LOG_DIR}/phaser-start.log"
60
+ PID_FILE="${LOG_DIR}/phaser-start.pid"
61
+ mkdir -p "${LOG_DIR}"
62
+
63
+ echo "Starting Phaser production server on port ${DEPLOY_RUN_PORT}..."
64
+ server_pid="$(COZE_PHASER_GAME_ENV=PROD node scripts/spawn-detached.cjs \
65
+ "${LOG_FILE}" pnpm vite preview \
66
+ --port "${DEPLOY_RUN_PORT}" \
67
+ --host 127.0.0.1 \
68
+ --strictPort)"
69
+ echo "${server_pid}" > "${PID_FILE}"
70
+
71
+ sleep 1
72
+ if ! kill -0 "${server_pid}" 2>/dev/null; then
73
+ echo "Phaser production server failed to start. See ${LOG_FILE}." >&2
74
+ tail -n 20 "${LOG_FILE}" >&2 || true
75
+ rm -f "${PID_FILE}"
76
+ exit 1
77
+ fi
78
+
79
+ echo "Phaser production server started (PID: ${server_pid})."
80
+ echo "Log file: ${LOG_FILE}"
81
+ echo "PID file: ${PID_FILE}"
82
+ <% } %>
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -Eeuo pipefail
3
+
4
+ COZE_WORKSPACE_PATH="${COZE_WORKSPACE_PATH:-$(pwd)}"
5
+ cd "${COZE_WORKSPACE_PATH}"
6
+
7
+ pnpm validate
@@ -0,0 +1,15 @@
1
+ export const IMAGES: Record<string, string> = {};
2
+
3
+ export const IMAGE_SEQUENCES: Record<string, string> = {
4
+ 'coze-game-loading': 'assets/image_sequence/coze-game-loading',
5
+ };
6
+
7
+ export const VIDEOS: Record<string, string> = {};
8
+
9
+ export const AUDIOS: Record<string, string> = {};
10
+
11
+ // 注册素材到 PhaserBridge,可以在扣子网页游戏开发中定位
12
+ window.PhaserBridge?.markImageAssetMap?.(IMAGES);
13
+ window.PhaserBridge?.markImageSequenceAssetMap?.(IMAGE_SEQUENCES);
14
+ window.PhaserBridge?.markVideoAssetMap?.(VIDEOS);
15
+ window.PhaserBridge?.markAudioAssetMap?.(AUDIOS);
@@ -0,0 +1,36 @@
1
+ import type Phaser from 'phaser';
2
+
3
+ declare global {
4
+ interface LaunchSpec {
5
+ protocol_version: 1;
6
+ target: {
7
+ graph_id: string;
8
+ node_id: string;
9
+ };
10
+ input: Record<string, any>;
11
+ }
12
+
13
+ interface PhaserBridgeBinding {
14
+ launchGame: (launchSpec: LaunchSpec | undefined) => Phaser.Game;
15
+ }
16
+
17
+ interface PhaserBridgeApi {
18
+ bind: (binding: PhaserBridgeBinding) => Phaser.Game;
19
+ markEditable?: <T extends Phaser.GameObjects.GameObject>(
20
+ stableId: string,
21
+ gameObject: T,
22
+ ) => T;
23
+ markImageAssetMap?: (images: Record<string, string>) => void;
24
+ markImageSequenceAssetMap?: (
25
+ imageSequences: Record<string, string>,
26
+ ) => void;
27
+ markVideoAssetMap?: (videos: Record<string, string>) => void;
28
+ markAudioAssetMap?: (audios: Record<string, string>) => void;
29
+ }
30
+
31
+ interface Window {
32
+ PhaserBridge?: PhaserBridgeApi;
33
+ }
34
+ }
35
+
36
+ export {};
@@ -0,0 +1,28 @@
1
+ import Phaser from 'phaser';
2
+
3
+ import { BootScene } from './scene/BootScene';
4
+ import { DevLoadingScene } from './scene/DevLoadingScene';
5
+ import './style.css';
6
+
7
+ const createGame = (_launchSpec?: LaunchSpec): Phaser.Game =>
8
+ new Phaser.Game({
9
+ type: Phaser.AUTO,
10
+ pixelArt: true,
11
+ parent: 'game',
12
+ // TODO 根据游戏的要求进行尺寸
13
+ width: 540,
14
+ height: 960,
15
+ backgroundColor: '#ffffff',
16
+ scale: {
17
+ mode: Phaser.Scale.FIT,
18
+ autoCenter: Phaser.Scale.CENTER_BOTH,
19
+ },
20
+ scene: [BootScene, DevLoadingScene],
21
+ });
22
+
23
+ // 注册游戏到 PhaserBridge,可以在扣子网页游戏开发中编辑评论
24
+ if (window.PhaserBridge) {
25
+ window.PhaserBridge.bind({ launchGame: createGame });
26
+ } else {
27
+ createGame();
28
+ }
@@ -0,0 +1,209 @@
1
+ import Phaser from 'phaser';
2
+
3
+ import {
4
+ getAssetLabelFromFileKey,
5
+ getImageSequenceRuntimeKeys,
6
+ loadAssets,
7
+ markEditable,
8
+ registerImageSequenceAnimations,
9
+ } from '@/utils';
10
+
11
+ import { AUDIOS, IMAGES, IMAGE_SEQUENCES, VIDEOS } from '../assets';
12
+
13
+ interface LoaderFileLike {
14
+ key: string;
15
+ }
16
+
17
+ const LOADING_TEXT_STYLE: Phaser.Types.GameObjects.Text.TextStyle = {
18
+ color: '#71717a',
19
+ fontFamily: '"PingFang SC", "Microsoft YaHei", sans-serif',
20
+ fontSize: '18px',
21
+ };
22
+
23
+ const DEV_LOADING_ASSET_KEY = 'coze-game-loading';
24
+ const devLoadingSource = IMAGE_SEQUENCES[DEV_LOADING_ASSET_KEY];
25
+
26
+ if (!devLoadingSource) {
27
+ throw new Error(`Image sequence asset not found: ${DEV_LOADING_ASSET_KEY}`);
28
+ }
29
+
30
+ const BOOT_IMAGE_SEQUENCES = {
31
+ [DEV_LOADING_ASSET_KEY]: devLoadingSource,
32
+ };
33
+
34
+ const BOOT_ASSETS = {
35
+ imageSequences: BOOT_IMAGE_SEQUENCES,
36
+ };
37
+
38
+ const GAME_IMAGE_SEQUENCES = Object.fromEntries(
39
+ Object.entries(IMAGE_SEQUENCES).filter(
40
+ ([assetKey]) => assetKey !== DEV_LOADING_ASSET_KEY,
41
+ ),
42
+ );
43
+
44
+ const GAME_ASSETS = {
45
+ images: IMAGES,
46
+ imageSequences: GAME_IMAGE_SEQUENCES,
47
+ videos: VIDEOS,
48
+ audios: AUDIOS,
49
+ };
50
+
51
+ export const DEV_LOADING_RUNTIME_KEYS =
52
+ getImageSequenceRuntimeKeys(DEV_LOADING_ASSET_KEY);
53
+
54
+ export class BootScene extends Phaser.Scene {
55
+ private loadingText?: Phaser.GameObjects.Text;
56
+ private progressBar?: Phaser.GameObjects.Graphics;
57
+ private progressText?: Phaser.GameObjects.Text;
58
+ private loadFailed = false;
59
+
60
+ constructor() {
61
+ super('boot');
62
+ }
63
+
64
+ preload(): void {
65
+ loadAssets(this, BOOT_ASSETS);
66
+ }
67
+
68
+ create(): void {
69
+ registerImageSequenceAnimations(this, BOOT_IMAGE_SEQUENCES);
70
+ this.createLoadingView();
71
+
72
+ const queuedFileCount = loadAssets(this, GAME_ASSETS);
73
+ if (queuedFileCount === 0) {
74
+ this.finishLoading();
75
+ return;
76
+ }
77
+
78
+ this.load.on(Phaser.Loader.Events.FILE_PROGRESS, this.onGameFile, this);
79
+ this.load.on(Phaser.Loader.Events.PROGRESS, this.onProgress, this);
80
+ this.load.once(
81
+ Phaser.Loader.Events.FILE_LOAD_ERROR,
82
+ this.onLoadError,
83
+ this,
84
+ );
85
+ this.load.once(Phaser.Loader.Events.COMPLETE, this.finishLoading, this);
86
+ this.load.start();
87
+ }
88
+
89
+ private createLoadingView(): void {
90
+ const { centerX, centerY } = this.cameras.main;
91
+
92
+ markEditable(
93
+ 'boot.loading-mascot',
94
+ this.add
95
+ .sprite(centerX, centerY - 58, DEV_LOADING_RUNTIME_KEYS.texture)
96
+ .setScale(0.56)
97
+ .play(DEV_LOADING_RUNTIME_KEYS.animation),
98
+ );
99
+
100
+ this.loadingText = markEditable(
101
+ 'boot.loading-text',
102
+ this.add
103
+ .text(
104
+ centerX,
105
+ centerY + 120,
106
+ '正在加载游戏资源…',
107
+ LOADING_TEXT_STYLE,
108
+ )
109
+ .setOrigin(0.5),
110
+ );
111
+
112
+ this.progressBar = markEditable(
113
+ 'boot.loading-progress-bar',
114
+ this.add.graphics(),
115
+ );
116
+ this.renderProgressBar(0);
117
+
118
+ this.progressText = markEditable(
119
+ 'boot.loading-progress',
120
+ this.add
121
+ .text(centerX, centerY + 190, '0%', LOADING_TEXT_STYLE)
122
+ .setOrigin(0.5),
123
+ );
124
+ }
125
+
126
+ private readonly onGameFile = (file: LoaderFileLike): void => {
127
+ const assetLabel = getAssetLabelFromFileKey(file.key, GAME_ASSETS);
128
+ this.loadingText?.setText(`正在加载${assetLabel}`);
129
+ };
130
+
131
+ private readonly onProgress = (progress: number): void => {
132
+ this.renderProgressBar(progress);
133
+ this.progressText?.setText(`${Math.round(progress * 100)}%`);
134
+ };
135
+
136
+ private readonly onLoadError = (file: LoaderFileLike): void => {
137
+ this.loadFailed = true;
138
+ const assetLabel = getAssetLabelFromFileKey(file.key, GAME_ASSETS);
139
+ this.loadingText?.setText(`${assetLabel}加载失败`);
140
+ };
141
+
142
+ private renderProgressBar(progress: number): void {
143
+ if (!this.progressBar) {
144
+ return;
145
+ }
146
+
147
+ const { centerX, centerY } = this.cameras.main;
148
+ const width = 300;
149
+ const height = 14;
150
+ const x = centerX - width / 2;
151
+ const y = centerY + 154;
152
+ const padding = 3;
153
+ const innerWidth = width - padding * 2;
154
+ const filledWidth = Phaser.Math.Clamp(progress, 0, 1) * innerWidth;
155
+
156
+ this.progressBar.clear();
157
+ this.progressBar.fillStyle(0x102536, 0.12);
158
+ this.progressBar.fillRoundedRect(x, y, width, height, height / 2);
159
+ this.progressBar.lineStyle(2, 0x102536, 0.28);
160
+ this.progressBar.strokeRoundedRect(x, y, width, height, height / 2);
161
+
162
+ if (filledWidth <= 0) {
163
+ return;
164
+ }
165
+
166
+ this.progressBar.fillStyle(0xef648d, 1);
167
+ this.progressBar.fillRoundedRect(
168
+ x + padding,
169
+ y + padding,
170
+ filledWidth,
171
+ height - padding * 2,
172
+ (height - padding * 2) / 2,
173
+ );
174
+
175
+ this.progressBar.fillStyle(0x165ca8, 1);
176
+ this.progressBar.fillCircle(
177
+ Phaser.Math.Clamp(
178
+ x + padding + filledWidth,
179
+ x + padding + 5,
180
+ x + width - padding - 5,
181
+ ),
182
+ y + height / 2,
183
+ 5,
184
+ );
185
+ }
186
+
187
+ private readonly finishLoading = (): void => {
188
+ this.removeLoaderListeners();
189
+
190
+ if (this.loadFailed) {
191
+ this.progressText?.setText('请刷新页面重试');
192
+ return;
193
+ }
194
+
195
+ registerImageSequenceAnimations(this, GAME_IMAGE_SEQUENCES);
196
+ this.scene.start('dev-loading');
197
+ };
198
+
199
+ private removeLoaderListeners(): void {
200
+ this.load.off(Phaser.Loader.Events.FILE_PROGRESS, this.onGameFile, this);
201
+ this.load.off(Phaser.Loader.Events.PROGRESS, this.onProgress, this);
202
+ this.load.off(
203
+ Phaser.Loader.Events.FILE_LOAD_ERROR,
204
+ this.onLoadError,
205
+ this,
206
+ );
207
+ this.load.off(Phaser.Loader.Events.COMPLETE, this.finishLoading, this);
208
+ }
209
+ }
@@ -0,0 +1,66 @@
1
+ import Phaser from 'phaser';
2
+
3
+ import { markEditable } from '@/utils';
4
+
5
+ import { DEV_LOADING_RUNTIME_KEYS } from './BootScene';
6
+
7
+ /**
8
+ * 本场景用于为兜底场景展示
9
+ */
10
+ export class DevLoadingScene extends Phaser.Scene {
11
+ constructor() {
12
+ super('dev-loading');
13
+ }
14
+
15
+ create(): void {
16
+ const { centerX, centerY } = this.cameras.main;
17
+
18
+ markEditable(
19
+ 'dev-loading.mascot',
20
+ this.add
21
+ .sprite(centerX, centerY - 82, DEV_LOADING_RUNTIME_KEYS.texture)
22
+ .setScale(0.64)
23
+ .play(DEV_LOADING_RUNTIME_KEYS.animation),
24
+ );
25
+
26
+ markEditable(
27
+ 'dev-loading.title',
28
+ this.add
29
+ .text(centerX, centerY + 92, '游戏开发中', {
30
+ color: '#18181b',
31
+ fontFamily: '"PingFang SC", "Microsoft YaHei", sans-serif',
32
+ fontSize: '32px',
33
+ fontStyle: 'bold',
34
+ })
35
+ .setOrigin(0.5),
36
+ );
37
+
38
+ markEditable(
39
+ 'dev-loading.description',
40
+ this.add
41
+ .text(centerX, centerY + 138, '请稍后,游戏世界即将呈现', {
42
+ color: '#71717a',
43
+ fontFamily: '"PingFang SC", "Microsoft YaHei", sans-serif',
44
+ fontSize: '18px',
45
+ })
46
+ .setOrigin(0.5),
47
+ );
48
+
49
+ const loadingDots = Array.from({ length: 3 }, (_value, index) =>
50
+ this.add.circle(centerX + (index - 1) * 18, centerY + 178, 4, 0xf43f75),
51
+ );
52
+
53
+ loadingDots.forEach((dot, index) => {
54
+ this.tweens.add({
55
+ targets: dot,
56
+ alpha: 0.25,
57
+ scale: 0.7,
58
+ duration: 520,
59
+ delay: index * 160,
60
+ ease: 'Sine.inOut',
61
+ repeat: -1,
62
+ yoyo: true,
63
+ });
64
+ });
65
+ }
66
+ }
@@ -0,0 +1,28 @@
1
+ :root {
2
+ color-scheme: dark;
3
+ font-family: Arial, sans-serif;
4
+ background: #000;
5
+ }
6
+
7
+ * {
8
+ box-sizing: border-box;
9
+ }
10
+
11
+ html,
12
+ body,
13
+ #game {
14
+ width: 100%;
15
+ height: 100%;
16
+ margin: 0;
17
+ overflow: hidden;
18
+ background: #000;
19
+ }
20
+
21
+ #game {
22
+ min-width: 0;
23
+ min-height: 0;
24
+ }
25
+
26
+ #game canvas {
27
+ display: block;
28
+ }
@@ -0,0 +1,32 @@
1
+ export type AssetSources = Readonly<Record<string, string>>;
2
+
3
+ export interface AssetCollections {
4
+ images?: AssetSources;
5
+ imageSequences?: AssetSources;
6
+ videos?: AssetSources;
7
+ audios?: AssetSources;
8
+ }
9
+
10
+ export interface ImageSequenceManifest {
11
+ schema_version: string;
12
+ id: string;
13
+ type: 'image_sequence';
14
+ name: string;
15
+ properties: {
16
+ fps: number;
17
+ frame_count: number;
18
+ };
19
+ sources: readonly string[];
20
+ }
21
+
22
+ export type LoadedAsset =
23
+ | {
24
+ kind: 'image' | 'video' | 'audio';
25
+ key: string;
26
+ }
27
+ | {
28
+ kind: 'image-sequence';
29
+ key: string;
30
+ texture: string;
31
+ animation: string;
32
+ };