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

Target

Select target project
  • tudo-fsinfo/events/workadventure-dialogs
  • nicolas.lenz/workadventure-dialogs
2 results
Show changes
......@@ -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,10 +11,13 @@
<main>
{#await dialogSetPromise then dialogSet}
<SavingComponent />
<br />
<DialogSetComponent bind:currentDialog {...dialogSet} />
<br />
<Debugger {dialogSet} bind:currentDialog />
{#if window.location.hostname.includes("localhost")}
<Debugger {dialogSet} bind:currentDialog />
{/if}
{:catch _error}
<h3>Oh no :(</h3>
<p>
......
<script lang="ts">
import {
gameFactsStore,
gameFacts,
addGameFactToFactArray,
toggleFactInFactArray,
} from "./gameFacts";
} 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;
let selectedDialogName: string;
/**
* Collect all facts referenced in these dialogs
......@@ -32,7 +32,7 @@
}
let seenFactIds = new Set<String>([
...$gameFactsStore,
...$gameFacts,
...getDialogFacts(dialogSet.dialogs),
]);
$: seenFactIdsArray = Array.from(seenFactIds);
......@@ -41,63 +41,59 @@
<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>
<br />
<ol>
{#each findDialogSetProblems(dialogSet) as { sourceDialog, text }}
<li>
{sourceDialog}: <code style="color: orange">{text}</code>
</li>
{/each}
</ol>
<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>
<br />
<ol>
{#each findDialogSetProblems(dialogSet) as { sourceDialog, text }}
<li>
{sourceDialog}: <code style="color: orange">{text}</code>
</li>
{/each}
</ol>
<hr />
<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}
</ul>
Add fact:
<input type="text" bind:value={addFactInputValue} />
<button
style="width: 3rem;"
on:click={() => {
gameFactsStore.set(
addGameFactToFactArray(addFactInputValue, $gameFactsStore)
);
seenFactIds = seenFactIds.add(addFactInputValue);
addFactInputValue = "";
}}>Add</button
>
<button style="width: 13rem;" on:click={() => gameFactsStore.set([])}
>Reset facts</button
>
<h3>Quest-Debugger</h3>
<b>GameFacts:</b>
<ul>
{#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={() => {
gameFacts.set(addGameFactToFactArray(addFactInputValue, $gameFacts));
seenFactIds = seenFactIds.add(addFactInputValue);
addFactInputValue = "";
}}>Add</button
>
<button style="width: 13rem;" on:click={() => gameFacts.set([])}
>Reset facts</button
>
</details>
</div>
......
<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)
isDialogOptionAllowedByGameFacts(option, $gameFacts)
);
export let addFacts: String[] = [];
$: {
if (Array.isArray(addFacts)) {
const gameFactsAfterAddedFacts = addFacts.reduce(
(accFacts, factToAdd) => addGameFactToFactArray(factToAdd, accFacts),
$gameFactsStore
$gameFacts
);
$gameFactsStore = gameFactsAfterAddedFacts;
$gameFacts = gameFactsAfterAddedFacts;
}
}
export let removeFacts: String[] = [];
......@@ -28,9 +29,9 @@
if (Array.isArray(removeFacts)) {
const gameFactsAfterRemovedFacts = removeFacts.reduce(
(accFacts, factToRemove) => accFacts.filter((f) => f !== factToRemove),
$gameFactsStore
$gameFacts
);
$gameFactsStore = gameFactsAfterRemovedFacts;
$gameFacts = gameFactsAfterRemovedFacts;
}
}
......@@ -72,15 +73,27 @@
</h3>
{/if}
<!-- 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>
<Typewriter cascade interval={10} cursor={false}>
{#each text.split("\n") as para}
<p>{para}</p>
{/each}
</Typewriter>
</div>
<hr />
<div>
{#each usableOptions as option}
......
......@@ -18,6 +18,10 @@
},
"type": "array"
},
"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"
......
import { writable } from "svelte-local-storage-store";
import type { Writable } from "svelte/store";
export const gameFactsStore: Writable<String[]> = writable("gameFacts", []);
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);
}
}
......@@ -45,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.
......@@ -97,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")
);
}
......@@ -18,7 +18,7 @@ function isValidDialogSetName(input: unknown) {
return !/[^a-zA-Z0-9\-\_]/i.test(input);
}
function getParamValue(paramName: string): string | null {
export function getParamValue(paramName: string): string | null {
return new URLSearchParams(window.location.search).get(paramName);
}
......