Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • alexandra.cloodt-master-patch-15365
  • master
2 results

Target

Select target project
  • tudo-fsinfo/events/workadventure-dialogs
  • nicolas.lenz/workadventure-dialogs
2 results
Select Git revision
  • master
1 result
Show changes
Showing with 516 additions and 205 deletions
public/studiboerse/mini-map1.png

389 KiB

public/studiboerse/mini-map2.png

137 KiB

public/studiboerse/open-in-firefox.png

6.78 KiB

public/studiboerse/support.png

6 KiB

public/studiboerse/wegweiser.png

4.88 KiB

......@@ -41,6 +41,7 @@ export default {
format: "iife",
name: "app",
file: "public/build/bundle.js",
inlineDynamicImports: true,
},
plugins: [
svelte({
......
......@@ -3,6 +3,7 @@
import { fetchDialogSet } from "./utils";
import Debugger from "./Debugger.svelte";
import type { Dialog } from "./types";
import SavingComponent from "./SavingComponent.svelte";
const dialogSetPromise = fetchDialogSet();
let currentDialog: Dialog;
......@@ -10,14 +11,18 @@
<main>
{#await dialogSetPromise then dialogSet}
<DialogSetComponent bind:currentDialog={currentDialog} {...dialogSet} />
<br>
<Debugger dialogSet={dialogSet} bind:currentDialog={currentDialog}/>
<SavingComponent />
<br />
<DialogSetComponent bind:currentDialog {...dialogSet} />
<br />
{#if window.location.hostname.includes("localhost")}
<Debugger {dialogSet} bind:currentDialog />
{/if}
{:catch _error}
<h3>Oh no :(</h3>
<p>
We could not load that dialogSet that you specified in the url. Sorry. Please go complain to your friendly admin(s).
We could not load that dialogSet that you specified in the url. Sorry.
Please go complain to your friendly admin(s).
</p>
{/await}
</main>
<script lang="ts">
import { gameFactsStore, addGameFactToFactArray, toggleFactInFactArray } from './gameFacts'
import type { Dialog, DialogMap, DialogSet } from './types';
import {
gameFacts,
addGameFactToFactArray,
toggleFactInFactArray,
} from "./gameStateStore";
import type { Dialog, DialogMap, DialogSet } from "./types";
import { findDialogSetProblems } from "./utils";
export let currentDialog: Dialog;
export let dialogSet: DialogSet;
$: dialogNames = Object.keys(dialogSet.dialogs)
export let selectedDialogName: string
$: dialogNames = Object.keys(dialogSet.dialogs);
let selectedDialogName: string;
/**
* Collect all facts referenced in these dialogs
*/
function getDialogFacts(dialogMap: DialogMap): String[] {
const dialogs = [];
for (const dmKey of Object.keys(dialogMap)) dialogs.push(dialogMap[dmKey]);
return dialogs.map(dialog => [...(dialog.addFacts || []), ...(dialog.removeFacts || [])]).flat()
const dialogs: Dialog[] = [];
for (const dmKey of Object.keys(dialogMap)) dialogs.push(dialogMap[dmKey]);
const allFacts = dialogs
.map((dialog) => [
...(dialog.addFacts || []),
...(dialog.removeFacts || []),
...dialog.options.map((opt) => opt.requiredFacts).flat(),
...dialog.options.map((opt) => opt.forbiddenFacts).flat(),
])
.flat();
// Deduplicate entries
const factsSet = new Set(allFacts);
return [...factsSet].filter((e) => e !== undefined);
}
let seenFactIds = new Set<String>([...$gameFactsStore, ...getDialogFacts(dialogSet.dialogs)]);
$: seenFactIdsArray = Array.from(seenFactIds)
let seenFactIds = new Set<String>([
...$gameFacts,
...getDialogFacts(dialogSet.dialogs),
]);
$: seenFactIdsArray = Array.from(seenFactIds);
let addFactInputValue: string;
</script>
<div>
<details>
<summary>Show debug tools</summary>
<h3>Dialog-Debugger</h3>
Jump to dialog:
<!-- svelte-ignore a11y-no-onchange -->
<select bind:value={selectedDialogName} on:change="{() => currentDialog = dialogSet.dialogs[selectedDialogName]}">
{#each dialogNames as dialogName}
<option value={dialogName} selected={dialogName === selectedDialogName}>
{dialogName}
</option>
{/each}
</select>
<hr>
<select
bind:value={selectedDialogName}
on:change={() => (currentDialog = dialogSet.dialogs[selectedDialogName])}
>
{#each dialogNames as dialogName}
<option value={dialogName} selected={dialogName === selectedDialogName}>
{dialogName}
</option>
{/each}
</select>
<br />
<ol>
{#each findDialogSetProblems(dialogSet) as { sourceDialog, text }}
<li>
{sourceDialog}: <code style="color: orange">{text}</code>
</li>
{/each}
</ol>
<hr />
<h3>Quest-Debugger</h3>
<b>GameFacts:</b>
<ul>
{#each seenFactIdsArray as gameFact}
<li>
<input type="checkbox" checked={$gameFactsStore.includes(gameFact)} on:change={() => gameFactsStore.set(toggleFactInFactArray(gameFact, $gameFactsStore))}>
{gameFact}
</li>
{/each}
{#each seenFactIdsArray as gameFact}
<li>
<input
type="checkbox"
checked={$gameFacts.includes(gameFact)}
on:change={() =>
gameFacts.set(toggleFactInFactArray(gameFact, $gameFacts))}
/>
{gameFact}
</li>
{/each}
</ul>
Add fact:
<input type="text" bind:value={addFactInputValue}>
<button style="width: 3rem;" on:click={() => {
gameFactsStore.set(addGameFactToFactArray(addFactInputValue, $gameFactsStore));
<input type="text" bind:value={addFactInputValue} />
<button
style="width: 3rem;"
on:click={() => {
gameFacts.set(addGameFactToFactArray(addFactInputValue, $gameFacts));
seenFactIds = seenFactIds.add(addFactInputValue);
addFactInputValue = '';
}}>Add</button>
addFactInputValue = "";
}}>Add</button
>
<button style="width: 13rem;" on:click={() => gameFacts.set([])}
>Reset facts</button
>
</details>
</div>
<style>
div {
border: 1px solid;
padding: 10px;
margin: 5px;
}
</style>
\ No newline at end of file
div {
border: 1px solid;
padding: 10px;
margin: 5px;
}
</style>
......@@ -11,16 +11,11 @@
function switchDialog(targetDialog: string) {
currentDialog = dialogs[targetDialog];
}
function checkForInvalidDialogReferences() {
const allDialogKeys = Object.keys(dialogs);
for (const dialogName of allDialogKeys) {
// TODO check all buttons for invalid references
}
}
</script>
<SingleDialogComponent
imageUrl={imageUrl} title={title} {...currentDialog}
{imageUrl}
{title}
{...currentDialog}
on:switchToDialog={(event) => switchDialog(event.detail)}
/>
<script lang="ts">
import { gameStateId, syncConnected, setupSyncing } from "./gameStateStore";
async function handleSyncState(): Promise<void> {
const success = await setupSyncing();
if (!success) {
alert(
"Herrgott, wie schwer kann es sein, eine gültige ID einzugeben? Nochmal!"
);
}
}
</script>
{#if !$syncConnected}
<div>
<p>
Dein Spielstand wird aktuell noch nicht synchronisiert, d.h. wenn du den
Browser schließt, kann es sein, dass du von vorne anfängst. Zum Speichern
gib bitte deine Bestellnummer ein. Deine Bestellnummer findest du in den
E-Mails, die wir dir zur O-Phase gesendet haben.
</p>
<br />
<input
type="text"
placeholder="Bsp: A1B2C3"
maxlength="5"
minlength="5"
bind:value={$gameStateId}
/>
<button style="width: 20rem;" on:click|preventDefault={handleSyncState}>
Spielstand synchronisieren
</button>
</div>
{/if}
<style>
div {
border: 1px solid;
padding: 10px;
padding-bottom: 0px;
margin: 5px;
}
</style>
<script lang="ts">
import type { DialogOption } from "./types";
import { createEventDispatcher } from "svelte";
import { gameFactsStore, addGameFactToFactArray } from './gameFacts'
import { gameFacts, addGameFactToFactArray } from "./gameStateStore";
import Typewriter from "svelte-typewriter";
const dispatch = createEventDispatcher();
export let imageUrl: string;
export let peertubeVideoId: string | undefined;
export let title: string | undefined;
export let text: string;
export let options: DialogOption[] = [];
$: usableOptions = options.filter(option => isDialogOptionAllowedByGameFacts(option, $gameFactsStore))
$: usableOptions = options.filter((option) =>
isDialogOptionAllowedByGameFacts(option, $gameFacts)
);
export let addFacts: String[] = [];
$: {
if (Array.isArray(addFacts)) {
const gameFactsAfterAddedFacts = addFacts.reduce((accFacts, factToAdd) => addGameFactToFactArray(factToAdd, accFacts), $gameFactsStore)
$gameFactsStore = gameFactsAfterAddedFacts;
const gameFactsAfterAddedFacts = addFacts.reduce(
(accFacts, factToAdd) => addGameFactToFactArray(factToAdd, accFacts),
$gameFacts
);
$gameFacts = gameFactsAfterAddedFacts;
}
}
export let removeFacts: String[] = [];
$: {
if (Array.isArray(removeFacts)) {
const gameFactsAfterRemovedFacts = removeFacts.reduce((accFacts, factToRemove) => accFacts.filter(f => f !== factToRemove), $gameFactsStore)
$gameFactsStore = gameFactsAfterRemovedFacts;
const gameFactsAfterRemovedFacts = removeFacts.reduce(
(accFacts, factToRemove) => accFacts.filter((f) => f !== factToRemove),
$gameFacts
);
$gameFacts = gameFactsAfterRemovedFacts;
}
}
function isDialogOptionAllowedByGameFacts(option: DialogOption, gameFacts: String[]): boolean {
function isDialogOptionAllowedByGameFacts(
option: DialogOption,
gameFacts: String[]
): boolean {
// Check if all required facts are given
if (Array.isArray(option.requiredFacts)) {
const isOk = option.requiredFacts.every(requiredFact => gameFacts.includes(requiredFact));
const isOk = option.requiredFacts.every((requiredFact) =>
gameFacts.includes(requiredFact)
);
if (!isOk) {
return false;
}
......@@ -36,7 +51,9 @@
// Check if no forbidden facts are given
if (Array.isArray(option.forbiddenFacts)) {
const isOk = option.forbiddenFacts.every(forbiddenFact => !gameFacts.includes(forbiddenFact));
const isOk = option.forbiddenFacts.every(
(forbiddenFact) => !gameFacts.includes(forbiddenFact)
);
if (!isOk) {
return false;
}
......@@ -46,7 +63,7 @@
}
function handleDialogOptionClick(option: DialogOption): void {
dispatch("switchToDialog", option.linksToDialog)
dispatch("switchToDialog", option.linksToDialog);
}
</script>
......@@ -56,13 +73,27 @@
</h3>
{/if}
<img src={imageUrl} alt="Portrait" />
<!-- svelte-ignore a11y-missing-attribute -->
<img src={imageUrl} />
{#if typeof peertubeVideoId === "string" && peertubeVideoId.length > 0}
<iframe
title="Embedded video"
height="400"
sandbox="allow-same-origin allow-scripts allow-popups"
src="https://video.oh14.de/videos/embed/{peertubeVideoId}?autoplay=1&title=0&warningTitle=0&peertubeLink=0"
frameborder="0"
/>
{/if}
{#if typeof text === "string" && text.length > 0}
<div>
<Typewriter cascade interval={10} cursor={false}>
{#each text.split("\n") as para}
<p>{para}</p>
{/each}
</Typewriter>
</div>
{/if}
<div>
{#each text.split("\n") as para}
<p>{para}</p>
{/each}
</div>
<hr />
<div>
{#each usableOptions as option}
......@@ -72,3 +103,9 @@
<br />
{/each}
</div>
<style>
img {
max-height: 15rem;
}
</style>
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"Dialog": {
"description": "A dialog situation that a player can encounter in this dialogSet.\n\nPrinciple: NPC says something, user has options how to react.",
"properties": {
"addFacts": {
"description": "Upon entering this dialog, register these facts for the user.\n\nFacts are texts and may unlock or block dialog options across the game.\nE.g. The fact \"acceptedMissionX\" could unlock a dialog option regarding Mission X with another NPC.",
"items": {
"additionalProperties": false,
"description": "Allows manipulation and formatting of text strings and determination and location of substrings within strings.",
"patternProperties": {
"^[0-9]+$": {
"type": "string"
}
},
"type": "object"
},
"type": "array"
},
"imageUrl": {
"description": "Can be used to overwrite imageUrl from DialogSet.\nE.g. when a different emotion should be shown for this dialog,",
"type": "string"
},
"options": {
"description": "Choices of what the user might say in this dialog and where those choices lead to.\nThe player should be given between 0 and 4 options.",
"items": {
"$ref": "#/definitions/DialogOption"
},
"type": "array"
},
"removeFacts": {
"description": "Upon entering this dialog, remove these facts from the user.\n\nFacts are texts and may unlock or block dialog options across the game.\nE.g. The fact \"acceptedMissionX\" could unlock a dialog option regarding Mission X with another NPC.",
"items": {
"additionalProperties": false,
"description": "Allows manipulation and formatting of text strings and determination and location of substrings within strings.",
"patternProperties": {
"^[0-9]+$": {
"type": "string"
}
},
"type": "object"
},
"type": "array"
},
"text": {
"description": "What the characters says to the player.\nLine breaks (\\n) are rendered as paragraphs.\nYou may leave this empty or omit it completely.",
"type": "string"
},
"title": {
"description": "The title is displayed at the very top of the dialog screen\nand usually corresponds to the name of the NPC the player is speaking to.\n\nCan be used to overwrite title from DialogSet.",
"type": "string"
}
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"Dialog": {
"description": "A dialog situation that a player can encounter in this dialogSet.\n\nPrinciple: NPC says something, user has options how to react.",
"properties": {
"addFacts": {
"description": "Upon entering this dialog, register these facts for the user.\n\nFacts are texts and may unlock or block dialog options across the game.\nE.g. The fact \"acceptedMissionX\" could unlock a dialog option regarding Mission X with another NPC.",
"items": {
"additionalProperties": false,
"description": "Allows manipulation and formatting of text strings and determination and location of substrings within strings.",
"patternProperties": {
"^[0-9]+$": {
"type": "string"
}
},
"type": "object"
},
"type": "array"
},
"DialogOption": {
"properties": {
"forbiddenFacts": {
"description": "All facts in this array must *not* be registered for this player for this option to show up.\n\nFacts are texts and may unlock or block dialog options across the game.\nE.g. The fact \"acceptedMissionX\" could unlock a dialog option regarding Mission X with another NPC.",
"items": {
"additionalProperties": false,
"description": "Allows manipulation and formatting of text strings and determination and location of substrings within strings.",
"patternProperties": {
"^[0-9]+$": {
"type": "string"
}
},
"type": "object"
},
"type": "array"
},
"linksToDialog": {
"description": "Which dialog in this dialogSet to jump to if this option is chosen.",
"type": "string"
},
"requiredFacts": {
"description": "All facts in this array must be registered for this player for this option to show up.\n\nFacts are texts and may unlock or block dialog options across the game.\nE.g. The fact \"acceptedMissionX\" could unlock a dialog option regarding Mission X with another NPC.",
"items": {
"additionalProperties": false,
"description": "Allows manipulation and formatting of text strings and determination and location of substrings within strings.",
"patternProperties": {
"^[0-9]+$": {
"type": "string"
}
},
"type": "object"
},
"type": "array"
},
"text": {
"description": "Text on option button.\nShould be rather short and not have line breaks.",
"type": "string"
}
"peertubeVideoId": {
"description": "Embeds a peerTube-Video into this dialog and plays it when the dialog is opened.",
"type": "string"
},
"imageUrl": {
"description": "Can be used to overwrite imageUrl from DialogSet.\nE.g. when a different emotion should be shown for this dialog,",
"type": "string"
},
"options": {
"description": "Choices of what the user might say in this dialog and where those choices lead to.\nThe player should be given between 0 and 4 options.",
"items": {
"$ref": "#/definitions/DialogOption"
},
"type": "array"
},
"removeFacts": {
"description": "Upon entering this dialog, remove these facts from the user.\n\nFacts are texts and may unlock or block dialog options across the game.\nE.g. The fact \"acceptedMissionX\" could unlock a dialog option regarding Mission X with another NPC.",
"items": {
"additionalProperties": false,
"description": "Allows manipulation and formatting of text strings and determination and location of substrings within strings.",
"patternProperties": {
"^[0-9]+$": {
"type": "string"
}
},
"type": "object"
},
"type": "array"
},
"text": {
"description": "What the characters says to the player.\nLine breaks (\\n) are rendered as paragraphs.\nYou may leave this empty or omit it completely.",
"type": "string"
},
"title": {
"description": "The title is displayed at the very top of the dialog screen\nand usually corresponds to the name of the NPC the player is speaking to.\n\nCan be used to overwrite title from DialogSet.",
"type": "string"
}
},
"type": "object"
},
"description": "Set of dialogs, beginning with startDialogName.\nThis should be",
"properties": {
"dialogs": {
"additionalProperties": {
"$ref": "#/definitions/Dialog"
"DialogOption": {
"properties": {
"forbiddenFacts": {
"description": "All facts in this array must *not* be registered for this player for this option to show up.\n\nFacts are texts and may unlock or block dialog options across the game.\nE.g. The fact \"acceptedMissionX\" could unlock a dialog option regarding Mission X with another NPC.",
"items": {
"additionalProperties": false,
"description": "Allows manipulation and formatting of text strings and determination and location of substrings within strings.",
"patternProperties": {
"^[0-9]+$": {
"type": "string"
}
},
"description": "All dialogs in this dialogSet.",
"type": "object"
},
"type": "array"
},
"imageUrl": {
"description": "Url of image.\n\nShould link to image in this repo, to avoid CORS errors.",
"type": "string"
"linksToDialog": {
"description": "Which dialog in this dialogSet to jump to if this option is chosen.",
"type": "string"
},
"startDialogName": {
"description": "At which dialog to start when the dialogSet is first opened by the player.",
"type": "string"
"requiredFacts": {
"description": "All facts in this array must be registered for this player for this option to show up.\n\nFacts are texts and may unlock or block dialog options across the game.\nE.g. The fact \"acceptedMissionX\" could unlock a dialog option regarding Mission X with another NPC.",
"items": {
"additionalProperties": false,
"description": "Allows manipulation and formatting of text strings and determination and location of substrings within strings.",
"patternProperties": {
"^[0-9]+$": {
"type": "string"
}
},
"type": "object"
},
"type": "array"
},
"title": {
"description": "The title is displayed at the very top of the dialog screen\nand usually corresponds to the name of the NPC the player is speaking to.",
"type": "string"
"text": {
"description": "Text on option button.\nShould be rather short and not have line breaks.",
"type": "string"
}
},
"type": "object"
}
},
"description": "Set of dialogs, beginning with startDialogName.\nThis should be",
"properties": {
"dialogs": {
"additionalProperties": {
"$ref": "#/definitions/Dialog"
},
"description": "All dialogs in this dialogSet.",
"type": "object"
},
"imageUrl": {
"description": "Url of image.\n\nShould link to image in this repo, to avoid CORS errors.",
"type": "string"
},
"startDialogName": {
"description": "At which dialog to start when the dialogSet is first opened by the player.",
"type": "string"
},
"type": "object"
"title": {
"description": "The title is displayed at the very top of the dialog screen\nand usually corresponds to the name of the NPC the player is speaking to.",
"type": "string"
}
},
"type": "object"
}
import { writable } from "svelte-local-storage-store";
import type { Writable } from "svelte/store";
export const gameFactsStore: Writable<String[]> = writable("gameFacts", [
"debugRequiredFact",
]);
export function addGameFactToFactArray(
gameFact: String,
factArray: String[]
): String[] {
if (!factArray.includes(gameFact)) {
return [...factArray, gameFact];
} else {
return factArray;
}
}
export function toggleFactInFactArray(
gameFact: String,
factArray: String[]
): String[] {
if (!factArray.includes(gameFact)) {
return [...factArray, gameFact];
} else {
return factArray.filter((f) => f !== gameFact);
}
}
import { GameState, isValidGameState } from "./types";
export class GameStateServerClient {
_apiBaseUrl: string;
constructor(apiBaseUrl: string) {
this._apiBaseUrl = apiBaseUrl;
}
async saveState(id: string, gameState: GameState): Promise<boolean> {
try {
await fetch(this._apiBaseUrl + "/game/state/" + id, {
method: "POST",
mode: "cors",
cache: "no-cache",
credentials: "omit",
headers: {
"Content-Type": "application/json",
},
referrerPolicy: "no-referrer",
body: JSON.stringify(gameState),
});
return true;
} catch (saveStateToServerError) {
console.error(
"Failed to save state to server. The following info may help to solve this problem:",
saveStateToServerError
);
return false;
}
}
async loadState(id: string): Promise<GameState | null> {
try {
const rawResponse = await fetch(this._apiBaseUrl + "/game/state/" + id, {
method: "GET",
mode: "cors",
cache: "no-cache",
credentials: "omit",
referrerPolicy: "no-referrer",
});
if (rawResponse.status === 400) {
return null;
}
const decodedResponse = await rawResponse.json();
if (!isValidGameState(decodedResponse)) {
throw new TypeError("State from server is not a valid gameState");
}
return decodedResponse;
} catch (loadGameStateFromServerError) {}
}
}
import { writable } from "svelte-local-storage-store";
import debounce from "lodash.debounce";
import type { Writable } from "svelte/store";
import { GameStateServerClient } from "./gameStateServerClient";
import type { GameState } from "./types";
export const gameFacts: Writable<String[]> = writable("gameFacts", []);
const currentGameState: GameState = {
gameFacts: [],
};
// Update currentGameState whenever gameFacts change
gameFacts.subscribe((newValue) => (currentGameState.gameFacts = [...newValue]));
let currentGameStateId = "";
export const gameStateId: Writable<string> = writable(
"gameStateId",
currentGameStateId
);
gameStateId.subscribe((newValue) => (currentGameStateId = newValue));
export const syncServerBaseUrl = "https://state.world.oh14.de";
let isSyncConnected = false;
export const syncConnected: Writable<boolean> = writable(
"syncConnected",
isSyncConnected
);
const syncClient: GameStateServerClient = new GameStateServerClient(
syncServerBaseUrl
);
// Try to sync currentGameState whenever gameFacts change
gameFacts.subscribe(
debounce(
async () => {
if (isSyncConnected) {
await syncClient.saveState(currentGameStateId, currentGameState);
try {
} catch (error) {
// Failed to save state on update via store
}
}
},
500,
{ leading: true }
)
);
export async function setupSyncing(): Promise<boolean> {
try {
const syncedGameState = await syncClient.loadState(currentGameStateId);
if (syncedGameState == null) {
// No save game found yet, but id allowed
isSyncConnected = true;
syncConnected.set(true);
} else if (syncedGameState) {
gameFacts.set(syncedGameState.gameFacts);
isSyncConnected = true;
syncConnected.set(true);
}
return true;
} catch (error) {
return false;
}
}
export function addGameFactToFactArray(
gameFact: String,
factArray: String[]
): String[] {
if (!factArray.includes(gameFact)) {
return [...factArray, gameFact];
} else {
return factArray;
}
}
export function toggleFactInFactArray(
gameFact: String,
factArray: String[]
): String[] {
if (!factArray.includes(gameFact)) {
return [...factArray, gameFact];
} else {
return factArray.filter((f) => f !== gameFact);
}
}
/**
* Set of dialogs, beginning with startDialogName.
* This should be
*/
export interface DialogSet {
/**
......@@ -46,6 +45,11 @@ export interface Dialog {
* E.g. when a different emotion should be shown for this dialog,
*/
imageUrl?: string;
/**
* Embeds a peerTube-Video into this dialog and plays it.
*/
peertubeVideoId?: string;
/**
* What the characters says to the player.
* Line breaks (\n) are rendered as paragraphs.
......@@ -98,3 +102,16 @@ export interface DialogOption {
*/
forbiddenFacts?: String[];
}
export interface GameState {
gameFacts: String[];
}
export function isValidGameState(input: unknown): input is GameState {
if (typeof input !== "object") return false;
return (
input["gameFacts"] !== undefined &&
Array.isArray(input["gameFacts"]) &&
input["gameFacts"].every((gameFact) => typeof gameFact === "string")
);
}
import type { DialogSet } from "./types";
import type { DialogMap, DialogOption, DialogSet } from "./types";
/**
* Returns true, if input is a valid name of a dialogSet.
......@@ -18,15 +18,15 @@ function isValidDialogSetName(input: unknown) {
return !/[^a-zA-Z0-9\-\_]/i.test(input);
}
function getHashValue(): string {
return window.location.hash.substr(1);
export function getParamValue(paramName: string): string | null {
return new URLSearchParams(window.location.search).get(paramName);
}
/**
* Fetch a dialogSet based on the hash value
*/
export async function fetchDialogSet(): Promise<DialogSet> {
const setName = getHashValue();
const setName = getParamValue("dialogSet");
if (!isValidDialogSetName(setName))
throw new Error("Name is not valid dialogSet name");
......@@ -39,3 +39,58 @@ export async function fetchDialogSet(): Promise<DialogSet> {
throw error;
}
}
export function findDialogSetProblems(dialogSet: DialogSet) {
const dialogReferenceProblems = checkForInvalidDialogReferences(dialogSet);
return [...dialogReferenceProblems];
}
interface DialogReferenceProblem {
type: "DialogReferenceProblem";
sourceDialog: string;
text: string;
}
function checkForInvalidDialogReferences(
dialogSet: DialogSet
): DialogReferenceProblem[] {
const dialogs = dialogSet.dialogs;
const problems: DialogReferenceProblem[] = [];
const allDialogKeys = Object.keys(dialogs);
const allLinkedToDialogKeys: Set<string> = new Set();
for (const dialogName of allDialogKeys) {
// TODO check all buttons for invalid references
for (const { text, linksToDialog } of dialogs[dialogName].options) {
allLinkedToDialogKeys.add(linksToDialog);
if (!allDialogKeys.includes(linksToDialog)) {
problems.push({
type: "DialogReferenceProblem",
sourceDialog: dialogName,
text: `Dialog ${dialogName}'s option "${text}" links to unknown dialog '${linksToDialog}'`,
});
}
if (linksToDialog === dialogName) {
problems.push({
type: "DialogReferenceProblem",
sourceDialog: dialogName,
text: `Dialog ${dialogName}'s option "${text}" links to itself!`,
});
}
}
}
for (const dialogName of allDialogKeys) {
if (
!allLinkedToDialogKeys.has(dialogName) &&
dialogSet.startDialogName !== dialogName
) {
problems.push({
type: "DialogReferenceProblem",
sourceDialog: dialogName,
text: `Dialog ${dialogName} is unreachable: Neither linked to from any other dialog, nor startDialog`,
});
}
}
return problems;
}