diff --git a/packages/renderer/src/App.svelte b/packages/renderer/src/App.svelte index c316c22..3569be9 100644 --- a/packages/renderer/src/App.svelte +++ b/packages/renderer/src/App.svelte @@ -7,6 +7,7 @@ import { createChoice, createChoiceOutcome, + createEffect, createEvent, editChoice, editEvent, @@ -18,6 +19,7 @@ import type {CreateEvent} from '/@/file-synchronization/CreateEvent'; import type {CreateChoice} from '/@/file-synchronization/CreateChoice'; import type {CreateChoiceOutcome} from '/@/file-synchronization/CreateChoiceOutcome'; + import type {CreateEffect} from '/@/file-synchronization/CreateEffect'; let selectedContentToEdit: ChoiceToDisplay | EventToDisplay | undefined; @@ -46,6 +48,9 @@ case 'createChoiceOutcome': createChoiceOutcome(chainFileAbsolutePath, chain, modificationEvent.detail.content as CreateChoiceOutcome).then(v => onChainSelectionChange(chainFileAbsolutePath)); break; + case 'createEffect': + createEffect(chainFileAbsolutePath, chain, modificationEvent.detail.content as CreateEffect).then(v => onChainSelectionChange(chainFileAbsolutePath)); + break; default: throw Error('unhandled modification event: ' + modificationEvent.detail.type); } @@ -60,7 +65,7 @@ }; - alert(errorEvent.message)}/> + alert(errorEvent.message)} /> {#await currentChainsDirectory} Getting directory path, this show be instantaneous diff --git a/packages/renderer/src/admin-view/EffectCreationForm.svelte b/packages/renderer/src/admin-view/EffectCreationForm.svelte new file mode 100644 index 0000000..bcbebee --- /dev/null +++ b/packages/renderer/src/admin-view/EffectCreationForm.svelte @@ -0,0 +1,173 @@ + +
+

Create a new effect from parent:

+ {#if isChoiceToDisplayId(parentId)} +

choice: {parentId.get()}

+ {:else} +

{parentId.value}

+ {/if} +
validateAndSubmitOnCreateEvent(parentId, effectName, description, operation, target,value,type,activated)}> + + + + + + + + +
+
+ diff --git a/packages/renderer/src/admin-view/EventEditionSidebar.svelte b/packages/renderer/src/admin-view/EventEditionSidebar.svelte index d2dfc26..6c2483a 100644 --- a/packages/renderer/src/admin-view/EventEditionSidebar.svelte +++ b/packages/renderer/src/admin-view/EventEditionSidebar.svelte @@ -6,11 +6,16 @@ import type {CreateEvent} from '/@/file-synchronization/CreateEvent'; import ChoiceCreationForm from '/@/admin-view/ChoiceCreationForm.svelte'; import type {CreateChoice} from '/@/file-synchronization/CreateChoice'; + import EffectCreationForm from '/@/admin-view/EffectCreationForm.svelte'; + import type {CreateEffect} from '/@/file-synchronization/CreateEffect'; export let selectedContentToEdit: EventToDisplay; - let isEventCreationFormDisplayed = false; - let isChoiceCreationFormDisplayed = false; + enum CreationFormDisplayed { + none, createEvent, createChoice, createEffect + } + + let currentCreationFormDisplayed: CreationFormDisplayed = CreationFormDisplayed.none; enum EventOutcomeType { NONE, CHOICES, EVENTS @@ -47,7 +52,7 @@ id: createEvent.id, content: createEvent, }); - isEventCreationFormDisplayed = false; + currentCreationFormDisplayed = CreationFormDisplayed.none; }; const onChoiceCreation = (createChoice: CreateChoice) => { @@ -56,7 +61,16 @@ type: 'createChoice', content: createChoice, }); - isChoiceCreationFormDisplayed = false; + currentCreationFormDisplayed = CreationFormDisplayed.none; + }; + + const onEffectCreation = (createEffect: CreateEffect) => { + console.log('creating effect'); + dispatch('save', { + type: 'createEffect', + content: createEffect, + }); + currentCreationFormDisplayed = CreationFormDisplayed.none; }; @@ -64,17 +78,22 @@

Admin sidebar

- {#if isEventCreationFormDisplayed} + {#if currentCreationFormDisplayed === CreationFormDisplayed.createEvent } onEventCreation(createEvent.detail)} > - {:else if isChoiceCreationFormDisplayed} + {:else if currentCreationFormDisplayed === CreationFormDisplayed.createChoice } onChoiceCreation(createChoice.detail)} > - {:else} + {:else if currentCreationFormDisplayed === CreationFormDisplayed.createEffect} + onEffectCreation(createEffect.detail)} + > + {:else if currentCreationFormDisplayed === CreationFormDisplayed.none}

Modifying event:

@@ -84,14 +103,20 @@
{#if eventOutcomeType === EventOutcomeType.EVENTS || eventOutcomeType === EventOutcomeType.NONE}
- +
{/if} {#if eventOutcomeType === EventOutcomeType.CHOICES || eventOutcomeType === EventOutcomeType.NONE}
- +
{/if} +
+ +
{/if}
diff --git a/packages/renderer/src/admin-view/Select.svelte b/packages/renderer/src/admin-view/Select.svelte new file mode 100644 index 0000000..d18ab55 --- /dev/null +++ b/packages/renderer/src/admin-view/Select.svelte @@ -0,0 +1,45 @@ + + +
+ {#if label} + {label} + {/if} + + + + {#if error} + {error} + {/if} +
+ + diff --git a/packages/renderer/src/file-synchronization/ChainFileModificationAPI.spec.ts b/packages/renderer/src/file-synchronization/ChainFileModificationAPI.spec.ts index 72765b9..0d7e226 100644 --- a/packages/renderer/src/file-synchronization/ChainFileModificationAPI.spec.ts +++ b/packages/renderer/src/file-synchronization/ChainFileModificationAPI.spec.ts @@ -3,6 +3,7 @@ import type {Chain} from '../model/Chain'; import { createChoice, createChoiceOutcome, + createEffect, createEvent, editChoice, editEvent, @@ -10,8 +11,11 @@ import { } from './ChainFileModificationAPI'; import {ChoiceToDisplayId} from '../model/todisplay/ChoiceToDisplay'; import {v4 as uuid} from 'uuid'; +import type {Effect} from '../model/Effect'; +import type {EventId} from '../model/todisplay/EventId'; -const START_EVENT_ID: string = 'start'; +const START_EVENT_ID_DEPRECATED: string = 'start'; // TODO replace with below EntityId +const START_EVENT_ID: EventId = {value: START_EVENT_ID_DEPRECATED}; // TODO replace with below EntityId const CHAIN_ABSOLUTE_PATH: string = 'dummy chain absolute path'; const EMPTY_CHAIN: Chain = { title: 'my-chain-id-1', @@ -38,9 +42,9 @@ describe('testing modification of an Event of a Chain', () => { const chain = deepCopy(EMPTY_CHAIN); const newText = 'my new text'; - editEvent(CHAIN_ABSOLUTE_PATH, chain, START_EVENT_ID, newText); + editEvent(CHAIN_ABSOLUTE_PATH, chain, START_EVENT_ID_DEPRECATED, newText); - expect(chain.events[START_EVENT_ID]).toEqual({ + expect(chain.events[START_EVENT_ID_DEPRECATED]).toEqual({ text: newText, effects: {}, choices: [], @@ -56,11 +60,11 @@ describe('testing modification of an Event of a Chain', () => { await createEvent(CHAIN_ABSOLUTE_PATH, chain, { id: eventId, text: text, - parentEventId: START_EVENT_ID, + parentEventId: START_EVENT_ID_DEPRECATED, }); expect( - chain.events[START_EVENT_ID].next, + chain.events[START_EVENT_ID_DEPRECATED].next, 'parent event.next should have been linked to new event', ).toEqual([ { @@ -86,14 +90,14 @@ describe('testing modification of an Event of a Chain', () => { await createEvent(CHAIN_ABSOLUTE_PATH, chain, { id: 'new-event-1', text: someText, - parentEventId: START_EVENT_ID, + parentEventId: START_EVENT_ID_DEPRECATED, }); }); expect(() => createChoice(CHAIN_ABSOLUTE_PATH, chain, { text: someText, - parentEventId: START_EVENT_ID, + parentEventId: START_EVENT_ID_DEPRECATED, }), ).toThrowError('cannot create a choice on an event outcoming events'); }); @@ -104,7 +108,7 @@ describe('testing modification of an Event of a Chain', () => { await given('given the start_event already has one outcoming choice', async () => { await createChoice(CHAIN_ABSOLUTE_PATH, chain, { text: someText, - parentEventId: START_EVENT_ID, + parentEventId: START_EVENT_ID_DEPRECATED, }); }); @@ -112,7 +116,7 @@ describe('testing modification of an Event of a Chain', () => { createEvent(CHAIN_ABSOLUTE_PATH, chain, { id: 'eventid', text: someText, - parentEventId: START_EVENT_ID, + parentEventId: START_EVENT_ID_DEPRECATED, }), ).toThrowError('cannot create an event on an event outcoming choices'); }); @@ -120,14 +124,19 @@ describe('testing modification of an Event of a Chain', () => { it('should not allow linking an un-existing event', async () => { const chain = deepCopy(EMPTY_CHAIN); expect(() => - linkEvent(CHAIN_ABSOLUTE_PATH, chain, START_EVENT_ID, 'unexisting-event-id'), + linkEvent(CHAIN_ABSOLUTE_PATH, chain, START_EVENT_ID_DEPRECATED, 'unexisting-event-id'), ).toThrowError('event should exist'); }); it('should not allow linking to an un-existing parent event', async () => { const chain = deepCopy(EMPTY_CHAIN); expect(() => - linkEvent(CHAIN_ABSOLUTE_PATH, chain, 'unexisting-parent-event-id', START_EVENT_ID), + linkEvent( + CHAIN_ABSOLUTE_PATH, + chain, + 'unexisting-parent-event-id', + START_EVENT_ID_DEPRECATED, + ), ).toThrowError('parent event should exist'); }); @@ -141,12 +150,12 @@ describe('testing modification of an Event of a Chain', () => { await createEvent(CHAIN_ABSOLUTE_PATH, chain, { id: firstEvent, text: someText, - parentEventId: START_EVENT_ID, + parentEventId: START_EVENT_ID_DEPRECATED, }); await createEvent(CHAIN_ABSOLUTE_PATH, chain, { id: secondEventWithChoice, text: someText, - parentEventId: START_EVENT_ID, + parentEventId: START_EVENT_ID_DEPRECATED, }); }); await given('given the second event has a choice', async () => { @@ -165,18 +174,18 @@ describe('testing modification of an Event of a Chain', () => { describe('testing modification of a Choice of a Chain', () => { it('should edit choice text', () => { const chain = deepCopy(EMPTY_CHAIN); - chain.events[START_EVENT_ID].choices.push({ + chain.events[START_EVENT_ID_DEPRECATED].choices.push({ text: 'my original choice text', effects: {}, next: null, }); - const choiceId: ChoiceToDisplayId = new ChoiceToDisplayId(START_EVENT_ID, 0); + const choiceId: ChoiceToDisplayId = new ChoiceToDisplayId(START_EVENT_ID_DEPRECATED, 0); const newText = 'my new text'; editChoice(CHAIN_ABSOLUTE_PATH, chain, choiceId, newText); - expect(chain.events[START_EVENT_ID].choices).toEqual([ + expect(chain.events[START_EVENT_ID_DEPRECATED].choices).toEqual([ { text: newText, effects: {}, @@ -190,12 +199,12 @@ describe('testing modification of a Choice of a Chain', () => { const text = 'a new choice with text'; await createChoice(CHAIN_ABSOLUTE_PATH, chain, { - parentEventId: START_EVENT_ID, + parentEventId: START_EVENT_ID_DEPRECATED, text: text, }); expect( - chain.events[START_EVENT_ID].choices, + chain.events[START_EVENT_ID_DEPRECATED].choices, 'choice should have been created on parent event', ).toEqual([ { @@ -222,12 +231,12 @@ describe('testing modification of a Choice of a Chain', () => { const chain = deepCopy(EMPTY_CHAIN); const text = 'a new event with text'; - const choiceId = new ChoiceToDisplayId(START_EVENT_ID, 0); + const choiceId = new ChoiceToDisplayId(START_EVENT_ID_DEPRECATED, 0); const newEventId = uuid(); await given('given the start_event has a choice', async () => { await createChoice(CHAIN_ABSOLUTE_PATH, chain, { text: someText, - parentEventId: START_EVENT_ID, + parentEventId: START_EVENT_ID_DEPRECATED, }); }); @@ -238,7 +247,7 @@ describe('testing modification of a Choice of a Chain', () => { }); expect( - chain.events[START_EVENT_ID].choices, + chain.events[START_EVENT_ID_DEPRECATED].choices, 'choice should have a new outcoming event', ).toEqual([ { @@ -263,6 +272,72 @@ describe('testing modification of a Choice of a Chain', () => { }); }); +describe('testing effect creation', () => { + it('should create effect on an event', () => { + const chain = deepCopy(EMPTY_CHAIN); + const effectName = 'my-new-effect'; + const newEffect: Effect = { + description: 'desc', + type: 'instant', + value: 1, + operation: 'add', + target: 'population', + }; + const activated = true; + + createEffect(CHAIN_ABSOLUTE_PATH, chain, { + parentId: START_EVENT_ID, + effectName: effectName, + newEffect: newEffect, + activated: activated, + }); + + expect(chain.effects, 'effect should have been created in the chain').toEqual({ + 'my-new-effect': newEffect, + }); + + expect( + chain.events[START_EVENT_ID.value], + 'effect should have been trigger in event', + ).toHaveProperty('effects', {'my-new-effect': activated}); + }); + + it('should create effect on an choice', () => { + const chain = deepCopy(EMPTY_CHAIN); + given('given a choice without effects', async () => + createChoice(CHAIN_ABSOLUTE_PATH, chain, { + text: someText, + parentEventId: START_EVENT_ID.value, + }), + ); + const effectName = 'my-new-effect'; + const newEffect: Effect = { + description: 'desc', + type: 'instant', + value: 1, + operation: 'add', + target: 'population', + }; + const activated = true; + + createEffect(CHAIN_ABSOLUTE_PATH, chain, { + parentId: {parentId: START_EVENT_ID.value, choiceIndex: 0} as ChoiceToDisplayId, + effectName: effectName, + newEffect: newEffect, + activated: activated, + }); + + expect(chain.effects, 'effect should have been created in the chain').toEqual({ + 'my-new-effect': newEffect, + }); + + expect( + chain.events[START_EVENT_ID.value].choices[0], + 'effect should have been trigger in choice', + ).toHaveProperty('effects', {'my-new-effect': activated}); + }); +}); + function deepCopy(variable: Chain) { return JSON.parse(JSON.stringify(variable)); } diff --git a/packages/renderer/src/file-synchronization/ChainFileModificationAPI.ts b/packages/renderer/src/file-synchronization/ChainFileModificationAPI.ts index a37d246..8468c72 100644 --- a/packages/renderer/src/file-synchronization/ChainFileModificationAPI.ts +++ b/packages/renderer/src/file-synchronization/ChainFileModificationAPI.ts @@ -2,10 +2,14 @@ import type {Chain} from '../model/Chain'; import type {Event} from '../model/Event'; import {EventOutcomeType, getEventOutcomeType} from '../model/Event'; import type {ChoiceToDisplayId} from '../model/todisplay/ChoiceToDisplay'; +import {isChoiceToDisplayId} from '../model/todisplay/ChoiceToDisplay'; import {updateChainFile} from '../ElectronAPIUtils'; import type {CreateEvent} from './CreateEvent'; import type {CreateChoice} from './CreateChoice'; import type {CreateChoiceOutcome} from '/@/file-synchronization/CreateChoiceOutcome'; +import type {CreateEffect} from '/@/file-synchronization/CreateEffect'; +import type {EventId} from '/@/model/todisplay/EventId'; +import type {LinkEffect} from '/@/file-synchronization/LinkEffect'; export function editChoice( chainFileAbsolutePath: string, @@ -173,6 +177,70 @@ export function createChoiceOutcome( return requestUpdateChainFile(chainFileAbsolutePath, chain); } +export function createEffect( + chainFileAbsolutePath: string, + chain: Chain, + createEffect: CreateEffect, +): Promise { + console.debug(createEffect); + if (chain.effects[createEffect.effectName]) { + throw new Error('cannot create effect, it already exists'); + } + chain.effects[createEffect.effectName] = createEffect.newEffect; + + return linkEffect(chainFileAbsolutePath, chain, { + parentId: createEffect.parentId, + effectName: createEffect.effectName, + activated: createEffect.activated, + }); +} + +export function linkEffect( + chainFileAbsolutePath: string, + chain: Chain, + linkEffect: LinkEffect, +): Promise { + if (isChoiceToDisplayId(linkEffect.parentId)) { + const choiceId = linkEffect.parentId as ChoiceToDisplayId; + const parentEvent = chain.events[choiceId.parentId]; + if (!parentEvent) { + throw new Error('choice parent event could not be found'); + } + if (!parentEvent.choices) { + parentEvent.choices = []; + } + + const choice = parentEvent.choices[choiceId.choiceIndex]; + if (!choice) { + throw new Error('choice could not be found'); + } + if (!choice.effects) { + choice.effects = {}; + } + if (choice.effects[linkEffect.effectName]) { + throw new Error('effect is already triggered by this choice'); + } + + choice.effects[linkEffect.effectName] = linkEffect.activated; + } else { + const eventId = linkEffect.parentId as EventId; + const event = chain.events[eventId.value]; + + if (!event) { + throw new Error('event could not be found'); + } + if (!event.effects) { + event.effects = {}; + } + if (event.effects[linkEffect.effectName]) { + throw new Error('event already have this effect triggered'); + } + + event.effects[linkEffect.effectName] = linkEffect.activated; + } + return requestUpdateChainFile(chainFileAbsolutePath, chain); +} + function requestUpdateChainFile(chainFileAbsolutePath: string, chain: Chain) { const filterOutNullPropertiesReplacer = (key: number | string, value: number | string) => { if (value === null) { diff --git a/packages/renderer/src/file-synchronization/CreateEffect.ts b/packages/renderer/src/file-synchronization/CreateEffect.ts new file mode 100644 index 0000000..26fae65 --- /dev/null +++ b/packages/renderer/src/file-synchronization/CreateEffect.ts @@ -0,0 +1,10 @@ +import type {Effect} from '/@/model/Effect'; +import type {ChoiceToDisplayId} from '/@/model/todisplay/ChoiceToDisplay'; +import type {EventId} from '/@/model/todisplay/EventId'; + +export interface CreateEffect { + parentId: EventId | ChoiceToDisplayId; + effectName: string; + newEffect: Effect; + activated: boolean; +} diff --git a/packages/renderer/src/file-synchronization/LinkEffect.ts b/packages/renderer/src/file-synchronization/LinkEffect.ts new file mode 100644 index 0000000..5f5c503 --- /dev/null +++ b/packages/renderer/src/file-synchronization/LinkEffect.ts @@ -0,0 +1,8 @@ +import type {ChoiceToDisplayId} from '/@/model/todisplay/ChoiceToDisplay'; +import type {EventId} from '/@/model/todisplay/EventId'; + +export interface LinkEffect { + parentId: EventId | ChoiceToDisplayId; + effectName: string; + activated: boolean; +} diff --git a/packages/renderer/src/model/todisplay/ChoiceToDisplay.ts b/packages/renderer/src/model/todisplay/ChoiceToDisplay.ts index bf8a087..f0d6990 100644 --- a/packages/renderer/src/model/todisplay/ChoiceToDisplay.ts +++ b/packages/renderer/src/model/todisplay/ChoiceToDisplay.ts @@ -26,6 +26,16 @@ export class ChoiceToDisplayId { } } +/** + * user-defined type guard + * see https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates + * @param item you want to check if it has ChoiceToDisplayId type + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isChoiceToDisplayId(item: any): item is ChoiceToDisplayId { + return 'choiceIndex' in item; +} + function convertToChoiceOutcomeToDisplay( parentId: ChoiceToDisplayId, source: ChoiceOutcome[], diff --git a/packages/renderer/src/model/todisplay/EventId.ts b/packages/renderer/src/model/todisplay/EventId.ts new file mode 100644 index 0000000..3d2021f --- /dev/null +++ b/packages/renderer/src/model/todisplay/EventId.ts @@ -0,0 +1,3 @@ +export interface EventId { + value: string; +}