wip
This commit is contained in:
+12
-6
@@ -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>
|
||||
<Outlet />
|
||||
{ popupContent && <Popup /> }
|
||||
</PageContext.Provider>
|
||||
{ popupContent && <Popup /> }
|
||||
</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 <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>
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
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")} {diag.range.start.line}, {t("Character")} {diag.range.start.character}</p>
|
||||
<pre className="font-code pre-wrap">
|
||||
|
||||
+173
-125
@@ -1,118 +1,43 @@
|
||||
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';
|
||||
|
||||
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")
|
||||
/** 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: () => {}
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
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}/>
|
||||
}
|
||||
{ (tab == "definition") && levelInfo?.definitions &&
|
||||
<InventoryList items={levelInfo?.definitions} docType="Definition" openDoc={openDoc} enableAll={enableAll}/>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function InventoryList({items, docType, openDoc, tab=null, setTab=undefined, level=undefined, enableAll=false} :
|
||||
{
|
||||
items: InventoryTile[],
|
||||
docType: string,
|
||||
openDoc(props: {name: string, type: string}): void,
|
||||
tab?: any,
|
||||
setTab?: any,
|
||||
level?: LevelInfo|InventoryOverview,
|
||||
enableAll?: boolean,
|
||||
}) {
|
||||
// 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 difficulty = useSelector(selectDifficulty(gameId))
|
||||
|
||||
const categorySet = new Set<string>()
|
||||
for (let item of items) {
|
||||
categorySet.add(item.category)
|
||||
}
|
||||
const categories = 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)
|
||||
|
||||
// Item(s) proved in the preceeding level
|
||||
let recentItems = modifiedItems.filter(x => x.world == worldId && x.level == levelId - 1)
|
||||
|
||||
return <>
|
||||
{categories.length > 1 &&
|
||||
<div className="tab-bar">
|
||||
{categories.map((cat) => {
|
||||
let hasNew = modifiedItems.filter(item => item.new && (cat == item.category)).length > 0
|
||||
return <div key={`category-${cat}`} className={`tab${cat == (tab ?? categories[0]) ? " active": ""}${hasNew ? ' new': ''}${recentItems.map(x => x.category).includes(cat) ? ' recent': ''}`}
|
||||
onClick={() => { setTab(cat) }}>{cat}</div>})}
|
||||
</div>}
|
||||
<div className="inventory-list">
|
||||
{[...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)
|
||||
).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})}}
|
||||
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} />
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
function InventoryItem({item, name, displayName, locked, disabled, newly, showDoc, recent=false, enableAll=false}) {
|
||||
/**
|
||||
*/
|
||||
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" : ""
|
||||
@@ -121,12 +46,16 @@ function InventoryItem({item, name, displayName, locked, disabled, newly, showDo
|
||||
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))
|
||||
|
||||
// local state to show checkmark after pressing the copy button
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleClick = () => {
|
||||
if (enableAll || !locked) {
|
||||
// if ((difficulty == 0) || !locked) {
|
||||
showDoc()
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
const copyItemName = (ev) => {
|
||||
@@ -138,7 +67,7 @@ function InventoryItem({item, name, displayName, locked, disabled, newly, showDo
|
||||
ev.stopPropagation()
|
||||
}
|
||||
|
||||
return <div className={`item ${className}${enableAll ? ' enabled' : ''}${recent ? ' recent' : ''}`} onClick={handleClick} title={title}>
|
||||
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} />}
|
||||
@@ -146,42 +75,161 @@ return <div className={`item ${className}${enableAll ? ' enabled' : ''}${recent
|
||||
</div>
|
||||
}
|
||||
|
||||
export function Documentation({name, type, handleClose}) {
|
||||
const {gameId} = React.useContext(GameIdContext)
|
||||
const doc = useLoadDocQuery({game: gameId, type: type, name: name})
|
||||
|
||||
function InventoryList({ items, tab=null, setTab=()=>{} } :
|
||||
{
|
||||
items: InventoryTile[],
|
||||
tab?: string,
|
||||
setTab?: React.Dispatch<React.SetStateAction<string>>
|
||||
}) {
|
||||
|
||||
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)
|
||||
}
|
||||
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 _modifiedItems : InventoryTile[] = items?.map(tile => inventory.includes(tile.name) ? {...tile, locked: false} : tile)
|
||||
setModifiedItems(_modifiedItems)
|
||||
// Item(s) proved in the preceeding level
|
||||
setRecentItems(_modifiedItems.filter(x => x.world == worldId && x.level == levelId - 1))
|
||||
|
||||
}, [items, inventory])
|
||||
|
||||
return <>
|
||||
{ categories.length > 1 &&
|
||||
<div className="tab-bar">
|
||||
{categories.map((cat) => {
|
||||
let hasNew = modifiedItems.filter(item => item.new && (cat == item.category)).length > 0
|
||||
return <div key={`category-${cat}`} className={`tab${cat == (tab ?? categories[0]) ? " active": ""}${hasNew ? ' new': ''}${recentItems.map(x => x.category).includes(cat) ? ' recent': ''}`}
|
||||
onClick={() => { setTab(cat) }}>{cat}</div>})}
|
||||
</div>}
|
||||
<div className="inventory-list">
|
||||
{[...modifiedItems].sort(
|
||||
// For lemas, sort entries `available > disabled > locked`
|
||||
// otherwise alphabetically
|
||||
(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={() => {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} />
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** The `Inventory` shows all items present in the game sorted by item type. */
|
||||
export function Inventory () {
|
||||
const { t } = useTranslation()
|
||||
|
||||
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="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>
|
||||
)
|
||||
}
|
||||
|
||||
/** The `documentation` */
|
||||
export function Documentation() {
|
||||
const { gameId } = React.useContext(GameIdContext)
|
||||
// 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(() => {
|
||||
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,23 +109,26 @@ function LandingPage() {
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
<div className="game-list">
|
||||
{allTiles.filter(x => x != null).length == 0 ?
|
||||
<p>
|
||||
<Trans>
|
||||
No Games loaded. Use <a>http://localhost:3000/#/g/local/FOLDER</a> to open a
|
||||
game directly from a local folder.
|
||||
</Trans>
|
||||
</p>
|
||||
: lean4gameConfig.allGames.map((id, i) => (
|
||||
<Tile
|
||||
key={id}
|
||||
gameId={`g/${id}`}
|
||||
data={allTiles[i]}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<React.Suspense>
|
||||
<div className="game-list">
|
||||
|
||||
{allTiles.filter(x => x != null).length == 0 ?
|
||||
<p>
|
||||
<Trans>
|
||||
No Games loaded. Use <a>http://localhost:3000/#/g/local/FOLDER</a> to open a
|
||||
game directly from a local folder.
|
||||
</Trans>
|
||||
</p>
|
||||
: lean4gameConfig.allGames.map((id, i) => (
|
||||
<Tile
|
||||
key={id}
|
||||
gameId={`g/${id}`}
|
||||
data={allTiles[i]}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</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}/>}
|
||||
</WorldLevelIdContext.Provider>
|
||||
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")} <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) {
|
||||
@@ -151,32 +154,34 @@ function DesktopNavigationLevel () {
|
||||
</span>
|
||||
</div>
|
||||
<div className="nav-title-right" >
|
||||
{ levelId > 0 &&
|
||||
<NavButton
|
||||
icon={faArrowLeft}
|
||||
text={t("Previous")}
|
||||
inverted={true}
|
||||
href={`#/${gameId}/world/${worldId}/level/${levelId - 1}`} />
|
||||
}
|
||||
{ levelId == gameInfo.data?.worldSize[worldId] ?
|
||||
<NavButton
|
||||
icon={faHome}
|
||||
text={t("Leave World")}
|
||||
inverted={true}
|
||||
href={`#/${gameId}`} /> :
|
||||
<NavButton
|
||||
icon={faArrowRight}
|
||||
text={levelId == 0 ? t("Start") : t("Next")} inverted={true}
|
||||
href={`#/${gameId}/world/${worldId}/level/${levelId + 1}`} />
|
||||
}
|
||||
{ levelId > 0 &&
|
||||
<NavButton
|
||||
icon={(typewriterMode && !lockEditorMode) ? faCode : faTerminal}
|
||||
inverted={true}
|
||||
disabled={levelId == 0 || lockEditorMode}
|
||||
onClick={(ev) => toggleInputMode(ev)}
|
||||
title={lockEditorMode ? t("Editor mode is enforced!") : typewriterMode ? t("Editor mode") : t("Typewriter mode")} />
|
||||
}
|
||||
{ levelId > 0 &&
|
||||
<NavButton
|
||||
icon={faArrowLeft}
|
||||
text={t("Previous")}
|
||||
inverted={true}
|
||||
href={`#/${gameId}/world/${worldId}/level/${levelId - 1}`} />
|
||||
}
|
||||
{ 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}`} />
|
||||
}
|
||||
{ levelId > 0 &&
|
||||
<NavButton
|
||||
icon={(typewriterMode && !lockEditorMode) ? faCode : faTerminal}
|
||||
inverted={true}
|
||||
disabled={levelId == 0 || lockEditorMode}
|
||||
onClick={(ev) => toggleInputMode(ev)}
|
||||
title={lockEditorMode ? t("Editor mode is enforced!") : typewriterMode ? t("Editor mode") : t("Typewriter mode")} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -203,9 +208,9 @@ function MobileNavigationLevel () {
|
||||
</div>
|
||||
<div className="nav-title-right">
|
||||
<NavButton
|
||||
icon={page?faBookOpen:faBook}
|
||||
onClick={() => setPage(page?0:1)}
|
||||
inverted={true}/>
|
||||
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;
|
||||
|
||||
+15
-25
@@ -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%;
|
||||
}
|
||||
@@ -198,4 +182,4 @@ h5, h6 {
|
||||
|
||||
.preferences-item.leave-left-gap{
|
||||
margin-left: 3em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user