feat: 전투 시뮬레이터를 브라우저 Worker로 이관
서버가 권위 환경과 반복 seed를 준비하고 공용 logic 프로세서를 브라우저와 기존 서버 fallback이 함께 사용하도록 변경한다. production Chromium에서 고정 seed와 1000회 Node 결과 동등성을 검증한다.
This commit is contained in:
@@ -2,6 +2,12 @@ import { expect, test, type Page, type Route } from '@playwright/test';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
processBattleSimJob,
|
||||
type BattleSimJobPayload,
|
||||
type BattleSimRequestPayload,
|
||||
type BattleSimResultPayload,
|
||||
} from '@sammo-ts/logic';
|
||||
import { gameProfile, gameTrpcRoute } from './gameTestPaths.js';
|
||||
|
||||
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
||||
@@ -71,6 +77,67 @@ const simulatorOptions = {
|
||||
],
|
||||
};
|
||||
|
||||
const engineUnitSet: BattleSimJobPayload['unitSet'] = {
|
||||
id: 'playwright',
|
||||
name: 'playwright',
|
||||
crewTypes: [
|
||||
{
|
||||
id: 100,
|
||||
armType: 1,
|
||||
name: '보병',
|
||||
attack: 100,
|
||||
defence: 100,
|
||||
speed: 7,
|
||||
avoid: 10,
|
||||
magicCoef: 0,
|
||||
cost: 9,
|
||||
rice: 9,
|
||||
requirements: [],
|
||||
attackCoef: {},
|
||||
defenceCoef: {},
|
||||
info: [],
|
||||
initSkillTrigger: null,
|
||||
phaseSkillTrigger: null,
|
||||
iActionList: null,
|
||||
},
|
||||
{
|
||||
id: 999,
|
||||
armType: 9,
|
||||
name: '성벽',
|
||||
attack: 0,
|
||||
defence: 0,
|
||||
speed: 1,
|
||||
avoid: 0,
|
||||
magicCoef: 0,
|
||||
cost: 0,
|
||||
rice: 9,
|
||||
requirements: [],
|
||||
attackCoef: {},
|
||||
defenceCoef: {},
|
||||
info: [],
|
||||
initSkillTrigger: null,
|
||||
phaseSkillTrigger: null,
|
||||
iActionList: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const engineConfig: BattleSimJobPayload['config'] = {
|
||||
armPerPhase: 500,
|
||||
maxTrainByCommand: 100,
|
||||
maxAtmosByCommand: 100,
|
||||
maxTrainByWar: 110,
|
||||
maxAtmosByWar: 150,
|
||||
castleCrewTypeId: 999,
|
||||
armTypes: {
|
||||
footman: 1,
|
||||
wizard: 4,
|
||||
siege: 5,
|
||||
misc: 6,
|
||||
castle: 9,
|
||||
},
|
||||
};
|
||||
|
||||
const generalMe = {
|
||||
general: {
|
||||
id: 7,
|
||||
@@ -132,40 +199,25 @@ const importedGeneral = {
|
||||
},
|
||||
};
|
||||
|
||||
const simulationResult = {
|
||||
result: true,
|
||||
reason: 'success',
|
||||
datetime: '205-08',
|
||||
avgWar: 5,
|
||||
phase: 13,
|
||||
killed: 1234,
|
||||
maxKilled: 1400,
|
||||
minKilled: 1100,
|
||||
dead: 432,
|
||||
maxDead: 500,
|
||||
minDead: 400,
|
||||
attackerRice: 321,
|
||||
defenderRice: 654,
|
||||
attackerSkills: { 필살: 2 },
|
||||
defendersSkills: [{ 회피: 1 }],
|
||||
lastWarLog: {
|
||||
generalHistoryLog: '',
|
||||
generalActionLog: '',
|
||||
generalBattleResultLog: '<span>유비가 모의전에서 승리했습니다.</span>',
|
||||
generalBattleDetailLog: '<span>필살 발동, 피해 1,234</span>',
|
||||
nationalHistoryLog: '',
|
||||
globalHistoryLog: '',
|
||||
globalActionLog: '',
|
||||
},
|
||||
};
|
||||
|
||||
type Fixture = {
|
||||
hasGeneral: boolean;
|
||||
failNextSimulation?: boolean;
|
||||
queueFirst?: boolean;
|
||||
pollingCount: number;
|
||||
requests: string[];
|
||||
simulationPayloads: unknown[];
|
||||
preparedPayloads: BattleSimJobPayload[];
|
||||
serverResults: BattleSimResultPayload[];
|
||||
};
|
||||
|
||||
const readOperationInput = (
|
||||
requestBody: Record<string, unknown>,
|
||||
operationCount: number,
|
||||
operationIndex: number
|
||||
): unknown => {
|
||||
const rawPayload = requestBody[String(operationIndex)] ?? (operationCount === 1 ? requestBody : undefined);
|
||||
if (!rawPayload || typeof rawPayload !== 'object') {
|
||||
return rawPayload;
|
||||
}
|
||||
const payload = rawPayload as { json?: unknown; input?: { json?: unknown } };
|
||||
return payload.json ?? payload.input?.json ?? rawPayload;
|
||||
};
|
||||
|
||||
const installImages = async (page: Page) => {
|
||||
@@ -182,6 +234,22 @@ const installImages = async (page: Page) => {
|
||||
|
||||
const installApi = async (page: Page, fixture: Fixture) => {
|
||||
await installImages(page);
|
||||
await page.addInitScript(() => {
|
||||
const nativeWorker = window.Worker;
|
||||
const testWindow = window as unknown as {
|
||||
__battleWorkerResponses: unknown[];
|
||||
__battleWorkerUrls: string[];
|
||||
};
|
||||
testWindow.__battleWorkerResponses = [];
|
||||
testWindow.__battleWorkerUrls = [];
|
||||
window.Worker = class TrackedWorker extends nativeWorker {
|
||||
constructor(scriptURL: string | URL, options?: WorkerOptions) {
|
||||
super(scriptURL, options);
|
||||
testWindow.__battleWorkerUrls.push(String(scriptURL));
|
||||
this.addEventListener('message', (event) => testWindow.__battleWorkerResponses.push(event.data));
|
||||
}
|
||||
};
|
||||
});
|
||||
await page.addInitScript((profile) => {
|
||||
window.localStorage.setItem('sammo-game-token', 'ga_battle_sim_playwright');
|
||||
window.localStorage.setItem('sammo-game-profile', profile);
|
||||
@@ -212,36 +280,29 @@ const installApi = async (page: Page, fixture: Fixture) => {
|
||||
});
|
||||
}
|
||||
if (operation === 'battle.getGeneralDetail') return response(importedGeneral);
|
||||
if (operation === 'battle.simulate') {
|
||||
const rawPayload =
|
||||
requestBody[String(operationIndex)] ?? (operations.length === 1 ? requestBody : undefined);
|
||||
const payload =
|
||||
rawPayload && typeof rawPayload === 'object'
|
||||
? (rawPayload as {
|
||||
json?: unknown;
|
||||
input?: { json?: unknown };
|
||||
})
|
||||
: undefined;
|
||||
fixture.simulationPayloads.push(payload?.json ?? payload?.input?.json ?? rawPayload);
|
||||
if (operation === 'battle.prepareSimulation') {
|
||||
if (fixture.failNextSimulation) {
|
||||
fixture.failNextSimulation = false;
|
||||
return errorResponse(operation, '시뮬레이터 입력 오류');
|
||||
}
|
||||
if (fixture.queueFirst) {
|
||||
return response({ status: 'queued', jobId: 'job-playwright' });
|
||||
}
|
||||
return response({ status: 'completed', jobId: 'job-playwright', payload: simulationResult });
|
||||
}
|
||||
if (operation === 'battle.getSimulation') {
|
||||
fixture.pollingCount += 1;
|
||||
if (fixture.pollingCount === 1) {
|
||||
return response({ status: 'queued', jobId: 'job-playwright' });
|
||||
}
|
||||
return response({
|
||||
status: 'completed',
|
||||
jobId: 'job-playwright',
|
||||
payload: simulationResult,
|
||||
});
|
||||
const request = readOperationInput(
|
||||
requestBody,
|
||||
operations.length,
|
||||
operationIndex
|
||||
) as BattleSimRequestPayload;
|
||||
const prepared: BattleSimJobPayload = {
|
||||
...request,
|
||||
seeds: request.seed
|
||||
? []
|
||||
: Array.from({ length: request.repeatCnt }, (_, index) => `playwright-repeat-${index}`),
|
||||
unitSet: engineUnitSet,
|
||||
config: engineConfig,
|
||||
time: { year: request.year, month: request.month, startYear: 190 },
|
||||
scenarioEffect: null,
|
||||
};
|
||||
fixture.preparedPayloads.push(prepared);
|
||||
fixture.serverResults.push(processBattleSimJob(structuredClone(prepared)));
|
||||
return response(prepared);
|
||||
}
|
||||
return errorResponse(operation, `Unhandled battle simulator fixture operation: ${operation}`);
|
||||
});
|
||||
@@ -253,6 +314,26 @@ const installApi = async (page: Page, fixture: Fixture) => {
|
||||
});
|
||||
};
|
||||
|
||||
const readBrowserWorkerResult = async (page: Page, resultIndex: number): Promise<BattleSimResultPayload> => {
|
||||
await expect
|
||||
.poll(async () =>
|
||||
page.evaluate((index) => {
|
||||
const testWindow = window as unknown as { __battleWorkerResponses?: unknown[] };
|
||||
return Boolean(testWindow.__battleWorkerResponses?.[index]);
|
||||
}, resultIndex)
|
||||
)
|
||||
.toBe(true);
|
||||
const response = (await page.evaluate((index) => {
|
||||
const testWindow = window as unknown as { __battleWorkerResponses?: unknown[] };
|
||||
return testWindow.__battleWorkerResponses?.[index];
|
||||
}, resultIndex)) as {
|
||||
ok: boolean;
|
||||
result: BattleSimResultPayload;
|
||||
};
|
||||
expect(response.ok).toBe(true);
|
||||
return response.result;
|
||||
};
|
||||
|
||||
const gotoSimulator = async (page: Page) => {
|
||||
await page.goto('battle-simulator');
|
||||
await expect(page.getByText('전역 설정')).toBeVisible();
|
||||
@@ -263,10 +344,9 @@ const gotoSimulator = async (page: Page) => {
|
||||
test('operates independent/game presets, imports my general, and renders battle logs', async ({ page }) => {
|
||||
const fixture: Fixture = {
|
||||
hasGeneral: true,
|
||||
queueFirst: true,
|
||||
pollingCount: 0,
|
||||
requests: [],
|
||||
simulationPayloads: [],
|
||||
preparedPayloads: [],
|
||||
serverResults: [],
|
||||
};
|
||||
await installApi(page, fixture);
|
||||
await page.setViewportSize({ width: 1280, height: 900 });
|
||||
@@ -295,11 +375,22 @@ test('operates independent/game presets, imports my general, and renders battle
|
||||
await page.getByLabel('시드').fill('playwright-fixed-seed');
|
||||
await battleButton.click();
|
||||
|
||||
await expect(page.getByText('유비가 모의전에서 승리했습니다.')).toBeVisible();
|
||||
await expect(page.getByText('5', { exact: true })).toBeVisible();
|
||||
expect(fixture.pollingCount).toBe(2);
|
||||
expect(fixture.requests).toContain('battle.getSimulation');
|
||||
expect(fixture.simulationPayloads[0]).toMatchObject({
|
||||
await expect(page.getByText('전투를 진행 중입니다.')).toHaveCount(0);
|
||||
expect(await readBrowserWorkerResult(page, 0)).toEqual(fixture.serverResults[0]);
|
||||
for (const [parityId, html] of [
|
||||
['battle-log', fixture.serverResults[0]?.lastWarLog?.generalBattleResultLog ?? ''],
|
||||
['battle-detail-log', fixture.serverResults[0]?.lastWarLog?.generalBattleDetailLog ?? ''],
|
||||
] as const) {
|
||||
const browserNormalizedHtml = await page.evaluate((rawHtml) => {
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = rawHtml;
|
||||
return element.innerHTML;
|
||||
}, html);
|
||||
expect(await page.locator(`[data-parity-id="${parityId}"]`).innerHTML()).toBe(browserNormalizedHtml);
|
||||
}
|
||||
expect(fixture.requests).not.toContain('battle.simulate');
|
||||
expect(fixture.requests).not.toContain('battle.getSimulation');
|
||||
expect(fixture.preparedPayloads[0]).toMatchObject({
|
||||
attackerGeneral: { special: 'che_event_신산' },
|
||||
});
|
||||
|
||||
@@ -318,8 +409,9 @@ test('operates independent/game presets, imports my general, and renders battle
|
||||
await page.locator('.header-actions input[type="file"]').setInputFiles(downloadPath!);
|
||||
|
||||
await battleButton.click();
|
||||
await expect.poll(() => fixture.simulationPayloads.length).toBe(2);
|
||||
expect(fixture.simulationPayloads[1]).toMatchObject({
|
||||
await expect.poll(() => fixture.preparedPayloads.length).toBe(2);
|
||||
expect(await readBrowserWorkerResult(page, 1)).toEqual(fixture.serverResults[1]);
|
||||
expect(fixture.preparedPayloads[1]).toMatchObject({
|
||||
attackerGeneral: { special: 'che_event_신산' },
|
||||
});
|
||||
|
||||
@@ -336,9 +428,9 @@ test('keeps simulation available without a game general and preserves input afte
|
||||
const fixture: Fixture = {
|
||||
hasGeneral: false,
|
||||
failNextSimulation: true,
|
||||
pollingCount: 0,
|
||||
requests: [],
|
||||
simulationPayloads: [],
|
||||
preparedPayloads: [],
|
||||
serverResults: [],
|
||||
};
|
||||
await installApi(page, fixture);
|
||||
await page.setViewportSize({ width: 500, height: 900 });
|
||||
@@ -355,8 +447,9 @@ test('keeps simulation available without a game general and preserves input afte
|
||||
await expect(page.getByLabel('시드')).toHaveValue('keep-this-seed');
|
||||
|
||||
await page.getByRole('button', { name: '전투', exact: true }).click();
|
||||
await expect(page.getByText('유비가 모의전에서 승리했습니다.')).toBeVisible();
|
||||
await expect(page.getByText('전투를 진행 중입니다.')).toHaveCount(0);
|
||||
await expect(page.getByText('시뮬레이터 입력 오류')).toHaveCount(0);
|
||||
expect(await readBrowserWorkerResult(page, 0)).toEqual(fixture.serverResults[0]);
|
||||
|
||||
const notice = page.getByLabel('시뮬레이터 데이터 안내');
|
||||
expect(await notice.evaluate((element) => getComputedStyle(element).position)).toBe('absolute');
|
||||
@@ -370,3 +463,33 @@ test('keeps simulation available without a game general and preserves input afte
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('runs 1000 battles in the Chromium worker and matches the Node processor exactly', async ({ page }) => {
|
||||
test.setTimeout(60_000);
|
||||
const fixture: Fixture = {
|
||||
hasGeneral: false,
|
||||
requests: [],
|
||||
preparedPayloads: [],
|
||||
serverResults: [],
|
||||
};
|
||||
await installApi(page, fixture);
|
||||
await page.setViewportSize({ width: 1280, height: 900 });
|
||||
await gotoSimulator(page);
|
||||
|
||||
await page.getByLabel('반복 횟수').selectOption('1000');
|
||||
await page.getByLabel('시드').fill('');
|
||||
await page.getByRole('button', { name: '전투', exact: true }).click();
|
||||
await expect(page.getByText('전투를 진행 중입니다.')).toHaveCount(0, { timeout: 30_000 });
|
||||
|
||||
expect(fixture.preparedPayloads).toHaveLength(1);
|
||||
expect(fixture.preparedPayloads[0]?.seeds).toHaveLength(1000);
|
||||
expect(new Set(fixture.preparedPayloads[0]?.seeds).size).toBe(1000);
|
||||
expect(await readBrowserWorkerResult(page, 0)).toEqual(fixture.serverResults[0]);
|
||||
expect(fixture.requests).not.toContain('battle.simulate');
|
||||
expect(fixture.requests).not.toContain('battle.getSimulation');
|
||||
const workerUrls = await page.evaluate(() => {
|
||||
const testWindow = window as unknown as { __battleWorkerUrls?: string[] };
|
||||
return testWindow.__battleWorkerUrls ?? [];
|
||||
});
|
||||
expect(workerUrls.some((url) => url.includes('battleSimulator.worker'))).toBe(true);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { BattleSimJobPayload, BattleSimResultPayload } from '@sammo-ts/logic';
|
||||
|
||||
import type { BattleSimulatorWorkerRequest, BattleSimulatorWorkerResponse } from './battleSimulatorWorkerProtocol';
|
||||
|
||||
type PendingRequest = {
|
||||
resolve: (result: BattleSimResultPayload) => void;
|
||||
reject: (error: Error) => void;
|
||||
};
|
||||
|
||||
export class BattleSimulatorWorkerClient {
|
||||
private worker: Worker | null = null;
|
||||
private nextRequestId = 1;
|
||||
private readonly pending = new Map<number, PendingRequest>();
|
||||
|
||||
private ensureWorker(): Worker {
|
||||
if (this.worker) {
|
||||
return this.worker;
|
||||
}
|
||||
|
||||
const worker = new Worker(new URL('../workers/battleSimulator.worker.ts', import.meta.url), {
|
||||
type: 'module',
|
||||
name: 'sammo-battle-simulator',
|
||||
});
|
||||
worker.addEventListener('message', this.handleMessage);
|
||||
worker.addEventListener('error', this.handleWorkerError);
|
||||
this.worker = worker;
|
||||
return worker;
|
||||
}
|
||||
|
||||
private readonly handleMessage = (event: MessageEvent<BattleSimulatorWorkerResponse>): void => {
|
||||
const pending = this.pending.get(event.data.requestId);
|
||||
if (!pending) {
|
||||
return;
|
||||
}
|
||||
this.pending.delete(event.data.requestId);
|
||||
if (event.data.ok) {
|
||||
pending.resolve(event.data.result);
|
||||
return;
|
||||
}
|
||||
pending.reject(new Error(event.data.error));
|
||||
};
|
||||
|
||||
private readonly handleWorkerError = (event: ErrorEvent): void => {
|
||||
const error = new Error(event.message || '전투 시뮬레이션 worker 오류');
|
||||
for (const pending of this.pending.values()) {
|
||||
pending.reject(error);
|
||||
}
|
||||
this.pending.clear();
|
||||
this.disposeWorker();
|
||||
};
|
||||
|
||||
public run(payload: BattleSimJobPayload): Promise<BattleSimResultPayload> {
|
||||
const requestId = this.nextRequestId;
|
||||
this.nextRequestId += 1;
|
||||
const request: BattleSimulatorWorkerRequest = { requestId, payload };
|
||||
const promise = new Promise<BattleSimResultPayload>((resolve, reject) => {
|
||||
this.pending.set(requestId, { resolve, reject });
|
||||
});
|
||||
this.ensureWorker().postMessage(request);
|
||||
return promise;
|
||||
}
|
||||
|
||||
private disposeWorker(): void {
|
||||
if (!this.worker) {
|
||||
return;
|
||||
}
|
||||
this.worker.removeEventListener('message', this.handleMessage);
|
||||
this.worker.removeEventListener('error', this.handleWorkerError);
|
||||
this.worker.terminate();
|
||||
this.worker = null;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
const error = new Error('전투 시뮬레이션이 취소되었습니다.');
|
||||
for (const pending of this.pending.values()) {
|
||||
pending.reject(error);
|
||||
}
|
||||
this.pending.clear();
|
||||
this.disposeWorker();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { BattleSimJobPayload, BattleSimResultPayload } from '@sammo-ts/logic';
|
||||
|
||||
export interface BattleSimulatorWorkerRequest {
|
||||
requestId: number;
|
||||
payload: BattleSimJobPayload;
|
||||
}
|
||||
|
||||
export type BattleSimulatorWorkerResponse =
|
||||
| {
|
||||
requestId: number;
|
||||
ok: true;
|
||||
result: BattleSimResultPayload;
|
||||
}
|
||||
| {
|
||||
requestId: number;
|
||||
ok: false;
|
||||
error: string;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
|
||||
import type { BattleSimRequestPayload, BattleSimResultPayload } from '@sammo-ts/game-api';
|
||||
import PanelCard from '../components/ui/PanelCard.vue';
|
||||
import SkeletonLines from '../components/ui/SkeletonLines.vue';
|
||||
@@ -7,6 +7,7 @@ import BattleGeneralCard from '../components/battle/BattleGeneralCard.vue';
|
||||
import { trpc } from '../utils/trpc';
|
||||
import { getNpcColor } from '../utils/npcColor';
|
||||
import type { BattleSimOptions, GeneralDraft, InheritBuff } from '../utils/battleSimulatorTypes';
|
||||
import { BattleSimulatorWorkerClient } from '../utils/battleSimulatorWorkerClient';
|
||||
|
||||
type GeneralExport = Omit<GeneralDraft, 'id'>;
|
||||
|
||||
@@ -67,6 +68,11 @@ const defenders = ref<GeneralDraft[]>([]);
|
||||
|
||||
const isSimulating = ref(false);
|
||||
const statusMessage = ref<string | null>(null);
|
||||
const simulationWorker = new BattleSimulatorWorkerClient();
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
simulationWorker.dispose();
|
||||
});
|
||||
|
||||
const importOpen = ref(false);
|
||||
const importTarget = ref<GeneralDraft | null>(null);
|
||||
@@ -567,20 +573,6 @@ const buildBattlePayload = (action: BattleSimRequestPayload['action']): BattleSi
|
||||
};
|
||||
};
|
||||
|
||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const waitForSimulationResult = async (jobId: string): Promise<BattleSimResultPayload> => {
|
||||
const deadline = Date.now() + 15000;
|
||||
while (Date.now() < deadline) {
|
||||
const response = await trpc.battle.getSimulation.query({ jobId });
|
||||
if ('payload' in response && response.payload) {
|
||||
return response.payload;
|
||||
}
|
||||
await delay(800);
|
||||
}
|
||||
throw new Error('simulation_timeout');
|
||||
};
|
||||
|
||||
const runSimulation = async (action: BattleSimRequestPayload['action']) => {
|
||||
if (!options.value) {
|
||||
return;
|
||||
@@ -595,11 +587,8 @@ const runSimulation = async (action: BattleSimRequestPayload['action']) => {
|
||||
|
||||
try {
|
||||
const payload = buildBattlePayload(action);
|
||||
const response = await trpc.battle.simulate.mutate(payload);
|
||||
const result =
|
||||
'payload' in response && response.payload
|
||||
? response.payload
|
||||
: await waitForSimulationResult(response.jobId);
|
||||
const preparedPayload = await trpc.battle.prepareSimulation.mutate(payload);
|
||||
const result = await simulationWorker.run(preparedPayload);
|
||||
|
||||
if (!result.result) {
|
||||
error.value = result.reason || 'battle_failed';
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
import type {
|
||||
BattleSimulatorWorkerRequest,
|
||||
BattleSimulatorWorkerResponse,
|
||||
} from '../utils/battleSimulatorWorkerProtocol';
|
||||
|
||||
const workerScope: DedicatedWorkerGlobalScope = self as DedicatedWorkerGlobalScope;
|
||||
|
||||
const loadProcessor = () => import('@sammo-ts/logic');
|
||||
let processorPromise: ReturnType<typeof loadProcessor> | null = null;
|
||||
|
||||
workerScope.addEventListener('message', async (event: MessageEvent<BattleSimulatorWorkerRequest>) => {
|
||||
const { requestId, payload } = event.data;
|
||||
let response: BattleSimulatorWorkerResponse;
|
||||
try {
|
||||
processorPromise ??= loadProcessor();
|
||||
const { processBattleSimJob } = await processorPromise;
|
||||
response = {
|
||||
requestId,
|
||||
ok: true,
|
||||
result: processBattleSimJob(payload),
|
||||
};
|
||||
} catch (error) {
|
||||
response = {
|
||||
requestId,
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : '전투 시뮬레이션 오류',
|
||||
};
|
||||
}
|
||||
workerScope.postMessage(response);
|
||||
});
|
||||
@@ -33,6 +33,14 @@ export default defineConfig(({ mode }) => {
|
||||
build: {
|
||||
sourcemap: true,
|
||||
},
|
||||
worker: {
|
||||
format: 'es',
|
||||
rolldownOptions: {
|
||||
output: {
|
||||
codeSplitting: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(import.meta.dirname, './src'),
|
||||
|
||||
Reference in New Issue
Block a user