diff options
Diffstat (limited to 'web/pw-server/src/routes/editor.svelte')
-rw-r--r-- | web/pw-server/src/routes/editor.svelte | 277 |
1 files changed, 277 insertions, 0 deletions
diff --git a/web/pw-server/src/routes/editor.svelte b/web/pw-server/src/routes/editor.svelte new file mode 100644 index 0000000..85c2454 --- /dev/null +++ b/web/pw-server/src/routes/editor.svelte @@ -0,0 +1,277 @@ +<script lang="ts"> + import Visualizer from "$lib/components/Visualizer.svelte"; + import EditorView from "$lib/components/EditorView.svelte"; + import { onMount } from "svelte"; + + import { DateTime } from "luxon"; + + import type { Ace } from "ace-builds"; + import ace from "ace-builds/src-noconflict/ace?client"; + import * as AcePythonMode from "ace-builds/src-noconflict/mode-python?client"; + import { getBotCode, saveBotCode, hasBotCode } from "$lib/bot_code"; + import { debounce } from "$lib/utils"; + import SubmitPane from "$lib/components/SubmitPane.svelte"; + import OutputPane from "$lib/components/OutputPane.svelte"; + import RulesView from "$lib/components/RulesView.svelte"; + import Leaderboard from "$lib/components/Leaderboard.svelte"; + + enum ViewMode { + Editor, + MatchVisualizer, + Rules, + Leaderboard, + } + + let matches = []; + + let viewMode = ViewMode.Editor; + let selectedMatchId: string | undefined = undefined; + let selectedMatchLog: string | undefined = undefined; + + let editSession: Ace.EditSession; + + onMount(() => { + if (!hasBotCode()) { + viewMode = ViewMode.Rules; + } + init_editor(); + }); + + function init_editor() { + editSession = new ace.EditSession(getBotCode()); + editSession.setMode(new AcePythonMode.Mode()); + + const saveCode = () => { + const code = editSession.getDocument().getValue(); + saveBotCode(code); + }; + + // cast to any because the type annotations are wrong here + (editSession as any).on("change", debounce(saveCode, 2000)); + } + + async function onMatchCreated(e: CustomEvent) { + const matchData = e.detail["match"]; + matches.unshift(matchData); + matches = matches; + await selectMatch(matchData["id"]); + } + + async function selectMatch(matchId: string) { + selectedMatchId = matchId; + selectedMatchLog = null; + fetchSelectedMatchLog(matchId); + + viewMode = ViewMode.MatchVisualizer; + } + + async function fetchSelectedMatchLog(matchId: string) { + if (matchId !== selectedMatchId) { + return; + } + + let matchLog = await getMatchLog(matchId); + + if (matchLog) { + selectedMatchLog = matchLog; + } else { + // try again in 1 second + setTimeout(fetchSelectedMatchLog, 1000, matchId); + } + } + + async function getMatchData(matchId: string) { + let response = await fetch(`/api/matches/${matchId}`, { + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw Error(response.statusText); + } + + let matchData = await response.json(); + return matchData; + } + + async function getMatchLog(matchId: string) { + const matchData = await getMatchData(matchId); + console.log(matchData); + if (matchData["state"] !== "Finished") { + // log is not available yet + return null; + } + + const res = await fetch(`/api/matches/${matchId}/log`, { + headers: { + "Content-Type": "application/json", + }, + }); + + let log = await res.text(); + return log; + } + + function setViewMode(viewMode_: ViewMode) { + selectedMatchId = undefined; + selectedMatchLog = undefined; + viewMode = viewMode_; + } + + function selectRules() { + selectedMatchId = undefined; + selectedMatchLog = undefined; + viewMode = ViewMode.Rules; + } + + function formatMatchTimestamp(timestampString: string): string { + let timestamp = DateTime.fromISO(timestampString, { zone: "utc" }).toLocal(); + if (timestamp.startOf("day").equals(DateTime.now().startOf("day"))) { + return timestamp.toFormat("HH:mm"); + } else { + return timestamp.toFormat("dd/MM"); + } + } + + $: selectedMatch = matches.find((m) => m["id"] === selectedMatchId); +</script> + +<div class="container"> + <div class="sidebar-left"> + <div + class="editor-button sidebar-item" + class:selected={viewMode === ViewMode.Editor} + on:click={() => setViewMode(ViewMode.Editor)} + > + Editor + </div> + <div + class="rules-button sidebar-item" + class:selected={viewMode === ViewMode.Rules} + on:click={() => setViewMode(ViewMode.Rules)} + > + Rules + </div> + <div + class="sidebar-item" + class:selected={viewMode === ViewMode.Leaderboard} + on:click={() => setViewMode(ViewMode.Leaderboard)} + > + Leaderboard + </div> + <div class="sidebar-header">match history</div> + <ul class="match-list"> + {#each matches as match} + <li + class="match-card sidebar-item" + on:click={() => selectMatch(match.id)} + class:selected={match.id === selectedMatchId} + > + <span class="match-timestamp">{formatMatchTimestamp(match.timestamp)}</span> + <!-- hex is hardcoded for now, don't show map name --> + <!-- <span class="match-mapname">hex</span> --> + <!-- ugly temporary hardcode --> + <span class="match-opponent">{match["players"][1]["bot_name"]}</span> + </li> + {/each} + </ul> + </div> + <div class="editor-container"> + {#if viewMode === ViewMode.MatchVisualizer} + <Visualizer matchData={selectedMatch} matchLog={selectedMatchLog} /> + {:else if viewMode === ViewMode.Editor} + <EditorView {editSession} /> + {:else if viewMode === ViewMode.Rules} + <RulesView /> + {:else if viewMode === ViewMode.Leaderboard} + <Leaderboard /> + {/if} + </div> + <div class="sidebar-right"> + {#if viewMode === ViewMode.MatchVisualizer} + <OutputPane matchLog={selectedMatchLog} /> + {:else if viewMode === ViewMode.Editor} + <SubmitPane {editSession} on:matchCreated={onMatchCreated} /> + {/if} + </div> +</div> + +<style lang="scss"> + @import "src/styles/variables.scss"; + + .container { + display: flex; + flex-grow: 1; + min-height: 0; + } + + .sidebar-left { + width: 240px; + background-color: $bg-color; + display: flex; + flex-direction: column; + } + .sidebar-right { + width: 400px; + background-color: white; + border-left: 1px solid; + padding: 0; + display: flex; + overflow: hidden; + } + .editor-container { + flex-grow: 1; + flex-shrink: 1; + overflow: hidden; + background-color: white; + } + + .editor-container { + height: 100%; + } + + .sidebar-item { + color: #eee; + padding: 15px; + } + + .sidebar-item:hover { + background-color: #333; + } + + .sidebar-item.selected { + background-color: #333; + } + + .match-list { + list-style: none; + color: #eee; + padding-top: 15px; + overflow-y: scroll; + padding-left: 0px; + } + + .match-card { + padding: 10px 15px; + font-size: 11pt; + } + + .match-timestamp { + color: #ccc; + } + + .match-opponent { + padding: 0 0.5em; + } + + .sidebar-header { + margin-top: 2em; + text-transform: uppercase; + font-weight: 600; + color: rgba(255, 255, 255, 0.7); + font-size: 14px; + font-family: "Open Sans", sans-serif; + padding-left: 14px; + } +</style> |