|
|
|
@ -4,7 +4,7 @@ import { useSelector, useStore } from 'react-redux'
|
|
|
|
import Split from 'react-split'
|
|
|
|
import Split from 'react-split'
|
|
|
|
import { useParams } from 'react-router-dom'
|
|
|
|
import { useParams } from 'react-router-dom'
|
|
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
|
|
import { faHome, faArrowRight } from '@fortawesome/free-solid-svg-icons'
|
|
|
|
import { faHome, faArrowRight, faCode, faTerminal } from '@fortawesome/free-solid-svg-icons'
|
|
|
|
import { CircularProgress } from '@mui/material'
|
|
|
|
import { CircularProgress } from '@mui/material'
|
|
|
|
import type { Location } from 'vscode-languageserver-protocol'
|
|
|
|
import type { Location } from 'vscode-languageserver-protocol'
|
|
|
|
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
|
|
|
|
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
|
|
|
|
@ -17,6 +17,7 @@ import { EditorContext } from '../../../node_modules/lean4-infoview/src/infoview
|
|
|
|
import { EditorConnection, EditorEvents } from '../../../node_modules/lean4-infoview/src/infoview/editorConnection'
|
|
|
|
import { EditorConnection, EditorEvents } from '../../../node_modules/lean4-infoview/src/infoview/editorConnection'
|
|
|
|
import { EventEmitter } from '../../../node_modules/lean4-infoview/src/infoview/event'
|
|
|
|
import { EventEmitter } from '../../../node_modules/lean4-infoview/src/infoview/event'
|
|
|
|
import { Diagnostic } from 'vscode-languageserver-types'
|
|
|
|
import { Diagnostic } from 'vscode-languageserver-types'
|
|
|
|
|
|
|
|
import { DiagnosticSeverity } from 'vscode-languageclient';
|
|
|
|
|
|
|
|
|
|
|
|
import { GameIdContext } from '../app'
|
|
|
|
import { GameIdContext } from '../app'
|
|
|
|
import { useAppDispatch, useAppSelector } from '../hooks'
|
|
|
|
import { useAppDispatch, useAppSelector } from '../hooks'
|
|
|
|
@ -27,12 +28,13 @@ import { store } from '../state/store'
|
|
|
|
import { Button } from './button'
|
|
|
|
import { Button } from './button'
|
|
|
|
import Markdown from './markdown'
|
|
|
|
import Markdown from './markdown'
|
|
|
|
import {InventoryPanel} from './inventory'
|
|
|
|
import {InventoryPanel} from './inventory'
|
|
|
|
import { hasInteractiveErrors } from './infoview/typewriter'
|
|
|
|
import { Editor } from './editor'
|
|
|
|
import { DeletedChatContext, InputModeContext, PreferencesContext, MonacoEditorContext,
|
|
|
|
import { Typewriter } from './typewriter'
|
|
|
|
ProofContext, SelectionContext, WorldLevelIdContext, PageContext } from './infoview/context'
|
|
|
|
import { ChatContext, InputModeContext, PreferencesContext, MonacoEditorContext,
|
|
|
|
import { DualEditor } from './infoview/main'
|
|
|
|
ProofContext, PageContext } from './infoview/context'
|
|
|
|
|
|
|
|
import { DualEditor, ExerciseStatement } from './infoview/main'
|
|
|
|
import { GameHint, InteractiveGoalsWithHints, ProofState } from './infoview/rpc_api'
|
|
|
|
import { GameHint, InteractiveGoalsWithHints, ProofState } from './infoview/rpc_api'
|
|
|
|
import { DeletedHints, Hint, Hints, MoreHelpButton, filterHints } from './hints'
|
|
|
|
import { DeletedHints, Hints, MoreHelpButton } from './hints'
|
|
|
|
import path from 'path';
|
|
|
|
import path from 'path';
|
|
|
|
|
|
|
|
|
|
|
|
import '@fontsource/roboto/300.css'
|
|
|
|
import '@fontsource/roboto/300.css'
|
|
|
|
@ -54,14 +56,14 @@ import { PreferencesPopup } from './popup/preferences'
|
|
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
import i18next from 'i18next'
|
|
|
|
import i18next from 'i18next'
|
|
|
|
import { ChatButtons } from './chat'
|
|
|
|
import { ChatButtons } from './chat'
|
|
|
|
|
|
|
|
import { NavButton } from './navigation'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
monacoSetup()
|
|
|
|
monacoSetup()
|
|
|
|
|
|
|
|
|
|
|
|
export function Level({visible = true}) {
|
|
|
|
export function Level({visible = true}) {
|
|
|
|
// const params = useParams()
|
|
|
|
let { t } = useTranslation()
|
|
|
|
// const levelId = parseInt(params.levelId)
|
|
|
|
|
|
|
|
// const worldId = params.worldId
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const {gameId, worldId, levelId} = React.useContext(GameIdContext)
|
|
|
|
const {gameId, worldId, levelId} = React.useContext(GameIdContext)
|
|
|
|
|
|
|
|
|
|
|
|
@ -71,153 +73,9 @@ export function Level({visible = true}) {
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
|
|
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
|
|
|
|
|
|
|
const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
|
|
|
|
|
|
|
|
|
|
|
// pop-ups
|
|
|
|
|
|
|
|
const [impressum, setImpressum] = React.useState(false)
|
|
|
|
|
|
|
|
const [privacy, setPrivacy] = React.useState(false)
|
|
|
|
|
|
|
|
const [info, setInfo] = React.useState(false)
|
|
|
|
|
|
|
|
const [preferencesPopup, setPreferencesPopup] = React.useState(false)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function closeImpressum() {setImpressum(false)}
|
|
|
|
|
|
|
|
function closePrivacy() {setPrivacy(false)}
|
|
|
|
|
|
|
|
function closeInfo() {setInfo(false)}
|
|
|
|
|
|
|
|
function closePreferencesPopup() {setPreferencesPopup(false)}
|
|
|
|
|
|
|
|
function toggleImpressum() {setImpressum(!impressum)}
|
|
|
|
|
|
|
|
function toggleInfo() {setInfo(!info)}
|
|
|
|
|
|
|
|
function togglePreferencesPopup() {setPreferencesPopup(!preferencesPopup)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {}, [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return <div className={visible?'':'hidden'}>
|
|
|
|
|
|
|
|
<WorldLevelIdContext.Provider value={{worldId, levelId}} >
|
|
|
|
|
|
|
|
<PlayableLevel key={`${worldId}/${levelId}`} />
|
|
|
|
|
|
|
|
</WorldLevelIdContext.Provider>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function ChatPanel({lastLevel, visible = true}) {
|
|
|
|
|
|
|
|
let { t } = useTranslation()
|
|
|
|
|
|
|
|
const chatRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
|
|
|
const {mobile} = useContext(PreferencesContext)
|
|
|
|
|
|
|
|
const {gameId} = useContext(GameIdContext)
|
|
|
|
|
|
|
|
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
|
|
|
|
|
|
|
const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
|
|
|
|
|
|
|
const {proof, setProof} = useContext(ProofContext)
|
|
|
|
|
|
|
|
const {deletedChat, setDeletedChat, showHelp, setShowHelp} = useContext(DeletedChatContext)
|
|
|
|
|
|
|
|
const {selectedStep, setSelectedStep} = useContext(SelectionContext)
|
|
|
|
|
|
|
|
const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let k = proof?.steps.length ? proof?.steps.length - (lastStepHasErrors(proof) ? 2 : 1) : 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function toggleSelection(line: number) {
|
|
|
|
|
|
|
|
return (ev) => {
|
|
|
|
|
|
|
|
console.debug('toggled selection')
|
|
|
|
|
|
|
|
if (selectedStep == line) {
|
|
|
|
|
|
|
|
setSelectedStep(undefined)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
setSelectedStep(line)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
|
|
// TODO: For some reason this is always called twice
|
|
|
|
|
|
|
|
console.debug('scroll chat')
|
|
|
|
|
|
|
|
if (!mobile) {
|
|
|
|
|
|
|
|
chatRef.current!.lastElementChild?.scrollIntoView() //scrollTo(0,0)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}, [proof, showHelp])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Scroll to element if selection changes
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
|
|
if (typeof selectedStep !== 'undefined') {
|
|
|
|
|
|
|
|
Array.from(chatRef.current?.getElementsByClassName(`step-${selectedStep}`)).map((elem) => {
|
|
|
|
|
|
|
|
elem.scrollIntoView({block: "center"})
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}, [selectedStep])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// useEffect(() => {
|
|
|
|
|
|
|
|
// // // Scroll to top when loading a new level
|
|
|
|
|
|
|
|
// // // TODO: Thats the wrong behaviour probably
|
|
|
|
|
|
|
|
// // chatRef.current!.scrollTo(0,0)
|
|
|
|
|
|
|
|
// }, [gameId, worldId, levelId])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let introText: Array<string> = t(level?.data?.introduction, {ns: gameId}).split(/\n(\s*\n)+/)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return <div className={`chat-panel ${visible ? '' : 'hidden'}`}>
|
|
|
|
|
|
|
|
<div ref={chatRef} className="chat">
|
|
|
|
|
|
|
|
{introText?.filter(t => t.trim()).map(((t, i) =>
|
|
|
|
|
|
|
|
// Show the level's intro text as hints, too
|
|
|
|
|
|
|
|
<Hint key={`intro-p-${i}`}
|
|
|
|
|
|
|
|
hint={{text: t, hidden: false, rawText: t, varNames: []}} step={0} selected={selectedStep} toggleSelection={toggleSelection(0)} />
|
|
|
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
{proof?.steps.map((step, i) => {
|
|
|
|
|
|
|
|
let filteredHints = filterHints(step.goals[0]?.hints, proof?.steps[i-1]?.goals[0]?.hints)
|
|
|
|
|
|
|
|
if (step.goals.length > 0 && !isLastStepWithErrors(proof, i)) {
|
|
|
|
|
|
|
|
return <Hints key={`hints-${i}`}
|
|
|
|
|
|
|
|
hints={filteredHints} showHidden={showHelp.has(i)} step={i}
|
|
|
|
|
|
|
|
selected={selectedStep} toggleSelection={toggleSelection(i)} lastLevel={i == proof?.steps.length - 1}/>
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
})}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* {modifiedHints.map((step, i) => {
|
|
|
|
|
|
|
|
// It the last step has errors, it will have the same hints
|
|
|
|
|
|
|
|
// as the second-to-last step. Therefore we should not display them.
|
|
|
|
|
|
|
|
if (!(i == proof?.steps.length - 1 && withErr)) {
|
|
|
|
|
|
|
|
// TODO: Should not use index as key.
|
|
|
|
|
|
|
|
return <Hints key={`hints-${i}`}
|
|
|
|
|
|
|
|
hints={step} showHidden={showHelp.has(i)} step={i}
|
|
|
|
|
|
|
|
selected={selectedStep} toggleSelection={toggleSelection(i)} lastLevel={i == proof?.steps.length - 1}/>
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
})} */}
|
|
|
|
|
|
|
|
<DeletedHints hints={deletedChat}/>
|
|
|
|
|
|
|
|
{proof?.completed &&
|
|
|
|
|
|
|
|
<>
|
|
|
|
|
|
|
|
<div className={`message information recent step-${k}${selectedStep == k ? ' selected' : ''}`} onClick={toggleSelection(k)}>
|
|
|
|
|
|
|
|
{t("Level completed! 🎉")}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{level?.data?.conclusion?.trim() &&
|
|
|
|
|
|
|
|
<div className={`message information recent step-${k}${selectedStep == k ? ' selected' : ''}`} onClick={toggleSelection(k)}>
|
|
|
|
|
|
|
|
<Markdown>{t(level?.data?.conclusion, {ns: gameId})}</Markdown>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
</>
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="button-row">
|
|
|
|
|
|
|
|
{proof?.completed && (lastLevel ?
|
|
|
|
|
|
|
|
<Button to={`/${gameId}`}>
|
|
|
|
|
|
|
|
<FontAwesomeIcon icon={faHome} /> {t("Leave World")}
|
|
|
|
|
|
|
|
</Button> :
|
|
|
|
|
|
|
|
<Button to={`/${gameId}/world/${worldId}/level/${levelId + 1}`}>
|
|
|
|
|
|
|
|
{t("Next")} <FontAwesomeIcon icon={faArrowRight} />
|
|
|
|
|
|
|
|
</Button>)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
<MoreHelpButton />
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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})
|
|
|
|
|
|
|
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
|
|
|
|
|
|
|
return <div className={`exercise-panel ${visible ? '' : 'hidden'}`}>
|
|
|
|
|
|
|
|
<div className="exercise">
|
|
|
|
|
|
|
|
<DualEditor level={level?.data} codeviewRef={codeviewRef} levelId={levelId} worldId={worldId} worldSize={gameInfo.data?.worldSize[worldId]}/>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function PlayableLevel() {
|
|
|
|
|
|
|
|
let { t } = useTranslation()
|
|
|
|
|
|
|
|
const codeviewRef = useRef<HTMLDivElement>(null)
|
|
|
|
const codeviewRef = useRef<HTMLDivElement>(null)
|
|
|
|
const {gameId} = React.useContext(GameIdContext)
|
|
|
|
|
|
|
|
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
|
|
|
|
|
|
|
const {mobile} = React.useContext(PreferencesContext)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const dispatch = useAppDispatch()
|
|
|
|
const dispatch = useAppDispatch()
|
|
|
|
|
|
|
|
|
|
|
|
@ -227,20 +85,9 @@ export function PlayableLevel() {
|
|
|
|
const typewriterMode = useSelector(selectTypewriterMode(gameId))
|
|
|
|
const typewriterMode = useSelector(selectTypewriterMode(gameId))
|
|
|
|
const setTypewriterMode = (newTypewriterMode: boolean) => dispatch(changeTypewriterMode({game: gameId, typewriterMode: newTypewriterMode}))
|
|
|
|
const setTypewriterMode = (newTypewriterMode: boolean) => dispatch(changeTypewriterMode({game: gameId, typewriterMode: newTypewriterMode}))
|
|
|
|
|
|
|
|
|
|
|
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
|
|
|
const {deletedChat, setDeletedChat,showHelp, setShowHelp} = useContext(ChatContext)
|
|
|
|
const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
|
|
|
const {proof, setProof} = useContext(ProofContext)
|
|
|
|
|
|
|
|
|
|
|
|
// The state variables for the `ProofContext`
|
|
|
|
|
|
|
|
const [proof, setProof] = useState<ProofState>({steps: [], diagnostics: [], completed: false, completedWithWarnings: false})
|
|
|
|
|
|
|
|
const [interimDiags, setInterimDiags] = useState<Array<Diagnostic>>([])
|
|
|
|
|
|
|
|
const [isCrashed, setIsCrashed] = useState<Boolean>(false)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// When deleting the proof, we want to keep to old messages around until
|
|
|
|
|
|
|
|
// a new proof has been entered. e.g. to consult messages coming from dead ends
|
|
|
|
|
|
|
|
const [deletedChat, setDeletedChat] = useState<Array<GameHint>>([])
|
|
|
|
|
|
|
|
// A set of row numbers where help is displayed
|
|
|
|
|
|
|
|
const [showHelp, setShowHelp] = useState<Set<number>>(new Set())
|
|
|
|
|
|
|
|
// Only for mobile layout
|
|
|
|
// Only for mobile layout
|
|
|
|
const {page, setPage} = useContext(PageContext)
|
|
|
|
const {page, setPage} = useContext(PageContext)
|
|
|
|
|
|
|
|
|
|
|
|
@ -259,8 +106,6 @@ export function PlayableLevel() {
|
|
|
|
const [inventoryDoc, setInventoryDoc] = useState<{name: string, type: string}>(null)
|
|
|
|
const [inventoryDoc, setInventoryDoc] = useState<{name: string, type: string}>(null)
|
|
|
|
function closeInventoryDoc () {setInventoryDoc(null)}
|
|
|
|
function closeInventoryDoc () {setInventoryDoc(null)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const onDidChangeContent = (code) => {
|
|
|
|
const onDidChangeContent = (code) => {
|
|
|
|
dispatch(codeEdited({game: gameId, world: worldId, level: levelId, code}))
|
|
|
|
dispatch(codeEdited({game: gameId, world: worldId, level: levelId, code}))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -309,7 +154,7 @@ export function PlayableLevel() {
|
|
|
|
|
|
|
|
|
|
|
|
useEffect (() => {
|
|
|
|
useEffect (() => {
|
|
|
|
// Lock editor mode
|
|
|
|
// Lock editor mode
|
|
|
|
if (level?.data?.template) {
|
|
|
|
if (levelInfo.data?.template) {
|
|
|
|
setLockEditorMode(true)
|
|
|
|
setLockEditorMode(true)
|
|
|
|
|
|
|
|
|
|
|
|
if (editor) {
|
|
|
|
if (editor) {
|
|
|
|
@ -325,12 +170,12 @@ export function PlayableLevel() {
|
|
|
|
// TODO: It does seem that the template is always indented by spaces.
|
|
|
|
// TODO: It does seem that the template is always indented by spaces.
|
|
|
|
// This is a hack, assuming there are exactly two.
|
|
|
|
// This is a hack, assuming there are exactly two.
|
|
|
|
if (!code.join('').trim().length) {
|
|
|
|
if (!code.join('').trim().length) {
|
|
|
|
console.debug(`inserting template:\n${level.data.template}`)
|
|
|
|
console.debug(`inserting template:\n${levelInfo.data.template}`)
|
|
|
|
// TODO: This does not work! HERE
|
|
|
|
// TODO: This does not work! HERE
|
|
|
|
// Probably overwritten by a query to the server
|
|
|
|
// Probably overwritten by a query to the server
|
|
|
|
editor.executeEdits("template-writer", [{
|
|
|
|
editor.executeEdits("template-writer", [{
|
|
|
|
range: editor.getModel().getFullModelRange(),
|
|
|
|
range: editor.getModel().getFullModelRange(),
|
|
|
|
text: level.data.template + `\n`,
|
|
|
|
text: levelInfo.data.template + `\n`,
|
|
|
|
forceMoveMarkers: true
|
|
|
|
forceMoveMarkers: true
|
|
|
|
}])
|
|
|
|
}])
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
@ -340,7 +185,7 @@ export function PlayableLevel() {
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
setLockEditorMode(false)
|
|
|
|
setLockEditorMode(false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [level, levelId, worldId, gameId, editor])
|
|
|
|
}, [levelInfo, levelId, worldId, gameId, editor])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
useEffect(() => {
|
|
|
|
@ -407,104 +252,346 @@ export function PlayableLevel() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [editor, typewriterMode, lockEditorMode, onigasmH == null])
|
|
|
|
}, [editor, typewriterMode, lockEditorMode, onigasmH == null])
|
|
|
|
|
|
|
|
|
|
|
|
return <>
|
|
|
|
|
|
|
|
<div style={level.isLoading ? null : {display: "none"}} className="app-content loading"><CircularProgress /></div>
|
|
|
|
|
|
|
|
<DeletedChatContext.Provider value={{deletedChat, setDeletedChat, showHelp, setShowHelp}}>
|
|
|
|
|
|
|
|
<SelectionContext.Provider value={{selectedStep, setSelectedStep}}>
|
|
|
|
|
|
|
|
<InputModeContext.Provider value={{typewriterMode, setTypewriterMode, typewriterInput, setTypewriterInput, lockEditorMode, setLockEditorMode}}>
|
|
|
|
|
|
|
|
<ProofContext.Provider value={{proof, setProof, interimDiags, setInterimDiags, crashed: isCrashed, setCrashed: setIsCrashed}}>
|
|
|
|
|
|
|
|
<EditorContext.Provider value={editorConnection}>
|
|
|
|
|
|
|
|
<MonacoEditorContext.Provider value={editor}>
|
|
|
|
|
|
|
|
{/* <LevelAppBar
|
|
|
|
|
|
|
|
pageNumber={page} setPageNumber={setPage}
|
|
|
|
|
|
|
|
isLoading={level.isLoading}
|
|
|
|
|
|
|
|
levelTitle={(mobile ? "" : t("Level")) + ` ${levelId} / ${gameInfo.data?.worldSize[worldId]}` +
|
|
|
|
|
|
|
|
(level?.data?.title && ` : ${t(level?.data?.title, {ns: gameId})}`)}
|
|
|
|
|
|
|
|
toggleImpressum={toggleImpressum}
|
|
|
|
|
|
|
|
togglePrivacy={togglePrivacy}
|
|
|
|
|
|
|
|
toggleInfo={toggleInfo}
|
|
|
|
|
|
|
|
togglePreferencesPopup={togglePreferencesPopup}
|
|
|
|
|
|
|
|
/> */}
|
|
|
|
|
|
|
|
<ExercisePanel codeviewRef={codeviewRef} />
|
|
|
|
|
|
|
|
</MonacoEditorContext.Provider>
|
|
|
|
|
|
|
|
</EditorContext.Provider>
|
|
|
|
|
|
|
|
</ProofContext.Provider>
|
|
|
|
|
|
|
|
</InputModeContext.Provider>
|
|
|
|
|
|
|
|
</SelectionContext.Provider>
|
|
|
|
|
|
|
|
</DeletedChatContext.Provider>
|
|
|
|
|
|
|
|
</>
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// <Split minSize={0} snapOffset={200} sizes={[25, 75]} className={`app-content level ${level.isLoading ? 'hidden' : ''}`}>
|
|
|
|
return <div className={visible?'':'hidden'}>
|
|
|
|
// <ChatPanel lastLevel={lastLevel}/>
|
|
|
|
{/* <div style={levelInfo.isLoading ? null : {display: "none"}} className="app-content loading"><CircularProgress /></div> */}
|
|
|
|
// <InventoryPanel />
|
|
|
|
<EditorContext.Provider value={editorConnection}>
|
|
|
|
// </Split>
|
|
|
|
<MonacoEditorContext.Provider value={editor}>
|
|
|
|
|
|
|
|
{levelId > 0 &&
|
|
|
|
function IntroductionPanel({gameInfo}) {
|
|
|
|
<ExercisePanel codeviewRef={codeviewRef} />}
|
|
|
|
let { t } = useTranslation()
|
|
|
|
</MonacoEditorContext.Provider>
|
|
|
|
const {gameId} = React.useContext(GameIdContext)
|
|
|
|
</EditorContext.Provider>
|
|
|
|
const {worldId} = useContext(WorldLevelIdContext)
|
|
|
|
|
|
|
|
const {mobile} = React.useContext(PreferencesContext)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let text: Array<string> = t(gameInfo.data?.worlds.nodes[worldId].introduction, {ns: gameId}).split(/\n(\s*\n)+/)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return <div className="chat-panel">
|
|
|
|
|
|
|
|
<div className="chat">
|
|
|
|
|
|
|
|
{text?.filter(t => t.trim()).map(((t, i) =>
|
|
|
|
|
|
|
|
<Hint key={`intro-p-${i}`}
|
|
|
|
|
|
|
|
hint={{text: t, hidden: false, rawText: t, varNames: []}} step={0} selected={null} toggleSelection={undefined} />
|
|
|
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<ChatButtons />
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default Level
|
|
|
|
export function LevelWrapper({visible = true}) {
|
|
|
|
|
|
|
|
|
|
|
|
/** The site with the introduction text of a world */
|
|
|
|
|
|
|
|
function Introduction() {
|
|
|
|
|
|
|
|
let { t } = useTranslation()
|
|
|
|
let { t } = useTranslation()
|
|
|
|
|
|
|
|
const {gameId, worldId, levelId} = React.useContext(GameIdContext)
|
|
|
|
|
|
|
|
|
|
|
|
const {gameId} = React.useContext(GameIdContext)
|
|
|
|
// Load the namespace of the game
|
|
|
|
const {mobile} = useContext(PreferencesContext)
|
|
|
|
i18next.loadNamespaces(gameId).catch(err => {
|
|
|
|
|
|
|
|
console.warn(`translations for ${gameId} do not exist.`)
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const inventory = useLoadInventoryOverviewQuery({game: gameId})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
|
|
|
const { mobile } = useContext(PreferencesContext)
|
|
|
|
|
|
|
|
|
|
|
|
const {worldId} = useContext(WorldLevelIdContext)
|
|
|
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
|
|
|
|
|
|
|
const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
|
|
|
|
|
|
|
|
|
|
|
let image: string = gameInfo.data?.worlds.nodes[worldId].image
|
|
|
|
let image: string = gameInfo.data?.worlds.nodes[worldId].image
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const codeviewRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
|
|
|
const proofPanelRef = React.useRef<HTMLDivElement>(null)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// set to true to prevent switching between typewriter and editor
|
|
|
|
|
|
|
|
const [lockEditorMode, setLockEditorMode] = useState(false)
|
|
|
|
|
|
|
|
const [typewriterInput, setTypewriterInput] = useState("")
|
|
|
|
|
|
|
|
const lastLevel = levelId >= gameInfo.data?.worldSize[worldId]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// // 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.
|
|
|
|
|
|
|
|
// Set `inventoryDoc` to `null` to close the doc
|
|
|
|
|
|
|
|
const [inventoryDoc, setInventoryDoc] = useState<{name: string, type: string}>(null)
|
|
|
|
|
|
|
|
function closeInventoryDoc () {setInventoryDoc(null)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 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 ?
|
|
|
|
|
|
|
|
<div className="app-content loading"><CircularProgress /></div>
|
|
|
|
|
|
|
|
: mobile ?
|
|
|
|
|
|
|
|
<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="" />
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const dispatch = useAppDispatch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const typewriterMode = useSelector(selectTypewriterMode(gameId))
|
|
|
|
|
|
|
|
const setTypewriterMode = (newTypewriterMode: boolean) => dispatch(changeTypewriterMode({game: gameId, typewriterMode: newTypewriterMode}))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const initialCode = useAppSelector(selectCode(gameId, worldId, levelId))
|
|
|
|
|
|
|
|
const initialSelections = useAppSelector(selectSelections(gameId, worldId, levelId))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const onDidChangeContent = (code) => {
|
|
|
|
|
|
|
|
dispatch(codeEdited({game: gameId, world: worldId, level: levelId, code}))
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const onDidChangeSelection = (monacoSelections) => {
|
|
|
|
|
|
|
|
const selections = monacoSelections.map(
|
|
|
|
|
|
|
|
({selectionStartLineNumber, selectionStartColumn, positionLineNumber, positionColumn}) =>
|
|
|
|
|
|
|
|
{return {selectionStartLineNumber, selectionStartColumn, positionLineNumber, positionColumn}})
|
|
|
|
|
|
|
|
dispatch(changedSelection({game: gameId, world: worldId, level: levelId, selections}))
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
|
|
console.info(`Loading Level: ${mobile}`)
|
|
|
|
|
|
|
|
}, [mobile, gameId, worldId, levelId])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// const {editor, infoProvider, editorConnection} =
|
|
|
|
|
|
|
|
// useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** toggle input mode if allowed */
|
|
|
|
|
|
|
|
function toggleInputMode(ev: React.MouseEvent) {
|
|
|
|
|
|
|
|
if (!lockEditorMode) {
|
|
|
|
|
|
|
|
setTypewriterMode(!typewriterMode)
|
|
|
|
|
|
|
|
console.log('test')
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// { typewriterMode ? <Typewriter /> : <Editor /> }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// <EditorContext.Provider value={editorConnection}>
|
|
|
|
|
|
|
|
// <MonacoEditorContext.Provider value={editor}>
|
|
|
|
|
|
|
|
return <div className="exercise">
|
|
|
|
|
|
|
|
{ image &&
|
|
|
|
|
|
|
|
<div className='world-image'>
|
|
|
|
|
|
|
|
<img className="contain" src={path.join("data", gameId, image)} alt="" />
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
{ levelId > 0 &&
|
|
|
|
|
|
|
|
<div className="exercise-content">
|
|
|
|
|
|
|
|
<NavButton
|
|
|
|
|
|
|
|
className="btn-input-mode"
|
|
|
|
|
|
|
|
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 className="tmp-pusher" />
|
|
|
|
|
|
|
|
<div className='proof' ref={proofPanelRef}>
|
|
|
|
|
|
|
|
<ExerciseStatement showLeanStatement={!typewriterMode} />
|
|
|
|
|
|
|
|
<Level/>
|
|
|
|
|
|
|
|
{/* { typewriterMode ? <Typewriter /> : <Editor /> } */}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
// {/* <InventoryPanel /> */}
|
|
|
|
|
|
|
|
// </Split>
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
</>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// </MonacoEditorContext.Provider>
|
|
|
|
|
|
|
|
// </EditorContext.Provider>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// function ChatPanel({lastLevel, visible = true}) {
|
|
|
|
|
|
|
|
// let { t } = useTranslation()
|
|
|
|
|
|
|
|
// const chatRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
|
|
|
// const {mobile} = useContext(PreferencesContext)
|
|
|
|
|
|
|
|
// const {gameId, worldId, levelId} = useContext(GameIdContext)
|
|
|
|
|
|
|
|
// const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
|
|
|
|
|
|
|
// const {proof, setProof} = useContext(ProofContext)
|
|
|
|
|
|
|
|
// const {deletedChat, setDeletedChat, showHelp, setShowHelp} = useContext(DeletedChatContext)
|
|
|
|
|
|
|
|
// const {selectedStep, setSelectedStep} = useContext(SelectionContext)
|
|
|
|
|
|
|
|
// const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// let k = proof?.steps.length ? proof?.steps.length - (lastStepHasErrors(proof) ? 2 : 1) : 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// function toggleSelection(line: number) {
|
|
|
|
|
|
|
|
// return (ev) => {
|
|
|
|
|
|
|
|
// console.debug('toggled selection')
|
|
|
|
|
|
|
|
// if (selectedStep == line) {
|
|
|
|
|
|
|
|
// setSelectedStep(undefined)
|
|
|
|
|
|
|
|
// } else {
|
|
|
|
|
|
|
|
// setSelectedStep(line)
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// useEffect(() => {
|
|
|
|
|
|
|
|
// // TODO: For some reason this is always called twice
|
|
|
|
|
|
|
|
// console.debug('scroll chat')
|
|
|
|
|
|
|
|
// if (!mobile) {
|
|
|
|
|
|
|
|
// chatRef.current!.lastElementChild?.scrollIntoView() //scrollTo(0,0)
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
// }, [proof, showHelp])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// // Scroll to element if selection changes
|
|
|
|
|
|
|
|
// useEffect(() => {
|
|
|
|
|
|
|
|
// if (typeof selectedStep !== 'undefined') {
|
|
|
|
|
|
|
|
// Array.from(chatRef.current?.getElementsByClassName(`step-${selectedStep}`)).map((elem) => {
|
|
|
|
|
|
|
|
// elem.scrollIntoView({block: "center"})
|
|
|
|
|
|
|
|
// })
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
// }, [selectedStep])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// // useEffect(() => {
|
|
|
|
|
|
|
|
// // // // Scroll to top when loading a new level
|
|
|
|
|
|
|
|
// // // // TODO: Thats the wrong behaviour probably
|
|
|
|
|
|
|
|
// // // chatRef.current!.scrollTo(0,0)
|
|
|
|
|
|
|
|
// // }, [gameId, worldId, levelId])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// let introText: Array<string> = t(level?.data?.introduction, {ns: gameId}).split(/\n(\s*\n)+/)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// return <div className={`chat-panel ${visible ? '' : 'hidden'}`}>
|
|
|
|
|
|
|
|
// <div ref={chatRef} className="chat">
|
|
|
|
|
|
|
|
// {introText?.filter(t => t.trim()).map(((t, i) =>
|
|
|
|
|
|
|
|
// // Show the level's intro text as hints, too
|
|
|
|
|
|
|
|
// <Hint key={`intro-p-${i}`}
|
|
|
|
|
|
|
|
// hint={{text: t, hidden: false, rawText: t, varNames: []}} step={0} selected={selectedStep} toggleSelection={toggleSelection(0)} />
|
|
|
|
|
|
|
|
// ))}
|
|
|
|
|
|
|
|
// {proof?.steps.map((step, i) => {
|
|
|
|
|
|
|
|
// let filteredHints = filterHints(step.goals[0]?.hints, proof?.steps[i-1]?.goals[0]?.hints)
|
|
|
|
|
|
|
|
// if (step.goals.length > 0 && !isLastStepWithErrors(proof, i)) {
|
|
|
|
|
|
|
|
// return <Hints key={`hints-${i}`}
|
|
|
|
|
|
|
|
// hints={filteredHints} showHidden={showHelp.has(i)} step={i}
|
|
|
|
|
|
|
|
// selected={selectedStep} toggleSelection={toggleSelection(i)} lastLevel={i == proof?.steps.length - 1}/>
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
// })}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// {/* {modifiedHints.map((step, i) => {
|
|
|
|
|
|
|
|
// // It the last step has errors, it will have the same hints
|
|
|
|
|
|
|
|
// // as the second-to-last step. Therefore we should not display them.
|
|
|
|
|
|
|
|
// if (!(i == proof?.steps.length - 1 && withErr)) {
|
|
|
|
|
|
|
|
// // TODO: Should not use index as key.
|
|
|
|
|
|
|
|
// return <Hints key={`hints-${i}`}
|
|
|
|
|
|
|
|
// hints={step} showHidden={showHelp.has(i)} step={i}
|
|
|
|
|
|
|
|
// selected={selectedStep} toggleSelection={toggleSelection(i)} lastLevel={i == proof?.steps.length - 1}/>
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
// })} */}
|
|
|
|
|
|
|
|
// <DeletedHints hints={deletedChat}/>
|
|
|
|
|
|
|
|
// {proof?.completed &&
|
|
|
|
|
|
|
|
// <>
|
|
|
|
|
|
|
|
// <div className={`message information recent step-${k}${selectedStep == k ? ' selected' : ''}`} onClick={toggleSelection(k)}>
|
|
|
|
|
|
|
|
// {t("Level completed! 🎉")}
|
|
|
|
|
|
|
|
// </div>
|
|
|
|
|
|
|
|
// {level?.data?.conclusion?.trim() &&
|
|
|
|
|
|
|
|
// <div className={`message information recent step-${k}${selectedStep == k ? ' selected' : ''}`} onClick={toggleSelection(k)}>
|
|
|
|
|
|
|
|
// <Markdown>{t(level?.data?.conclusion, {ns: gameId})}</Markdown>
|
|
|
|
|
|
|
|
// </div>
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
// </>
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
// </div>
|
|
|
|
|
|
|
|
// <div className="button-row">
|
|
|
|
|
|
|
|
// {proof?.completed && (lastLevel ?
|
|
|
|
|
|
|
|
// <Button to={`/${gameId}`}>
|
|
|
|
|
|
|
|
// <FontAwesomeIcon icon={faHome} /> {t("Leave World")}
|
|
|
|
|
|
|
|
// </Button> :
|
|
|
|
|
|
|
|
// <Button to={`/${gameId}/world/${worldId}/level/${levelId + 1}`}>
|
|
|
|
|
|
|
|
// {t("Next")} <FontAwesomeIcon icon={faArrowRight} />
|
|
|
|
|
|
|
|
// </Button>)
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
// <MoreHelpButton />
|
|
|
|
|
|
|
|
// </div>
|
|
|
|
|
|
|
|
// </div>
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function ExercisePanel({codeviewRef, visible=true}: {codeviewRef: React.MutableRefObject<HTMLDivElement>, visible?: boolean}) {
|
|
|
|
|
|
|
|
const {gameId, worldId, levelId} = React.useContext(GameIdContext)
|
|
|
|
|
|
|
|
const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
|
|
|
|
|
|
|
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
|
|
|
|
|
|
|
return <div className={`exercise-panel ${visible ? '' : 'hidden'}`}>
|
|
|
|
|
|
|
|
<div className="">
|
|
|
|
|
|
|
|
<DualEditor level={level?.data} codeviewRef={codeviewRef} levelId={levelId} worldId={worldId} worldSize={gameInfo.data?.worldSize[worldId]}/>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// <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, worldId} = React.useContext(GameIdContext)
|
|
|
|
|
|
|
|
// const {mobile} = React.useContext(PreferencesContext)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// let text: Array<string> = t(gameInfo.data?.worlds.nodes[worldId].introduction, {ns: gameId}).split(/\n(\s*\n)+/)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// return <div className="chat-panel">
|
|
|
|
|
|
|
|
// <div className="chat">
|
|
|
|
|
|
|
|
// {text?.filter(t => t.trim()).map(((t, i) =>
|
|
|
|
|
|
|
|
// <Hint key={`intro-p-${i}`}
|
|
|
|
|
|
|
|
// hint={{text: t, hidden: false, rawText: t, varNames: []}} step={0} selected={null} toggleSelection={undefined} />
|
|
|
|
|
|
|
|
// ))}
|
|
|
|
|
|
|
|
// </div>
|
|
|
|
|
|
|
|
// <ChatButtons />
|
|
|
|
|
|
|
|
// </div>
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// /** The site with the introduction text of a world */
|
|
|
|
|
|
|
|
// function Introduction() {
|
|
|
|
|
|
|
|
// let { t } = useTranslation()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// const {gameId, worldId} = React.useContext(GameIdContext)
|
|
|
|
|
|
|
|
// const {mobile} = useContext(PreferencesContext)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// const inventory = useLoadInventoryOverviewQuery({game: gameId})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// const gameInfo = useGetGameInfoQuery({game: gameId})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// let image: string = gameInfo.data?.worlds.nodes[worldId].image
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// // 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 ?
|
|
|
|
|
|
|
|
// <div className="app-content loading"><CircularProgress /></div>
|
|
|
|
|
|
|
|
// : mobile ?
|
|
|
|
|
|
|
|
// <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 /> */}
|
|
|
|
|
|
|
|
// // </Split>
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// </>
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
// {mobile?
|
|
|
|
// {mobile?
|
|
|
|
// // TODO: This is copied from the `Split` component below...
|
|
|
|
// // TODO: This is copied from the `Split` component below...
|
|
|
|
// <>
|
|
|
|
// <>
|
|
|
|
@ -530,8 +617,7 @@ function Introduction() {
|
|
|
|
|
|
|
|
|
|
|
|
function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection) {
|
|
|
|
function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection) {
|
|
|
|
|
|
|
|
|
|
|
|
const {gameId} = React.useContext(GameIdContext)
|
|
|
|
const {gameId, worldId, levelId} = React.useContext(GameIdContext)
|
|
|
|
const {worldId, levelId} = useContext(WorldLevelIdContext)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor|null>(null)
|
|
|
|
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor|null>(null)
|
|
|
|
const [infoProvider, setInfoProvider] = useState<null|InfoProvider>(null)
|
|
|
|
const [infoProvider, setInfoProvider] = useState<null|InfoProvider>(null)
|
|
|
|
@ -544,6 +630,9 @@ function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChange
|
|
|
|
const difficulty: number = useSelector(selectDifficulty(gameId))
|
|
|
|
const difficulty: number = useSelector(selectDifficulty(gameId))
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
|
|
// monaco.editor.getModels().forEach(model => model.dispose());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console.info(`trying to create model: ${gameId} ${worldId} ${levelId} ${uri}`)
|
|
|
|
const model = monaco.editor.createModel(initialCode ?? '', 'lean4', uri)
|
|
|
|
const model = monaco.editor.createModel(initialCode ?? '', 'lean4', uri)
|
|
|
|
if (onDidChangeContent) {
|
|
|
|
if (onDidChangeContent) {
|
|
|
|
model.onDidChangeContent(() => onDidChangeContent(model.getValue()))
|
|
|
|
model.onDidChangeContent(() => onDidChangeContent(model.getValue()))
|
|
|
|
|