pull/251/merge
Jon Eugster 9 months ago
parent 71fad5699e
commit 8e3dfdea30

@ -49,10 +49,18 @@ function App() {
// mobile only: game intro should only be shown once and skipped afterwards
useEffect(() => {
if (openedIntro && !worldId && page == 0) {
if (worldId) {
console.log('setting page to 1')
setPage(1)
} else {
if (openedIntro && page == 0) {
console.log('setting page to 1')
setPage(1)
} else {
// setPage(0)
}
}, [openedIntro])
}
}, [openedIntro, worldId, levelId])
// option to pass language as `?lang=de` in the URL
useEffect(() => {
@ -96,11 +104,9 @@ function App() {
<PopupContext.Provider value={{popupContent, setPopupContent}}>
<PageContext.Provider value={{typewriterMode, setTypewriterMode, typewriterInput, setTypewriterInput, lockEditorMode, setLockEditorMode, page, setPage}}>
<Navigation />
<React.Suspense>
<Outlet />
</React.Suspense>
</PageContext.Provider>
{ popupContent && <Popup /> }
</PageContext.Provider>
</PopupContext.Provider>
</PreferencesContext.Provider>
</GameIdContext.Provider>

@ -0,0 +1,104 @@
import * as React from 'react'
import { PageContext, PreferencesContext } from './infoview/context'
import { GameIdContext } from '../app'
import { useTranslation } from 'react-i18next'
import { useAppDispatch } from '../hooks'
import { Hint } from './hints'
import { Button } from './button'
import { changedOpenedIntro } from '../state/progress'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faArrowRight } from '@fortawesome/free-solid-svg-icons'
import { useGetGameInfoQuery, useLoadLevelQuery } from '../state/api'
import { useContext, useEffect, useRef, useState } from 'react'
import '../css/level.css'
import '../css/chat.css'
/** Split a string by double newlines and filters out empty segments. */
function splitIntro (intro : string) {
return intro.split(/\n(\s*\n)+/).filter(t => t.trim())
}
/** The buttons at the bottom of chat */
export function ChatButtons () {
const { gameId, worldId, levelId } = useContext(GameIdContext)
const {setPage} = useContext(PageContext)
const dispatch = useAppDispatch()
const gameInfo = useGetGameInfoQuery({game: gameId})
return <div className="button-row">
{(!worldId || !levelId) &&
// Start button appears only on world selection and level 0.
<Button className="btn"
title=""
to={worldId ? `/${gameId}/world/${worldId}/level/1` : ''}
onClick={() => {
if (!worldId) {
console.log('setting `openedIntro` to true')
setPage(1)
dispatch(changedOpenedIntro({game: gameId, openedIntro: true}))
}
}} >
Start&nbsp;<FontAwesomeIcon icon={faArrowRight}/>
</Button>
}
</div>
}
/** the panel showing the game's introduction text */
export function ChatPanel ({visible = true}) {
let { t } = useTranslation()
const chatRef = useRef<HTMLDivElement>(null)
const { mobile } = useContext(PreferencesContext)
const { gameId, worldId, levelId } = useContext(GameIdContext)
const gameInfo = useGetGameInfoQuery({game: gameId})
const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
let [chatMessages, setChatMessages] = useState<Array<string>>([])
// Effect to clear chat and display the correct intro text
useEffect(() => {
if (levelId > 0) {
// playable level: show the level's intro
if (levelInfo.data?.introduction) {
setChatMessages([t(levelInfo.data?.introduction, {ns : gameId})])
}
else {
setChatMessages([])
}
} else {
if (worldId) {
// Level 0: show the world's intro
if (gameInfo.data?.worlds.nodes[worldId].introduction) {
setChatMessages(splitIntro(t(gameInfo.data?.worlds.nodes[worldId].introduction, {ns: gameId})))
} else {
setChatMessages([])
}
} else {
// world overview: show the game's intro
if (gameInfo.data?.introduction) {
setChatMessages(splitIntro(t(gameInfo.data?.introduction, {ns : gameId})))
} else {
setChatMessages([])
}
}
}
}, [gameInfo, levelInfo, gameId, worldId, levelId])
return <div className={`column chat-panel${visible ? '' : ' hidden'}`}>
<div ref={chatRef} className="chat">
{chatMessages.map(((t, i) =>
t.trim() ?
<Hint key={`intro-p-${i}`}
hint={{text: t, hidden: false, rawText: t, varNames: []}}
step={0} selected={null} toggleSelection={undefined} />
: <></>
))}
</div>
{ mobile && <ChatButtons /> }
</div>
}

@ -1,16 +1,101 @@
import i18next from "i18next"
import React from "react"
import { useParams } from "react-router-dom"
import { GameIdContext } from "../app"
import { useGetGameInfoQuery } from "../state/api"
import * as React from 'react'
import { useEffect, useRef } from 'react'
import Split from 'react-split'
import { Box, CircularProgress } from '@mui/material'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faArrowRight } from '@fortawesome/free-solid-svg-icons'
import { GameIdContext } from '../app'
import { useAppDispatch, useAppSelector } from '../hooks'
import { changedOpenedIntro, selectOpenedIntro } from '../state/progress'
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery, useLoadLevelQuery } from '../state/api'
import { Button } from './button'
import { PageContext, PreferencesContext } from './infoview/context'
import { InventoryPanel } from './inventory'
import { ErasePopup } from './popup/erase'
import { InfoPopup } from './popup/info'
import { PrivacyPolicyPopup } from './popup/privacy'
import { UploadPopup } from './popup/upload'
import { PreferencesPopup} from "./popup/preferences"
import { WorldTreePanel } from './world_tree'
import '../css/game.css'
import '../css/welcome.css'
import '../css/level.css'
import { Hint } from './hints'
import i18next from 'i18next'
import { useTranslation } from 'react-i18next'
import { LoadingIcon } from './utils'
import { ChatPanel } from './chat'
import { DualEditor } from './infoview/main'
import { Level } from './level'
/** main page of the game showing among others the tree of worlds/levels */
function Game() {
const params = useParams()
const levelId = parseInt(params.levelId)
const worldId = params.worldId
return <div>
<GameIdContext.Provider value={gameId}></GameIdContext.Provider>
const codeviewRef = useRef<HTMLDivElement>(null)
const { gameId, worldId, levelId } = React.useContext(GameIdContext)
// Load the namespace of the game
i18next.loadNamespaces(gameId)
const {mobile} = React.useContext(PreferencesContext)
const {isSavePreferences, language, setIsSavePreferences, setLanguage} = React.useContext(PreferencesContext)
const gameInfo = useGetGameInfoQuery({game: gameId})
const inventory = useLoadInventoryOverviewQuery({game: gameId})
const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
const {page, setPage} = React.useContext(PageContext)
// TODO: recover `openedIntro` functionality
// const [pageNumber, setPageNumber] = React.useState(openedIntro ? 1 : 0)
// pop-ups
const [eraseMenu, setEraseMenu] = React.useState(false)
const [impressum, setImpressum] = React.useState(false)
const [privacy, setPrivacy] = React.useState(false)
const [info, setInfo] = React.useState(false)
const [rulesHelp, setRulesHelp] = React.useState(false)
const [uploadMenu, setUploadMenu] = React.useState(false)
const [preferencesPopup, setPreferencesPopup] = React.useState(false)
// set the window title
useEffect(() => {
if (gameInfo.data?.title) {
window.document.title = gameInfo.data.title
}
}, [gameInfo.data?.title])
return mobile ?
<div className="app-content mobile">
{<>
<ChatPanel visible={worldId ? (levelId == 0 && page == 1) :(page == 0)} />
{ worldId ?
<Level visible={levelId > 0 && page == 1} /> :
<WorldTreePanel visible={page == 1} />
}
<InventoryPanel visible={page == 2} />
</>
}
</div>
:
<Split className="app-content" minSize={0} snapOffset={200} sizes={[25, 50, 25]}>
<ChatPanel />
<div>
{/* Note: apparently without this `div` the split panel bugs out. */}
{worldId ? <Level /> : <WorldTreePanel /> }
</div>
<InventoryPanel />
</Split>
}
export default Game
function useLevelEditor(codeviewRef: React.MutableRefObject<HTMLDivElement>, initialCode: any, initialSelections: any, onDidChangeContent: any, onDidChangeSelection: any): { editor: any; infoProvider: any; editorConnection: any } {
throw new Error('Function not implemented.')
}

@ -536,7 +536,7 @@ export function TypewriterInterface({props}) {
[DiagnosticSeverity.Hint]: 'hint',
}[diag.severity] : '';
return <div>
return <div key={diag.message} >
<div className={`${severityClass} ml1 message`}>
<p className="mv2">{t("Line")}&nbsp;{diag.range.start.line}, {t("Character")}&nbsp;{diag.range.start.character}</p>
<pre className="font-code pre-wrap">

@ -1,89 +1,119 @@
import * as React from 'react';
import { useState, useEffect } from 'react';
import * as React from 'react'
import { useState, useEffect, createContext, useContext } from 'react';
import '../css/inventory.css'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faLock, faBan, faCheck } from '@fortawesome/free-solid-svg-icons'
import { faLock, faBan, faCheck, faXmark } from '@fortawesome/free-solid-svg-icons'
import { faClipboard } from '@fortawesome/free-regular-svg-icons'
import { GameIdContext } from '../app';
import Markdown from './markdown';
import { useLoadDocQuery, InventoryTile, LevelInfo, InventoryOverview, useLoadInventoryOverviewQuery } from '../state/api';
import { useLoadDocQuery, InventoryTile, LevelInfo, InventoryOverview, useLoadInventoryOverviewQuery, useLoadLevelQuery } from '../state/api';
import { selectDifficulty, selectInventory } from '../state/progress';
import { store } from '../state/store';
import { useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { t } from 'i18next';
import { WorldLevelIdContext } from './infoview/context';
import { NavButton } from './navigation';
import { LoadingIcon } from './utils';
/** Context which manages the inventory */
const InventoryContext = createContext<{
theoremTab: string,
setTheoremTab: React.Dispatch<React.SetStateAction<string>>,
categoryTab: "tactic"|"theorem"|"definition",
setCategoryTab: React.Dispatch<React.SetStateAction<"tactic"|"theorem"|"definition">>,
docTile: any,
setDocTile: React.Dispatch<React.SetStateAction<any>>
}>({
theoremTab: null,
setTheoremTab: () => {},
categoryTab: "tactic",
setCategoryTab: () => {},
docTile: null,
setDocTile: () => {}
})
export function Inventory({levelInfo, openDoc, lemmaTab, setLemmaTab, enableAll=false} :
{
levelInfo: LevelInfo|InventoryOverview,
openDoc: (props: {name: string, type: string}) => void,
lemmaTab: any,
setLemmaTab: any,
enableAll?: boolean,
}) {
const { t } = useTranslation()
// TODO: this state should be preserved when loading a different level.
const [tab, setTab] = useState<"tactic"|"theorem"|"definition">("theorem")
/**
*/
function InventoryItem({item, name, displayName, locked, disabled, newly, showDoc, recent=false }) {
const icon = locked ? <FontAwesomeIcon icon={faLock} /> :
disabled ? <FontAwesomeIcon icon={faBan} /> : item.st
const className = locked ? "locked" : disabled ? "disabled" : newly ? "new" : ""
// Note: This is somewhat a hack as the statement of lemmas comes currently in the form
// `Namespace.statement_name (x y : Nat) : some type`
const title = locked ? t("Not unlocked yet") :
disabled ? t("Not available in this level") : (item.altTitle ? item.altTitle.substring(item.altTitle.indexOf(' ') + 1) : '')
const { gameId } = React.useContext(GameIdContext)
const difficulty = useSelector(selectDifficulty(gameId))
let newTheorems = levelInfo?.lemmas?.filter(item => item.new).length > 0
let newTactics = levelInfo?.tactics?.filter(item => item.new).length > 0
let newDefinitions = levelInfo?.definitions?.filter(item => item.new).length > 0
// local state to show checkmark after pressing the copy button
const [copied, setCopied] = useState(false)
return (
<div className="inventory">
<div className="tab-bar major">
<div className={`tab${(tab == "theorem") ? " active": ""}${newTheorems ? " new" : ""}`} onClick={() => { setTab("theorem") }}>{t("Theorems")}</div>
<div className={`tab${(tab == "tactic") ? " active": ""}${newTactics ? " new" : ""}`} onClick={() => { setTab("tactic") }}>{t("Tactics")}</div>
<div className={`tab${(tab == "definition") ? " active": ""}${newDefinitions ? " new" : ""}`} onClick={() => { setTab("definition") }}>{t("Definitions")}</div>
</div>
{ (tab == "theorem") && levelInfo?.lemmas &&
<InventoryList items={levelInfo?.lemmas} docType="Lemma" openDoc={openDoc} level={levelInfo} enableAll={enableAll} tab={lemmaTab} setTab={setLemmaTab}/>
}
{ (tab == "tactic") && levelInfo?.tactics &&
<InventoryList items={levelInfo?.tactics} docType="Tactic" openDoc={openDoc} enableAll={enableAll}/>
const handleClick = () => {
// if ((difficulty == 0) || !locked) {
showDoc()
// }
}
{ (tab == "definition") && levelInfo?.definitions &&
<InventoryList items={levelInfo?.definitions} docType="Definition" openDoc={openDoc} enableAll={enableAll}/>
const copyItemName = (ev) => {
navigator.clipboard.writeText(displayName)
setCopied(true)
setInterval(() => {
setCopied(false)
}, 3000);
ev.stopPropagation()
}
return <div className={`item ${className}${(difficulty == 0) ? ' enabled' : ''}${recent ? ' recent' : ''}`} onClick={handleClick} title={title}>
{icon} {displayName}
<div className="copy-button" onClick={copyItemName}>
{copied ? <FontAwesomeIcon icon={faCheck} /> : <FontAwesomeIcon icon={faClipboard} />}
</div>
</div>
)
}
function InventoryList({items, docType, openDoc, tab=null, setTab=undefined, level=undefined, enableAll=false} :
function InventoryList({ items, tab=null, setTab=()=>{} } :
{
items: InventoryTile[],
docType: string,
openDoc(props: {name: string, type: string}): void,
tab?: any,
setTab?: any,
level?: LevelInfo|InventoryOverview,
enableAll?: boolean,
tab?: string,
setTab?: React.Dispatch<React.SetStateAction<string>>
}) {
// TODO: `level` is only used in the `useEffect` below to check if a new level has
// been loaded. Is there a better way to observe this?
const {gameId} = React.useContext(GameIdContext)
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
const { gameId, worldId, levelId } = React.useContext(GameIdContext)
const { setDocTile, categoryTab, setCategoryTab } = useContext(InventoryContext)
const difficulty = useSelector(selectDifficulty(gameId))
const inventory: string[] = selectInventory(gameId)(store.getState())
const [categories, setCategories] = useState<Array<string>>([])
const [modifiedItems, setModifiedItems] = useState<Array<InventoryTile>>([])
const [recentItems, setRecentItems] = useState<Array<InventoryTile>>([])
useEffect(() => {
const categorySet = new Set<string>()
if (!items) {return}
for (let item of items) {
categorySet.add(item.category)
}
const categories = Array.from(categorySet).sort()
setCategories(Array.from(categorySet).sort())
// Add inventory items from local store as unlocked.
// Items are unlocked if they are in the local store, or if the server says they should be
// given the dependency graph. (OR-connection) (TODO: maybe add different logic for different
// modi)
let inv: string[] = selectInventory(gameId)(store.getState())
let modifiedItems : InventoryTile[] = items.map(tile => inv.includes(tile.name) ? {...tile, locked: false} : tile)
let _modifiedItems : InventoryTile[] = items?.map(tile => inventory.includes(tile.name) ? {...tile, locked: false} : tile)
setModifiedItems(_modifiedItems)
// Item(s) proved in the preceeding level
let recentItems = modifiedItems.filter(x => x.world == worldId && x.level == levelId - 1)
setRecentItems(_modifiedItems.filter(x => x.world == worldId && x.level == levelId - 1))
}, [items, inventory])
return <>
{ categories.length > 1 &&
@ -97,91 +127,109 @@ function InventoryList({items, docType, openDoc, tab=null, setTab=undefined, lev
{[...modifiedItems].sort(
// For lemas, sort entries `available > disabled > locked`
// otherwise alphabetically
(x, y) => +(docType == "Lemma") * (+x.locked - +y.locked || +x.disabled - +y.disabled) || x.displayName.localeCompare(y.displayName)
(x, y) => +(categoryTab == "theorem") * (+x.locked - +y.locked || +x.disabled - +y.disabled) || x.displayName.localeCompare(y.displayName)
).filter(item => !item.hidden && ((tab ?? categories[0]) == item.category)).map((item, i) => {
return <InventoryItem key={`${item.category}-${item.name}`}
item={item}
showDoc={() => {openDoc({name: item.name, type: docType})}}
showDoc={() => {setDocTile(item)}}
name={item.name} displayName={item.displayName} locked={difficulty > 0 ? item.locked : false}
disabled={item.disabled}
recent={recentItems.map(x => x.name).includes(item.name)}
newly={item.new} enableAll={enableAll} />
newly={item.new} />
})
}
</div>
</>
}
function InventoryItem({item, name, displayName, locked, disabled, newly, showDoc, recent=false, enableAll=false}) {
const icon = locked ? <FontAwesomeIcon icon={faLock} /> :
disabled ? <FontAwesomeIcon icon={faBan} /> : item.st
const className = locked ? "locked" : disabled ? "disabled" : newly ? "new" : ""
// Note: This is somewhat a hack as the statement of lemmas comes currently in the form
// `Namespace.statement_name (x y : Nat) : some type`
const title = locked ? t("Not unlocked yet") :
disabled ? t("Not available in this level") : (item.altTitle ? item.altTitle.substring(item.altTitle.indexOf(' ') + 1) : '')
const [copied, setCopied] = useState(false)
const handleClick = () => {
if (enableAll || !locked) {
showDoc()
}
}
/** The `Inventory` shows all items present in the game sorted by item type. */
export function Inventory () {
const { t } = useTranslation()
const copyItemName = (ev) => {
navigator.clipboard.writeText(displayName)
setCopied(true)
setInterval(() => {
setCopied(false)
}, 3000);
ev.stopPropagation()
const { gameId, worldId, levelId } = React.useContext(GameIdContext)
const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
let { theoremTab, setTheoremTab, categoryTab, setCategoryTab } = useContext(InventoryContext)
/** Helper function to find if a list of tiles comprises any new elements. */
function containsNew(tiles: InventoryTile[]) {
console.log(tiles)
return tiles?.filter(item => item.new).length > 0
}
return <div className={`item ${className}${enableAll ? ' enabled' : ''}${recent ? ' recent' : ''}`} onClick={handleClick} title={title}>
{icon} {displayName}
<div className="copy-button" onClick={copyItemName}>
{copied ? <FontAwesomeIcon icon={faCheck} /> : <FontAwesomeIcon icon={faClipboard} />}
return (
<div className="inventory">
{ levelInfo.data ? <>
<div className="tab-bar major">
<div className={`tab${(categoryTab == "theorem") ? " active": ""}${containsNew(levelInfo.data?.lemmas) ? " new" : ""}`} onClick={() => { setCategoryTab("theorem") }}>{t("Theorems")}</div>
<div className={`tab${(categoryTab == "tactic") ? " active": ""}${containsNew(levelInfo.data?.tactics) ? " new" : ""}`} onClick={() => { setCategoryTab("tactic") }}>{t("Tactics")}</div>
<div className={`tab${(categoryTab == "definition") ? " active": ""}${containsNew(levelInfo.data?.definitions) ? " new" : ""}`} onClick={() => { setCategoryTab("definition") }}>{t("Definitions")}</div>
</div>
{ (categoryTab == "theorem") &&
<InventoryList items={levelInfo.data?.lemmas} tab={theoremTab} setTab={setTheoremTab} />
}
{ (categoryTab == "tactic") &&
<InventoryList items={levelInfo.data?.tactics} />
}
{ (categoryTab == "definition") &&
<InventoryList items={levelInfo.data?.definitions} />
}
</> : <LoadingIcon /> }
</div>
)
}
export function Documentation({name, type, handleClose}) {
/** The `documentation` */
export function Documentation() {
const { gameId } = React.useContext(GameIdContext)
const doc = useLoadDocQuery({game: gameId, type: type, name: name})
// const docEntry = useLoadDocQuery({game: gameId, type: type, name: name})
let { docTile, setDocTile } = useContext(InventoryContext)
const docEntry = useLoadDocQuery({game: gameId, name: docTile.name})
// Set `inventoryDoc` to `null` to close the doc
function closeInventoryDoc() { setDocTile(null) }
return <div className="documentation">
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
<h1 className="doc">{doc.data?.displayName}</h1>
<p><code>{doc.data?.statement}</code></p>
{/* <code>docstring: {doc.data?.docstring}</code> */}
<Markdown>{t(doc.data?.content, {ns: gameId})}</Markdown>
<NavButton icon={faXmark}
onClick={closeInventoryDoc}
inverted={true} />
<h1 className="doc">{docTile.data?.displayName}</h1>
<p><code>{docEntry.data?.statement}</code></p>
<Markdown>{t(docEntry.data?.content, {ns: gameId})}</Markdown>
</div>
}
/** The panel (on the welcome page) showing the user's inventory with tactics, definitions, and lemmas */
export function InventoryPanel({levelInfo, visible = true}) {
const {gameId} = React.useContext(GameIdContext)
const [lemmaTab, setLemmaTab] = useState(levelInfo?.lemmaTab)
/** The panel showing the user's inventory with tactics, definitions, and lemmas */
export function InventoryPanel({visible = true}) {
const {gameId, worldId, levelId} = React.useContext(GameIdContext)
const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
const inventory = useLoadInventoryOverviewQuery({game: gameId})
const [theoremTab, setTheoremTab] = useState<string>(null)
const [categoryTab, setCategoryTab] = useState<"tactic"|"theorem"|"definition">('tactic')
// The inventory is overlayed by the doc entry of a clicked item
const [inventoryDoc, setInventoryDoc] = useState<{name: string, type: string}>(null)
// Set `inventoryDoc` to `null` to close the doc
function closeInventoryDoc() {setInventoryDoc(null)}
const [docTile, setDocTile] = useState<InventoryTile>(null)
useEffect(() => {
// If the level specifies `LemmaTab "Nat"`, we switch to this tab on loading.
// `defaultTab` is `null` or `undefined` otherwise, in which case we don't want to switch.
if (levelInfo?.lemmaTab) {
setLemmaTab(levelInfo?.lemmaTab)
if (levelInfo?.data?.lemmaTab) {
setTheoremTab(levelInfo?.data?.lemmaTab)
}}, [levelInfo])
return <div className={`column inventory-panel ${visible ? '' : 'hidden'}`}>
{inventoryDoc ?
<Documentation name={inventoryDoc.name} type={inventoryDoc.type} handleClose={closeInventoryDoc}/>
<InventoryContext.Provider value={{theoremTab, setTheoremTab, categoryTab, setCategoryTab, docTile, setDocTile }}>
{docTile ?
<Documentation />
:
<Inventory levelInfo={levelInfo} openDoc={setInventoryDoc} enableAll={true} lemmaTab={lemmaTab} setLemmaTab={setLemmaTab}/>
<Inventory />
}
</InventoryContext.Provider>
</div>
}
// HERE: next up: locked items should not be disabled!

@ -109,7 +109,9 @@ function LandingPage() {
</p>
</div>
</header>
<React.Suspense>
<div className="game-list">
{allTiles.filter(x => x != null).length == 0 ?
<p>
<Trans>
@ -126,6 +128,7 @@ function LandingPage() {
))
}
</div>
</React.Suspense>
<section>
<div className="wrapper">
<h2>{t("Development notes")}</h2>

@ -53,12 +53,13 @@ import { InfoPopup } from './popup/info'
import { PreferencesPopup } from './popup/preferences'
import { useTranslation } from 'react-i18next'
import i18next from 'i18next'
import { ChatButtons } from './chat'
monacoSetup()
function Level() {
const params = useParams()
export function Level({visible = true}) {
// const params = useParams()
// const levelId = parseInt(params.levelId)
// const worldId = params.worldId
@ -87,11 +88,11 @@ function Level() {
useEffect(() => {}, [])
return <WorldLevelIdContext.Provider value={{worldId, levelId}}>
{levelId == 0 ?
<Introduction impressum={impressum} setImpressum={setImpressum} privacy={privacy} setPrivacy={setPrivacy} toggleInfo={toggleInfo} togglePreferencesPopup={togglePreferencesPopup} /> :
<PlayableLevel key={`${worldId}/${levelId}`} impressum={impressum} setImpressum={setImpressum} privacy={privacy} setPrivacy={setPrivacy} toggleInfo={toggleInfo} togglePreferencesPopup={togglePreferencesPopup}/>}
return <div className={visible?'':'hidden'}>
<WorldLevelIdContext.Provider value={{worldId, levelId}} >
<PlayableLevel key={`${worldId}/${levelId}`} />
</WorldLevelIdContext.Provider>
</div>
}
function ChatPanel({lastLevel, visible = true}) {
@ -199,7 +200,7 @@ function ChatPanel({lastLevel, visible = true}) {
}
function ExercisePanel({codeviewRef, visible=true}: {codeviewRef: React.MutableRefObject<HTMLDivElement>, visible?: boolean}) {
export function ExercisePanel({codeviewRef, visible=true}: {codeviewRef: React.MutableRefObject<HTMLDivElement>, visible?: boolean}) {
const {gameId} = React.useContext(GameIdContext)
const {worldId, levelId} = useContext(WorldLevelIdContext)
const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
@ -211,7 +212,7 @@ function ExercisePanel({codeviewRef, visible=true}: {codeviewRef: React.MutableR
</div>
}
function PlayableLevel({impressum, setImpressum, privacy, setPrivacy, toggleInfo, togglePreferencesPopup}) {
export function PlayableLevel() {
let { t } = useTranslation()
const codeviewRef = useRef<HTMLDivElement>(null)
const {gameId} = React.useContext(GameIdContext)
@ -248,9 +249,9 @@ function PlayableLevel({impressum, setImpressum, privacy, setPrivacy, toggleInfo
const [typewriterInput, setTypewriterInput] = useState("")
const lastLevel = levelId >= gameInfo.data?.worldSize[worldId]
// impressum pop-up
function toggleImpressum() {setImpressum(!impressum)}
function togglePrivacy() {setPrivacy(!privacy)}
// // impressum pop-up
// function toggleImpressum() {setImpressum(!impressum)}
// function togglePrivacy() {setPrivacy(!privacy)}
// When clicking on an inventory item, the inventory is overlayed by the item's doc.
// If this state is set to a pair `(name, type)` then the according doc will be open.
@ -424,24 +425,7 @@ function PlayableLevel({impressum, setImpressum, privacy, setPrivacy, toggleInfo
toggleInfo={toggleInfo}
togglePreferencesPopup={togglePreferencesPopup}
/> */}
{mobile?
// TODO: This is copied from the `Split` component below...
<>
<div className={`app-content level-mobile ${level.isLoading ? 'hidden' : ''}`}>
<ExercisePanel
codeviewRef={codeviewRef}
visible={page == 0} />
<InventoryPanel levelInfo={level?.data} visible={page == 1} />
</div>
</>
:
<Split minSize={0} snapOffset={200} sizes={[25, 50, 25]} className={`app-content level ${level.isLoading ? 'hidden' : ''}`}>
<ChatPanel lastLevel={lastLevel}/>
<ExercisePanel
codeviewRef={codeviewRef} />
<InventoryPanel levelInfo={level?.data} />
</Split>
}
<ExercisePanel codeviewRef={codeviewRef} />
</MonacoEditorContext.Provider>
</EditorContext.Provider>
</ProofContext.Provider>
@ -451,6 +435,11 @@ function PlayableLevel({impressum, setImpressum, privacy, setPrivacy, toggleInfo
</>
}
// <Split minSize={0} snapOffset={200} sizes={[25, 75]} className={`app-content level ${level.isLoading ? 'hidden' : ''}`}>
// <ChatPanel lastLevel={lastLevel}/>
// <InventoryPanel />
// </Split>
function IntroductionPanel({gameInfo}) {
let { t } = useTranslation()
const {gameId} = React.useContext(GameIdContext)
@ -466,21 +455,14 @@ function IntroductionPanel({gameInfo}) {
hint={{text: t, hidden: false, rawText: t, varNames: []}} step={0} selected={null} toggleSelection={undefined} />
))}
</div>
<div className={`button-row${mobile ? ' mobile' : ''}`}>
{gameInfo.data?.worldSize[worldId] == 0 ?
<Button to={`/${gameId}`}><FontAwesomeIcon icon={faHome} /></Button> :
<Button to={`/${gameId}/world/${worldId}/level/1`}>
{t("Start")}&nbsp;<FontAwesomeIcon icon={faArrowRight} />
</Button>
}
</div>
<ChatButtons />
</div>
}
export default Level
/** The site with the introduction text of a world */
function Introduction({impressum, setImpressum, privacy, setPrivacy, toggleInfo, togglePreferencesPopup}) {
function Introduction() {
let { t } = useTranslation()
const {gameId} = React.useContext(GameIdContext)
@ -495,12 +477,12 @@ function Introduction({impressum, setImpressum, privacy, setPrivacy, toggleInfo,
let image: string = gameInfo.data?.worlds.nodes[worldId].image
const toggleImpressum = () => {
setImpressum(!impressum)
}
const togglePrivacy = () => {
setPrivacy(!privacy)
}
// const toggleImpressum = () => {
// setImpressum(!impressum)
// }
// const togglePrivacy = () => {
// setPrivacy(!privacy)
// }
return <>
{/* <LevelAppBar isLoading={gameInfo.isLoading} levelTitle={t("Introduction")} toggleImpressum={toggleImpressum} togglePrivacy={togglePrivacy} toggleInfo={toggleInfo} togglePreferencesPopup={togglePreferencesPopup}/> */}
{gameInfo.isLoading ?
@ -508,16 +490,16 @@ function Introduction({impressum, setImpressum, privacy, setPrivacy, toggleInfo,
: mobile ?
<IntroductionPanel gameInfo={gameInfo} />
:
<Split minSize={0} snapOffset={200} sizes={[25, 50, 25]} className={`app-content level`}>
<IntroductionPanel gameInfo={gameInfo} />
// <Split minSize={0} snapOffset={200} sizes={[25, 50, 25]} className={`app-content level`}>
// <IntroductionPanel gameInfo={gameInfo} />
<div className="world-image-container empty center">
{image &&
<img className="contain" src={path.join("data", gameId, image)} alt="" />
}
</div>
<InventoryPanel levelInfo={inventory?.data} />
</Split>
// {/* <InventoryPanel /> */}
// </Split>
}
</>

@ -12,9 +12,10 @@ import { useTranslation } from 'react-i18next'
import '../css/navigation.css'
import { PopupContext } from './popup/popup'
import { useSelector } from 'react-redux'
import { selectProgress } from '../state/progress'
import { selectCompleted, selectDifficulty, selectProgress } from '../state/progress'
import lean4gameConfig from '../config.json'
import { Flag } from './flag'
import { useAppSelector } from '../hooks'
/** SVG github icon */
function GithubIcon () {
@ -119,6 +120,8 @@ function DesktopNavigationLevel () {
const { typewriterMode, setTypewriterMode, lockEditorMode } = useContext(PageContext)
const gameInfo = useGetGameInfoQuery({game: gameId})
const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
const difficulty = useSelector(selectDifficulty(gameId))
const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
/** toggle input mode if allowed */
function toggleInputMode(ev: React.MouseEvent) {
@ -163,10 +166,12 @@ function DesktopNavigationLevel () {
icon={faHome}
text={t("Leave World")}
inverted={true}
disabled={difficulty == 0 || !completed}
href={`#/${gameId}`} /> :
<NavButton
icon={faArrowRight}
text={levelId == 0 ? t("Start") : t("Next")} inverted={true}
disabled={difficulty == 0 || !completed}
href={`#/${gameId}/world/${worldId}/level/${levelId + 1}`} />
}
{ levelId > 0 &&
@ -203,8 +208,8 @@ function MobileNavigationLevel () {
</div>
<div className="nav-title-right">
<NavButton
icon={page?faBookOpen:faBook}
onClick={() => setPage(page?0:1)}
icon={(page == 1) ? faBook : faBookOpen}
onClick={() => setPage((page == 1) ? 2 : 1)}
inverted={true} />
</div>
</div>
@ -213,17 +218,29 @@ function MobileNavigationLevel () {
/** The skeleton of the navigation which is the same across all layouts. */
export function Navigation () {
const { t, i18n } = useTranslation()
const { gameId, worldId } = useContext(GameIdContext)
const { gameId, worldId, levelId } = useContext(GameIdContext)
const { mobile, language, setLanguage } = useContext(PreferencesContext)
const { setPopupContent } = useContext(PopupContext)
const { typewriterMode, setTypewriterMode, lockEditorMode } = useContext(PageContext)
const gameProgress = useSelector(selectProgress(gameId))
const gameInfo = useGetGameInfoQuery({game: gameId})
const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
const difficulty = useSelector(selectDifficulty(gameId))
const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
const [navOpen, setNavOpen] = useState(false)
const [langNavOpen, setLangNavOpen] = useState(false)
function toggleNav () {setNavOpen(!navOpen); setLangNavOpen(false)}
function toggleLangNav () {setLangNavOpen(!langNavOpen); setNavOpen(false)}
/** toggle input mode if allowed */
function toggleInputMode(ev: React.MouseEvent) {
if (!lockEditorMode) {
setTypewriterMode(!typewriterMode)
console.log('test')
}
}
return <nav>
<NavigationContext.Provider value={{navOpen, setNavOpen}}>
{ gameId && <>
@ -280,6 +297,35 @@ export function Navigation () {
{ navOpen &&
<div className='dropdown' onClick={toggleNav} >
{ gameId && <>
{ mobile && (levelId == gameInfo.data?.worldSize[worldId] ?
<NavButton
icon={faHome}
text={t("Leave World")}
inverted={true}
disabled={difficulty == 0 || !completed}
href={`#/${gameId}`} /> :
<NavButton
icon={faArrowRight}
text={levelId == 0 ? t("Start") : t("Next")} inverted={true}
disabled={difficulty == 0 || !completed}
href={`#/${gameId}/world/${worldId}/level/${levelId + 1}`} />
)}
{mobile && levelId > 0 &&
<NavButton
icon={faArrowLeft}
text={t("Previous")}
inverted={true}
href={`#/${gameId}/world/${worldId}/level/${levelId - 1}`} />
}
{ mobile && levelId > 0 &&
<NavButton
icon={(typewriterMode && !lockEditorMode) ? faCode : faTerminal}
inverted={true}
text={typewriterMode ? t("Editor Mode") : t("Typewriter Mode")}
disabled={levelId == 0 || lockEditorMode}
onClick={(ev) => toggleInputMode(ev)}
title={lockEditorMode ? t("Editor mode is enforced!") : typewriterMode ? t("Editor mode") : t("Typewriter mode")} />
}
<NavButton
icon={faCircleInfo}
text={t("Game Info")}

@ -4,10 +4,11 @@ import { GameIdContext } from '../../app'
import { useAppDispatch } from '../../hooks'
import { deleteProgress, selectProgress } from '../../state/progress'
import { downloadFile } from '../world_tree'
import { Button } from '../button'
import { Button, Button2 } from '../button'
import { Trans, useTranslation } from 'react-i18next'
import { useContext } from 'react'
import { PopupContext } from './popup'
import { PageContext } from '../infoview/context'
/** download the current progress (i.e. what's saved in the browser store) */
export function downloadProgress(gameId: string, gameProgress) {
@ -28,6 +29,7 @@ export function downloadProgress(gameId: string, gameProgress) {
export function ErasePopup () {
let { t } = useTranslation()
const {gameId} = React.useContext(GameIdContext)
const {setPage} = useContext(PageContext)
const gameProgress = useSelector(selectProgress(gameId))
const dispatch = useAppDispatch()
const { setPopupContent } = useContext(PopupContext)
@ -35,7 +37,8 @@ export function ErasePopup () {
const eraseProgress = (ev) => {
dispatch(deleteProgress({game: gameId}))
setPopupContent(null)
ev.preventDefault() // TODO: this is a hack to prevent the buttons below from opening a link
setPage(0)
// ev.preventDefault() // TODO: this is a hack to prevent the buttons below from opening a link
}
const downloadAndErase = (ev) => {
@ -52,8 +55,8 @@ export function ErasePopup () {
Saves from other games are not deleted.)
</p>
</Trans>
<Button onClick={eraseProgress} to="">{t("Delete")}</Button>
<Button onClick={downloadAndErase} to="">{t("Download & Delete")}</Button>
<Button onClick={eraseProgress} to={`/${gameId}/`}>{t("Delete")}</Button>
<Button onClick={downloadAndErase} to={`/${gameId}/`}>{t("Download & Delete")}</Button>
<Button onClick={(ev) => {setPopupContent(null); ev.preventDefault()}} to="">{t("Cancel")}</Button>
</>
}

@ -8,6 +8,8 @@ import { PreferencesPopup } from './preferences'
import { UploadPopup } from './upload'
import { RulesPopup } from './rules'
import '../../css/popup.css'
import { NavButton } from '../navigation'
import { faXmark } from '@fortawesome/free-solid-svg-icons'
/** The context which manages if a popup is shown.
* If `popupContent` is `null`, the popup is closed.
@ -46,6 +48,9 @@ export function Popup () {
return <div className="modal-wrapper">
<div className="modal-backdrop" onClick={closePopup} />
<div className="modal">
<NavButton icon={faXmark}
onClick={closePopup}
inverted={true} />
<div className="codicon codicon-close modal-close" onClick={closePopup}></div>
{Popups[popupContent]}
</div>

@ -0,0 +1,8 @@
import * as React from 'react';
import { Box, CircularProgress } from "@mui/material";
export function LoadingIcon () {
return <Box display="flex" alignItems="center" justifyContent="center" sx={{ flex: 1, height: "calc(100vh - 64px)" }}>
<CircularProgress />
</Box>
}

@ -133,7 +133,7 @@ function Welcome() {
<WorldTreePanel worlds={gameInfo.data?.worlds} worldSize={gameInfo.data?.worldSize}
rulesHelp={rulesHelp} setRulesHelp={setRulesHelp} />
:
<InventoryPanel levelInfo={inventory?.data} />
<InventoryPanel />
)}
</div>
:
@ -141,7 +141,7 @@ function Welcome() {
<IntroductionPanel introduction={gameInfo.data?.introduction} setPageNumber={setPage} />
<WorldTreePanel worlds={gameInfo.data?.worlds} worldSize={gameInfo.data?.worldSize}
rulesHelp={rulesHelp} setRulesHelp={setRulesHelp} />
<InventoryPanel levelInfo={inventory?.data} />
<InventoryPanel />
</Split>
}
</div>

@ -18,6 +18,8 @@ import { store } from '../state/store'
import '../css/world_tree.css'
import { PreferencesContext } from './infoview/context'
import { useTranslation } from 'react-i18next'
import { useGetGameInfoQuery } from '../state/api'
import { LoadingIcon } from './utils'
// Settings for the world tree
cytoscape.use( klay )
@ -271,15 +273,14 @@ export function computeWorldLayout(worlds) {
}
export function WorldTreePanel({worlds, worldSize, rulesHelp, setRulesHelp}:
{ worlds: any,
worldSize: any,
rulesHelp: boolean,
setRulesHelp: any,
}) {
export function WorldTreePanel ({visible = true}) {
const {gameId} = React.useContext(GameIdContext)
const difficulty = useSelector(selectDifficulty(gameId))
const {nodes, bounds}: any = worlds ? computeWorldLayout(worlds) : {nodes: []}
const gameInfo = useGetGameInfoQuery({game: gameId})
const {nodes, bounds}: any = gameInfo.data?.worlds ? computeWorldLayout(gameInfo.data?.worlds) : {nodes: []}
// scroll to playable world
React.useEffect(() => {
@ -292,7 +293,7 @@ export function WorldTreePanel({worlds, worldSize, rulesHelp, setRulesHelp}:
console.debug(`scrolling to ${elem.textContent}`)
elem.scrollIntoView({block: "center"})
}
}, [worlds, worldSize])
}, [gameInfo])
let svgElements = []
@ -301,18 +302,18 @@ export function WorldTreePanel({worlds, worldSize, rulesHelp, setRulesHelp}:
// Indices `1, …, n` indicate if the corresponding level is completed
var completed = {}
if (worlds && worldSize) {
if (gameInfo.data?.worlds && gameInfo.data?.worldSize) {
// Fill `completed` with the level data.
for (let worldId in nodes) {
completed[worldId] = Array.from({ length: worldSize[worldId] + 1 }, (_, i) => {
completed[worldId] = Array.from({ length: gameInfo.data?.worldSize[worldId] + 1 }, (_, i) => {
// index `0` starts off as `true` but can be set to `false` by any edge with non-completed source
return i == 0 || selectCompleted(gameId, worldId, i)(store.getState())
})
}
// draw all connecting paths
for (let i in worlds.edges) {
const edge = worlds.edges[i]
for (let i in gameInfo.data?.worlds.edges) {
const edge = gameInfo.data?.worlds.edges[i]
let sourceCompleted = completed[edge[0]].slice(1).every(Boolean)
// if the origin world is not completed, mark the target world as non-playable
if (!sourceCompleted) {completed[edge[1]][0] = false}
@ -332,11 +333,11 @@ export function WorldTreePanel({worlds, worldSize, rulesHelp, setRulesHelp}:
completedLevels={completed[worldId]}
difficulty={difficulty}
key={`${gameId}-${worldId}`}
worldSize={worldSize[worldId]}
worldSize={gameInfo.data?.worldSize[worldId]}
/>
)
for (let i = 1; i <= worldSize[worldId]; i++) {
for (let i = 1; i <= gameInfo.data?.worldSize[worldId]; i++) {
svgElements.push(
<LevelIcon
world={worldId}
@ -345,7 +346,7 @@ export function WorldTreePanel({worlds, worldSize, rulesHelp, setRulesHelp}:
completed={completed[worldId][i]}
unlocked={completed[worldId][i-1]}
key={`${gameId}-${worldId}-${i}`}
worldSize={worldSize[worldId]}
worldSize={gameInfo.data?.worldSize[worldId]}
/>
)
}
@ -359,13 +360,14 @@ export function WorldTreePanel({worlds, worldSize, rulesHelp, setRulesHelp}:
let dx = bounds ? s*(bounds.x2 - bounds.x1) + 2*padding : null
return <div className="column">
return <div className={`column${visible ? '' : ' hidden'}`}>
{/* <WorldSelectionMenu rulesHelp={rulesHelp} setRulesHelp={setRulesHelp} /> */}
{ gameInfo.data ?
<svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink"
width={bounds ? `${ds * dx}` : ''}
viewBox={bounds ? `${s*bounds.x1 - padding} ${s*bounds.y1 - padding} ${dx} ${s*(bounds.y2 - bounds.y1) + 2 * padding}` : ''}
className="world-selection" >
{svgElements}
</svg>
</svg> : <LoadingIcon/> }
</div>
}

@ -17,6 +17,10 @@ body {
-moz-osx-font-smoothing: grayscale;
}
.hidden {
display: none !important;
}
.markdown code {
color: rgba(0, 32, 90, 0.87);
background-color: rgba(223, 227, 234);
@ -126,14 +130,6 @@ em {
/* margin: 0 1em; */
}
.app-content {
height: 100%;
flex: 1;
min-height: 0;
display: flex;
}
.markdown li ul, .markdown li ol {
margin:0 1.5em;
}

@ -0,0 +1,24 @@
.message {
margin: 10px 0;
padding: 5px 10px;
border-radius: 3px 3px 3px 3px;
}
.message.information, .message.info {
/* color: #059; */
color: #000;
background-color: #DDF6FF;
}
.message.warning {
color: #9F6000;
background-color: #FEEFB3;
}
.message.error {
color: #D8000C;
background-color: #FFBABA;
}
.message.deleted-hint {
background-color: #eee;
color: #777;
box-shadow: .0em .0em .5em .2em #eee;
}

@ -1,5 +1,5 @@
#error-page {
height: 100%;
height: 100vh;
background-image: url("/RoboSurprised.png");
background-repeat: no-repeat;
background-size: contain;

@ -0,0 +1,33 @@
.app-content {
height: 100%;
flex: 1;
min-height: 0;
display: flex;
}
.gutter {
background-color: #eee;
background-repeat: no-repeat;
background-position: 50%;
}
.gutter.gutter-vertical {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=');
}
.gutter.gutter-horizontal {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==');
}
.column {
width: 100%;
}
.slider .column {
height: 100%;
overflow: auto;
position: relative;
scroll-behavior: smooth;
}

@ -1,28 +1,4 @@
.message {
margin: 10px 0;
padding: 5px 10px;
border-radius: 3px 3px 3px 3px;
}
.message.information, .message.info {
/* color: #059; */
color: #000;
background-color: #DDF6FF;
}
.message.warning {
color: #9F6000;
background-color: #FEEFB3;
}
.message.error {
color: #D8000C;
background-color: #FFBABA;
}
.message.deleted-hint {
background-color: #eee;
color: #777;
box-shadow: .0em .0em .5em .2em #eee;
}
.hyp-group {
margin-bottom: 0.3em;

@ -5,25 +5,6 @@
/* display: flex; */
}
.hidden {
display: none;
}
.gutter {
background-color: #eee;
background-repeat: no-repeat;
background-position: 50%;
}
.gutter.gutter-vertical {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=');
}
.gutter.gutter-horizontal {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==');
}
.inventory-panel, .exercise-panel, .doc-panel, .introduction-panel {
height: 100%;
width: 100%;
@ -213,17 +194,26 @@ td code {
.chat-panel .button-row {
/* width:100%; */
margin-left: .5em;
margin-right: .5em;
/* margin-left: .5em;
margin-right: .5em; */
min-height: 2.5em;
border-top: 0.1em solid #aaa;
display: flex;
padding-top: .2rem;
margin: .5rem;
}
.chat-panel .btn {
margin-top: 1rem;
flex: 1;
/* margin-top: 1rem;
margin-bottom: 1rem;
margin-left: .5rem;
margin-right: .5rem;
margin-right: .5rem; */
text-align: center;
margin: 0;
padding: .5em;
}
/* .exercise-panel {
@ -232,7 +222,7 @@ td code {
height: 100%;
} */
.button-row.mobile {
/* .button-row.mobile {
margin: .5rem;
padding-top: .2rem;
}
@ -243,7 +233,7 @@ td code {
width: 100%;
margin: 0;
text-align: center;
}
} */
.typewriter-interface {

@ -5,3 +5,21 @@
padding-right: 2rem;
padding-top: 1rem;
}
.modal-close {
float: right;
scale: 2;
color: var(--vscode-breadcrumb-foreground);
cursor: pointer;
}
.modal-close:hover {
float: right;
scale: 2;
color: var(--vscode-breadcrumb-focusForeground);
}
.documentation .nav-button, .modal .nav-button {
float: right;
font-size: 1.5rem;
}

@ -14,9 +14,6 @@
width: 100%;
}
.app-content {
height: 100%
}
.welcome .column {
height: 100%;
@ -159,19 +156,6 @@ h5, h6 {
margin: 1em auto;
}
.modal-close {
float: right;
scale: 2;
color: var(--vscode-breadcrumb-foreground);
cursor: pointer;
}
.modal-close:hover {
float: right;
scale: 2;
color: var(--vscode-breadcrumb-focusForeground);
}
.modal table {
width: 100%;
}

@ -10,6 +10,7 @@ import Welcome from './components/welcome'
import LandingPage from './components/landing_page'
import Level from './components/level'
import './i18n';
import Game from './components/game'
@ -49,11 +50,11 @@ const router = createHashRouter([
landing_page,
{
path: "/g/:owner/:repo",
element: <Welcome />,
element: <Game />,
},
{
path: "/g/:owner/:repo/world/:worldId/level/:levelId",
element: <Level />,
element: <Game />,
},
],
},

@ -84,13 +84,19 @@ export const apiSlice = createApi({
query: ({game}) => `${game}/game.json`,
}),
loadLevel: builder.query<LevelInfo, {game: string, world: string, level: number}>({
query: ({game, world, level}) => `${game}/level__${world}__${level}.json`,
query: ({game, world, level}) => {
if (world) {
return `${game}/level__${world}__${level}.json`
} else {
return `${game}/inventory.json`
}
},
}),
loadInventoryOverview: builder.query<InventoryOverview, {game: string}>({
query: ({game}) => `${game}/inventory.json`,
}),
loadDoc: builder.query<Doc, {game: string, name: string, type: "lemma"|"tactic"}>({
query: ({game, type, name}) => `${game}/doc__${type}__${name}.json`,
loadDoc: builder.query<Doc, {game: string, name: string }>({
query: ({game, name}) => `${game}/doc__${name}.json`,
}),
}),
})

@ -54,7 +54,7 @@ const initalLevelProgressState: LevelProgressState = {code: "", completed: false
/** Add an empty skeleton with progress for the current game */
function addGameProgress (state: ProgressState, action: PayloadAction<{game: string}>) {
if (!state.games[action.payload.game.toLowerCase()]) {
state.games[action.payload.game.toLowerCase()] = {inventory: [], openedIntro: true, data: {}, difficulty: DEFAULT_DIFFICULTY}
state.games[action.payload.game.toLowerCase()] = {inventory: [], openedIntro: false, data: {}, difficulty: DEFAULT_DIFFICULTY}
}
if (!state.games[action.payload.game.toLowerCase()].data) {
state.games[action.payload.game.toLowerCase()].data = {}
@ -100,7 +100,7 @@ export const progressSlice = createSlice({
},
/** delete all progress for this game */
deleteProgress(state: ProgressState, action: PayloadAction<{game: string}>) {
state.games[action.payload.game.toLowerCase()] = {inventory: [], data: {}, openedIntro: true, difficulty: DEFAULT_DIFFICULTY}
state.games[action.payload.game.toLowerCase()] = {inventory: [], data: {}, openedIntro: false, difficulty: DEFAULT_DIFFICULTY}
},
/** delete progress for this level */
deleteLevelProgress(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number}>) {

@ -25,7 +25,7 @@ def copyImages : IO Unit := do
namespace GameData
def gameDataPath : System.FilePath := ".lake" / "gamedata"
def gameFileName := s!"game.json"
def docFileName := fun (inventoryType : InventoryType) (name : Name) => s!"doc__{inventoryType}__{name}.json"
def docFileName := fun (inventoryType : InventoryType) (name : Name) => s!"doc__{name}.json"
def levelFileName := fun (worldId : Name) (levelId : Nat) => s!"level__{worldId}__{levelId}.json"
def inventoryFileName := s!"inventory.json"
end GameData

Loading…
Cancel
Save