Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/webgal/public/game/config.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ Title_img:WebGAL_New_Enter_Image.webp;
Title_bgm:s_Title.mp3;
Game_Logo:WebGalEnter.webp;
Enable_Appreciation:true;
Enable_Editor_Sync:true;
284 changes: 284 additions & 0 deletions packages/webgal/src/Core/initializeScript.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

interface ActiveModuleState {
logger: {
info: ReturnType<typeof vi.fn>;
error: ReturnType<typeof vi.fn>;
};
infoFetcher: ReturnType<typeof vi.fn>;
assetSetter: ReturnType<typeof vi.fn>;
sceneFetcher: ReturnType<typeof vi.fn>;
sceneParser: ReturnType<typeof vi.fn>;
bindExtraFunc: ReturnType<typeof vi.fn>;
startPreviewSyncRuntime: ReturnType<typeof vi.fn>;
uniqWith: ReturnType<typeof vi.fn>;
scenePrefetcher: ReturnType<typeof vi.fn>;
PixiStage: ReturnType<typeof vi.fn>;
axiosGet: ReturnType<typeof vi.fn>;
loadTemplate: ReturnType<typeof vi.fn>;
WebGAL: Record<string, any>;
}

interface InitializeScriptHarness {
infoFetcher: ReturnType<typeof vi.fn>;
startPreviewSyncRuntime: ReturnType<typeof vi.fn>;
resolveGameConfig: (gameConfig: Record<string, unknown>) => void;
sceneFetcherResolve: (rawScene: string) => void;
sceneParser: ReturnType<typeof vi.fn>;
scenePrefetcher: ReturnType<typeof vi.fn>;
WebGAL: Record<string, any>;
}

let activeModuleState: ActiveModuleState | null = null;

function getActiveModuleState(): ActiveModuleState {
if (activeModuleState === null) {
throw new Error('Expected initializeScript test harness to initialize active module state.');
}

return activeModuleState;
}

vi.doMock('./util/logger', () => ({
get logger() {
return getActiveModuleState().logger;
},
}));

vi.doMock('./util/coreInitialFunction/infoFetcher', () => ({
get infoFetcher() {
return getActiveModuleState().infoFetcher;
},
}));

vi.doMock('./util/gameAssetsAccess/assetSetter', () => ({
get assetSetter() {
return getActiveModuleState().assetSetter;
},
fileType: {
scene: 'scene',
},
}));

vi.doMock('./controller/scene/sceneFetcher', () => ({
get sceneFetcher() {
return getActiveModuleState().sceneFetcher;
},
}));

vi.doMock('./parser/sceneParser', () => ({
get sceneParser() {
return getActiveModuleState().sceneParser;
},
}));

vi.doMock('@/Core/util/coreInitialFunction/bindExtraFunc', () => ({
get bindExtraFunc() {
return getActiveModuleState().bindExtraFunc;
},
}));

vi.doMock('@/Core/util/syncWithEditor/previewSyncRuntime', () => ({
get startPreviewSyncRuntime() {
return getActiveModuleState().startPreviewSyncRuntime;
},
}));

vi.doMock('lodash/uniqWith', () => ({
default: (...args: unknown[]) => getActiveModuleState().uniqWith(...args),
}));

vi.doMock('./util/prefetcher/scenePrefetcher', () => ({
get scenePrefetcher() {
return getActiveModuleState().scenePrefetcher;
},
}));

vi.doMock('@/Core/controller/stage/pixi/PixiController', () => ({
default: getActiveModuleState().PixiStage,
}));

vi.doMock('axios', () => ({
default: {
get: (...args: unknown[]) => getActiveModuleState().axiosGet(...args),
},
}));

vi.doMock('@/config/info', () => ({
__INFO: {
version: 'test-version',
},
}));

vi.doMock('@/Core/WebGAL', () => ({
get WebGAL() {
return getActiveModuleState().WebGAL;
},
}));

vi.doMock('@/Core/util/coreInitialFunction/templateLoader', () => ({
get loadTemplate() {
return getActiveModuleState().loadTemplate;
},
}));

function createMockDocument() {
const headElement = {
appendChild: vi.fn(),
};

return {
createElement: vi.fn(() => ({
type: '',
rel: '',
href: '',
})),
getElementsByTagName: vi.fn(() => [headElement]),
};
}

async function flushMicrotasks() {
for (let index = 0; index < 5; index += 1) {
await Promise.resolve();
}
}

async function setupInitializeScriptHarness(): Promise<InitializeScriptHarness> {
vi.resetModules();

const mockDocument = createMockDocument();
let sceneFetcherResolve!: (rawScene: string) => void;
const scenePromise = new Promise<string>((resolve) => {
sceneFetcherResolve = resolve;
});
let resolveGameConfig!: (gameConfig: Record<string, unknown>) => void;
const gameConfigPromise = new Promise<Record<string, unknown>>((resolve) => {
resolveGameConfig = resolve;
});
const parsedScene = {
sceneName: 'start.txt',
subSceneList: ['scene/a.txt', 'scene/a.txt', 'scene/b.txt'],
};

const infoFetcher = vi.fn(() => gameConfigPromise);
const assetSetter = vi.fn(() => 'asset://start.txt');
const sceneFetcher = vi.fn(() => scenePromise);
const sceneParser = vi.fn(() => parsedScene);
const bindExtraFunc = vi.fn();
const startPreviewSyncRuntime = vi.fn();
const uniqWith = vi.fn((list: string[]) => Array.from(new Set(list)));
const scenePrefetcher = vi.fn();
const PixiStage = vi.fn(function MockPixiStage(this: Record<string, unknown>) {
this.kind = 'pixi-stage';
});
const axiosGet = vi.fn(async () => ({
data: [],
}));
const loadTemplate = vi.fn();
const WebGAL = {
sceneManager: {
sceneData: {
currentScene: null,
},
settledScenes: [] as string[],
},
gameplay: {
pixiStage: null,
},
animationManager: {
addAnimation: vi.fn(),
},
};

activeModuleState = {
logger: {
info: vi.fn(),
error: vi.fn(),
},
infoFetcher,
assetSetter,
sceneFetcher,
sceneParser,
bindExtraFunc,
startPreviewSyncRuntime,
uniqWith,
scenePrefetcher,
PixiStage,
axiosGet,
loadTemplate,
WebGAL,
};

vi.stubGlobal('window', {
__WEBGAL_DEVICE_INFO__: {
isIOS: false,
},
innerWidth: 1920,
innerHeight: 1080,
});
vi.stubGlobal('document', mockDocument);
vi.stubGlobal('alert', vi.fn());

const { initializeScript } = await import('./initializeScript');
initializeScript();

return {
infoFetcher,
startPreviewSyncRuntime,
resolveGameConfig,
sceneFetcherResolve,
sceneParser,
scenePrefetcher,
WebGAL,
};
}

describe('initializeScript preview sync bootstrap wiring', () => {
beforeEach(() => {
vi.useFakeTimers();
});

afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
vi.unstubAllGlobals();
activeModuleState = null;
});

it('starts preview sync only after config enables it and the initial scene is ready', async () => {
const harness = await setupInitializeScriptHarness();

expect(harness.infoFetcher).toHaveBeenCalledWith('./game/config.txt');
expect(harness.startPreviewSyncRuntime).not.toHaveBeenCalled();

harness.resolveGameConfig({
Enable_Editor_Sync: true,
});
await flushMicrotasks();

expect(harness.startPreviewSyncRuntime).not.toHaveBeenCalled();

harness.sceneFetcherResolve('; start scene');
await flushMicrotasks();

expect(harness.sceneParser).toHaveBeenCalledWith('; start scene', 'start.txt', 'asset://start.txt');
expect(harness.WebGAL.sceneManager.sceneData.currentScene).toEqual({
sceneName: 'start.txt',
subSceneList: ['scene/a.txt', 'scene/a.txt', 'scene/b.txt'],
});
expect(harness.scenePrefetcher).toHaveBeenCalledWith(['scene/a.txt', 'scene/b.txt']);
expect(harness.startPreviewSyncRuntime).toHaveBeenCalledTimes(1);
});

it('skips preview sync startup when Enable_Editor_Sync is not explicitly enabled', async () => {
const harness = await setupInitializeScriptHarness();

harness.resolveGameConfig({
Enable_Editor_Sync: false,
});
harness.sceneFetcherResolve('; start scene');
await flushMicrotasks();

expect(harness.startPreviewSyncRuntime).not.toHaveBeenCalled();
});
});
21 changes: 16 additions & 5 deletions packages/webgal/src/Core/initializeScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { assetSetter, fileType } from './util/gameAssetsAccess/assetSetter';
import { sceneFetcher } from './controller/scene/sceneFetcher';
import { sceneParser } from './parser/sceneParser';
import { bindExtraFunc } from '@/Core/util/coreInitialFunction/bindExtraFunc';
import { webSocketFunc } from '@/Core/util/syncWithEditor/webSocketFunc';
import { startPreviewSyncRuntime } from '@/Core/util/syncWithEditor/previewSyncRuntime';
import uniqWith from 'lodash/uniqWith';
import { scenePrefetcher } from './util/prefetcher/scenePrefetcher';
import PixiStage from '@/Core/controller/stage/pixi/PixiController';
Expand Down Expand Up @@ -44,19 +44,31 @@ export const initializeScript = (): void => {
loadStyle('./game/userStyleSheet.css');
// 获得 user Animation
getUserAnimation();
// 获取游戏信息
infoFetcher('./game/config.txt');
// 获取start场景
const sceneUrl: string = assetSetter('start.txt', fileType.scene);
// 场景写入到运行时
sceneFetcher(sceneUrl).then((rawScene) => {
const initialSceneReady = sceneFetcher(sceneUrl).then((rawScene) => {
WebGAL.sceneManager.sceneData.currentScene = sceneParser(rawScene, 'start.txt', sceneUrl);
// 开始场景的预加载
const subSceneList = WebGAL.sceneManager.sceneData.currentScene.subSceneList;
WebGAL.sceneManager.settledScenes.push(sceneUrl); // 放入已加载场景列表,避免递归加载相同场景
const subSceneListUniq = uniqWith(subSceneList); // 去重
scenePrefetcher(subSceneListUniq);
});
// 获取游戏信息
const gameConfigReady = infoFetcher('./game/config.txt');
gameConfigReady
.then(async (gameConfig) => {
if (gameConfig.Enable_Editor_Sync !== true) {
return;
}

await initialSceneReady;
startPreviewSyncRuntime();
})
.catch((error) => {
logger.error('启动编辑器同步 V1 runtime 失败', error);
});
/**
* 启动Pixi
*/
Expand All @@ -79,7 +91,6 @@ export const initializeScript = (): void => {
* 绑定工具函数
*/
bindExtraFunc();
webSocketFunc();
};

function loadStyle(url: string) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { syncFast } from '@/Core/util/syncWithEditor/syncWithOrigine';
import { fastForwardToSentence } from '@/Core/util/syncWithEditor/runtime/previewSyncSceneCommand';

export const bindExtraFunc = () => {
(window as any).JMP = syncFast;
(window as any).JMP = fastForwardToSentence;
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import { IGameVar } from '@/store/stageInterface';
* 获取游戏信息
* @param url 游戏信息路径
*/
export const infoFetcher = (url: string) => {
export const infoFetcher = (url: string): Promise<IGameVar> => {
const dispatch = webgalStore.dispatch;
axios.get(url).then(async (r) => {
return axios.get(url).then(async (r) => {
let gameConfigRaw: string = r.data;
let gameConfig = WebgalParser.parseConfig(gameConfigRaw);
logger.info('获取到游戏信息', gameConfig);
Expand Down Expand Up @@ -75,5 +75,7 @@ export const infoFetcher = (url: string) => {
// @ts-expect-error renderPromiseResolve is a global variable
window.renderPromiseResolve();
setStorage();

return gameConfigInit;
});
};
Loading
Loading