Something went wrong. Try again.
Starship Simulator Controls thoriumsim.com
Something went wrong. Try again.
14 kB · 415 lines
TypeScript
at develop
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416import { ProcessTriggersSystem } from "@thorium/.server/systems/ProcessTriggersSystem";import { ShortRangeCommPowerSystem } from "@thorium/.server/systems/ShortRangeCommPowerSystem";import { TimerSystem } from "@thorium/.server/systems/TimerSystem";import { thoriumContext } from "@thorium/utils/.server/context";import { createMockDataContext, createMockRouter,} from "@thorium/utils/.server/createMockDataContext";import { testDataStoreProps } from "@thorium/utils/.server/db-fs/testDataStoreProps";import { executeBlocks } from "@thorium/utils/.server/executeBlocks";import { measureAudioDurationMs } from "@thorium/utils/.server/ink/measureAudioDuration";import { processTriggers } from "@thorium/utils/.server/processTriggers";import { Entity } from "@thorium/utils/ecs";import { it, aroundEach, expect, vi } from "vitest";
vi.mock("@thorium/utils/.server/ink/measureAudioDuration", async () => { return { measureAudioDurationMs: vi.fn(), };});
aroundEach(async (runTest) => { await thoriumContext.run( { ...testDataStoreProps, async readAsset(asset) { if (asset.endsWith("testInkPath")) return `VAR playerShipName = "Voyager"VAR playerShipId = 1VAR npcShipId = 2VAR conversationId = 3
Ranger: Hello there. This is a test message.* [Crew: Action Time]-> doAction* [Crew: Event Time]-> doEvent* [Crew: Audio Time]-> doAudio
= doActionAction: alertLevel.update shipId: {playerShipId} alertLevel: 2-> DONE
= doEventEvent: alertLevel.update shipId: {playerShipId} eventHandler-> DONE
= eventHandlerRanger: Looks like that worked just fine.-> DONE
= doAudioRanger: Great, let me just play this audio file. # rangerAudio.ogg
Ranger: And then this text line will appear.* [Crew: That's great]-> DONE* [Crew: Glad to hear it]-> DONE
`; return ""; }, }, async () => { await runTest(); }, );});
it("should auto-connect hails if the target is an NPC with a template conversation", async () => { const { dataContext, router, ship1, shortRangeComm1, ship2, shortRangeComm2, conversationTemplate, } = setUpTests();
expect(shortRangeComm1.components.isShortRangeComm?.state).toEqual("idle"); expect(shortRangeComm2.components.isShortRangeComm?.state).toEqual("idle");
shortRangeComm2.updateComponent("isShortRangeComm", { templateConversationId: conversationTemplate.id, });
await router.shortRangeComm.hail({ shipId: ship1.id, targetId: ship2.id }); expect(shortRangeComm1.components.isShortRangeComm?.state).toEqual("hailing"); expect(shortRangeComm1.components.isShortRangeComm?.conversationId).toBeTruthy(); expect(shortRangeComm2.components.isShortRangeComm?.state).toEqual("idle");
const conversation = dataContext.ecs.getEntityById( shortRangeComm1.components.isShortRangeComm?.conversationId || -1, ); expect(conversation?.components.isShortRangeCommConversation?.hostId).toEqual(ship1.id); expect(conversation?.components.isShortRangeCommConversation?.targetId).toEqual(ship2.id);
// Wait for the conversation to connect dataContext.ecs.update(3000 + 4000);
await new Promise((res) => process.nextTick(res)); expect(shortRangeComm1.components.isShortRangeComm?.state).toEqual("connected"); expect(shortRangeComm2.components.isShortRangeComm?.state).toEqual("connected");});it("should auto-reject hails if the target is an NPC with no template conversation", async () => { const { dataContext, router, ship1, shortRangeComm1, ship2, shortRangeComm2 } = setUpTests();
expect(shortRangeComm1.components.isShortRangeComm?.state).toEqual("idle"); expect(shortRangeComm2.components.isShortRangeComm?.state).toEqual("idle");
await router.shortRangeComm.hail({ shipId: ship1.id, targetId: ship2.id });
expect(shortRangeComm1.components.isShortRangeComm?.state).toEqual("hailing"); expect(shortRangeComm1.components.isShortRangeComm?.conversationId).toBeTruthy(); expect(shortRangeComm2.components.isShortRangeComm?.state).toEqual("idle");
// Wait for the conversation to reject dataContext.ecs.update(3000 + 4000);
await new Promise((res) => process.nextTick(res));
expect(shortRangeComm1.components.isShortRangeComm?.state).toEqual("idle"); expect(shortRangeComm2.components.isShortRangeComm?.state).toEqual("idle");});it("should allow another ship to join an existing conversation", async () => { const { dataContext, router, ship1, shortRangeComm1, ship2, shortRangeComm2, ship3, shortRangeComm3, conversationTemplate, } = setUpTests();
shortRangeComm2.updateComponent("isShortRangeComm", { templateConversationId: conversationTemplate.id, }); const { conversationId } = await router.shortRangeComm.hail({ shipId: ship1.id, targetId: ship2.id, allowOtherParticipants: true, }); // Wait for the conversation to connect dataContext.ecs.update(3000 + 4000); await new Promise((res) => process.nextTick(res)); expect(shortRangeComm1.components.isShortRangeComm?.state).toEqual("connected"); expect(shortRangeComm2.components.isShortRangeComm?.state).toEqual("connected"); await router.shortRangeComm.connect({ shipId: ship3.id, conversationId }); expect(shortRangeComm3.components.isShortRangeComm?.state).toEqual("connected"); expect(shortRangeComm3.components.isShortRangeComm?.conversationId).toEqual( shortRangeComm2.components.isShortRangeComm?.conversationId, );});it("should forbid another ship from joining an existing conversation if that conversation does not allow other participants", async () => { const { dataContext, router, ship1, shortRangeComm1, ship2, shortRangeComm2, ship3, shortRangeComm3, conversationTemplate, } = setUpTests();
shortRangeComm2.updateComponent("isShortRangeComm", { templateConversationId: conversationTemplate.id, }); const { conversationId } = await router.shortRangeComm.hail({ shipId: ship1.id, targetId: ship2.id, allowOtherParticipants: false, }); // Wait for the conversation to connect dataContext.ecs.update(3000 + 4000); await new Promise((res) => process.nextTick(res)); expect(shortRangeComm1.components.isShortRangeComm?.state).toEqual("connected"); expect(shortRangeComm2.components.isShortRangeComm?.state).toEqual("connected"); await expect( router.shortRangeComm.connect({ shipId: ship3.id, conversationId }), ).rejects.toThrow();
expect(shortRangeComm3.components.isShortRangeComm?.state).toEqual("idle"); expect(shortRangeComm3.components.isShortRangeComm?.conversationId).toEqual(null);});it("should work when an NPC hails a player ship", async () => { const { router, ship1, shortRangeComm1, ship2, shortRangeComm2, conversationTemplate } = setUpTests();
const { conversationId } = await router.shortRangeComm.hail({ shipId: ship2.id, targetId: ship1.id, conversationTemplateId: conversationTemplate.id, });
expect(shortRangeComm2.components.isShortRangeComm?.state).toEqual("hailing"); expect(shortRangeComm2.components.isShortRangeComm?.conversationId).toEqual(conversationId); expect(shortRangeComm1.components.isShortRangeComm?.state).toEqual("idle");
await router.shortRangeComm.connect({ shipId: ship1.id, conversationId, }); expect(shortRangeComm2.components.isShortRangeComm?.state).toEqual("connected"); expect(shortRangeComm1.components.isShortRangeComm?.state).toEqual("connected");
await router.shortRangeComm.disconnect({ shipId: ship1.id }); expect(shortRangeComm2.components.isShortRangeComm?.state).toEqual("connected"); expect(shortRangeComm1.components.isShortRangeComm?.state).toEqual("idle"); expect(shortRangeComm1.components.isShortRangeComm?.conversationId).toEqual(null);});it("should properly follow an ink script, including triggering actions", async () => { const { dataContext, router, ship1, ship2, conversationTemplate } = setUpTests();
const { conversationId } = await router.shortRangeComm.hail({ shipId: ship2.id, targetId: ship1.id, conversationTemplateId: conversationTemplate.id, }); await router.shortRangeComm.connect({ shipId: ship1.id, conversationId, });
const conversation = dataContext.ecs.getEntityById(conversationId); expect(conversation?.components.isConversation?.currentDialogue[0].text).toEqual( "Hello there. This is a test message.", );
expect(ship1.components.isShip?.alertLevel).toEqual("5"); await router.conversation.selectChoice({ shipId: ship1.id, conversationId, choice: "Crew: Action Time", }); expect(ship1.components.isShip?.alertLevel).toEqual("2");});it("should properly follow an ink script, including event listeners", async () => { const { dataContext, router, ship1, ship2, conversationTemplate } = setUpTests();
const { conversationId } = await router.shortRangeComm.hail({ shipId: ship2.id, targetId: ship1.id, conversationTemplateId: conversationTemplate.id, }); await router.shortRangeComm.connect({ shipId: ship1.id, conversationId, });
const conversation = dataContext.ecs.getEntityById(conversationId); expect(conversation?.components.isConversation?.currentDialogue[0].text).toEqual( "Hello there. This is a test message.", );
await router.conversation.selectChoice({ shipId: ship1.id, conversationId, choice: "Crew: Event Time", });
expect(conversation?.components.isConversation?.currentDialogue[0].text).toEqual( "Hello there. This is a test message.", );
await router.alertLevel.update({ alertLevel: "1", shipId: ship1.id });
await new Promise((res) => process.nextTick(res));
expect(conversation?.components.isConversation?.currentDialogue[1].text).toEqual("Event Time"); expect(conversation?.components.isConversation?.currentDialogue[2].text).toEqual( "Looks like that worked just fine.", );});it("should wait for an audio file to finish playing before continuing the story and mark the chosen choice as chosen until the next choices are available", async () => { const { dataContext, router, ship1, ship2, conversationTemplate } = setUpTests();
const { conversationId } = await router.shortRangeComm.hail({ shipId: ship2.id, targetId: ship1.id, conversationTemplateId: conversationTemplate.id, }); await router.shortRangeComm.connect({ shipId: ship1.id, conversationId, }); const conversation = dataContext.ecs.getEntityById(conversationId); expect(conversation?.components.isConversation?.currentDialogue[0].text).toEqual( "Hello there. This is a test message.", ); expect(conversation?.components.isConversation?.currentChoices[0].text).toEqual( "Crew: Action Time", ); expect(conversation?.components.isConversation?.currentChoices[1].text).toEqual( "Crew: Event Time", ); expect(conversation?.components.isConversation?.currentChoices[2].text).toEqual( "Crew: Audio Time", );
(measureAudioDurationMs as any).mockReturnValue(1000); await router.conversation.selectChoice({ shipId: ship1.id, conversationId, choice: "Crew: Audio Time", }); expect(conversation?.components.isConversation?.currentDialogue[1].text).toEqual("Audio Time"); expect(conversation?.components.isConversation?.currentDialogue[2].text).toEqual( "Great, let me just play this audio file.", ); expect(conversation?.components.isConversation?.currentChoices[2].selected).toEqual(true);
dataContext.ecs.update(1000 + 500);
await new Promise((res) => process.nextTick(res)); expect(conversation?.components.isConversation?.currentDialogue[3].text).toEqual( "And then this text line will appear.", ); expect(conversation?.components.isConversation?.currentChoices[0].text).toEqual( "Crew: That's great", );});
function setUpTests() { const dataContext = createMockDataContext(); dataContext.ecs.executeBlocks = (blocks, blockMetadata) => executeBlocks(dataContext.ecs, blocks, blockMetadata); dataContext.ecs.processTriggers = (events) => processTriggers(dataContext.ecs, events);
const router = createMockRouter(dataContext, { onCall: (opts, result) => { const ecs = dataContext?.ecs; if (!ecs || opts.type !== "send") return;
void processTriggers(ecs, { event: opts.path, values: { ...(opts.rawInput as any), ...(typeof result === "object" && !Array.isArray(result) ? result : {}), }, }); }, });
const conversationTemplate = new Entity(); conversationTemplate.addComponent("isConversationTemplate", { inkFilePath: "testInkPath", }); dataContext.ecs.addEntity(conversationTemplate);
const ship1 = new Entity(); ship1.addComponent("isShip"); ship1.addComponent("isPlayerShip"); ship1.addComponent("position"); dataContext.ecs.addEntity(ship1);
const shortRangeComm1 = new Entity(); shortRangeComm1.addComponent("isShipSystem", { shipId: ship1.id }); shortRangeComm1.addComponent("isShortRangeComm"); dataContext.ecs.addEntity(shortRangeComm1);
ship1.addComponent("shipSystems", { shipSystems: new Map([[shortRangeComm1.id, {}]]), }); const ship2 = new Entity(); ship2.addComponent("isShip"); ship2.addComponent("position"); ship2.addComponent("identity", { name: "Ranger" }); dataContext.ecs.addEntity(ship2);
const shortRangeComm2 = new Entity(); shortRangeComm2.addComponent("isShipSystem", { shipId: ship2.id }); shortRangeComm2.addComponent("isShortRangeComm"); dataContext.ecs.addEntity(shortRangeComm2); ship2.addComponent("shipSystems", { shipSystems: new Map([[shortRangeComm2.id, {}]]), });
const ship3 = new Entity(); ship3.addComponent("isShip"); ship3.addComponent("position"); dataContext.ecs.addEntity(ship3);
const shortRangeComm3 = new Entity(); shortRangeComm3.addComponent("isShipSystem", { shipId: ship3.id }); shortRangeComm3.addComponent("isShortRangeComm"); dataContext.ecs.addEntity(shortRangeComm3); ship3.addComponent("shipSystems", { shipSystems: new Map([[shortRangeComm3.id, {}]]), });
dataContext.ecs.addSystem(new ShortRangeCommPowerSystem()); dataContext.ecs.addSystem(new ProcessTriggersSystem()); dataContext.ecs.addSystem(new TimerSystem());
return { router, dataContext, ship1, shortRangeComm1, ship2, shortRangeComm2, ship3, shortRangeComm3, conversationTemplate, };}