@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.
- package/README.md +11 -0
- package/docs/deploy.md +164 -0
- package/lib/__templates__/nextjs/template.config.js +3 -1
- package/lib/__templates__/phaser/.coze +13 -0
- package/lib/__templates__/phaser/README.md +37 -0
- package/lib/__templates__/phaser/_gitignore +23 -0
- package/lib/__templates__/phaser/_npmrc +14 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0001.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0002.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0003.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0004.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0005.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0006.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0007.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0008.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0009.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0010.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0011.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0012.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0013.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0014.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0015.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0016.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0017.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0018.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0019.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0020.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0021.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0022.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0023.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/frame_0024.png +0 -0
- package/lib/__templates__/phaser/assets/image_sequence/coze-game-loading/manifest.json +11 -0
- package/lib/__templates__/phaser/debug-runtime/flow-graph.json +20 -0
- package/lib/__templates__/phaser/index.html +12 -0
- package/lib/__templates__/phaser/package.json +31 -0
- package/lib/__templates__/phaser/pnpm-lock.yaml +661 -0
- package/lib/__templates__/phaser/scripts/build.sh +8 -0
- package/lib/__templates__/phaser/scripts/dev.sh +82 -0
- package/lib/__templates__/phaser/scripts/prepare.sh +7 -0
- package/lib/__templates__/phaser/scripts/spawn-detached.cjs +29 -0
- package/lib/__templates__/phaser/scripts/start.sh +82 -0
- package/lib/__templates__/phaser/scripts/validate.sh +7 -0
- package/lib/__templates__/phaser/src/assets.ts +15 -0
- package/lib/__templates__/phaser/src/global.d.ts +36 -0
- package/lib/__templates__/phaser/src/main.ts +28 -0
- package/lib/__templates__/phaser/src/scene/BootScene.ts +209 -0
- package/lib/__templates__/phaser/src/scene/DevLoadingScene.ts +66 -0
- package/lib/__templates__/phaser/src/style.css +28 -0
- package/lib/__templates__/phaser/src/types.ts +32 -0
- package/lib/__templates__/phaser/src/utils/asset-loader.ts +319 -0
- package/lib/__templates__/phaser/src/utils/index.ts +2 -0
- package/lib/__templates__/phaser/src/utils/mark-editable.ts +6 -0
- package/lib/__templates__/phaser/template.config.js +43 -0
- package/lib/__templates__/phaser/tsconfig.json +23 -0
- package/lib/__templates__/phaser/vite.config.ts +96 -0
- package/lib/__templates__/templates.json +25 -0
- package/lib/__templates__/vite/template.config.js +3 -1
- package/lib/cli.js +3164 -201
- package/lib/deploy/package-project.js +220 -0
- package/lib/deploy/package-project.py +165 -0
- 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,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,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
|
+
};
|