introduce difficulties

pull/118/head
Jon Eugster 2 years ago
parent 1c99a3da64
commit 57351025c9

@ -6,8 +6,9 @@ import { faLock, faLockOpen, faBook, faHammer, faBan } from '@fortawesome/free-s
import { GameIdContext } from '../app';
import Markdown from './markdown';
import { useLoadDocQuery, InventoryTile, LevelInfo, InventoryOverview } from '../state/api';
import { selectInventory } from '../state/progress';
import { selectDifficulty, selectInventory } from '../state/progress';
import { store } from '../state/store';
import { useSelector } from 'react-redux';
export function Inventory({levelInfo, openDoc, enableAll=false} :
{
@ -50,6 +51,8 @@ function InventoryList({items, docType, openDoc, defaultTab=null, level=undefine
const gameId = React.useContext(GameIdContext)
const difficulty = useSelector(selectDifficulty(gameId))
const categorySet = new Set<string>()
for (let item of items) {
categorySet.add(item.category)
@ -87,7 +90,7 @@ function InventoryList({items, docType, openDoc, defaultTab=null, level=undefine
).filter(item => ((tab ?? categories[0]) == item.category)).map((item, i) => {
return <InventoryItem key={`${item.category}-${item.name}`}
showDoc={() => {openDoc(item.name, docType)}}
name={item.name} displayName={item.displayName} locked={item.locked}
name={item.name} displayName={item.displayName} locked={difficulty > 0 ? item.locked : false}
disabled={item.disabled} newly={item.new} enableAll={enableAll}/>
})
}

@ -20,7 +20,7 @@ import { InfoProvider } from 'lean4web/client/src/editor/infoview';
import 'lean4web/client/src/editor/infoview.css'
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
import './level.css'
import { useStore } from 'react-redux';
import { useSelector, useStore } from 'react-redux';
import { EditorContext, ConfigContext, ProgressContext, VersionContext } from '../../../node_modules/lean4-infoview/src/infoview/contexts';
import { EditorConnection, EditorEvents } from '../../../node_modules/lean4-infoview/src/infoview/editorConnection';
import { EventEmitter } from '../../../node_modules/lean4-infoview/src/infoview/event';
@ -36,7 +36,7 @@ import { Button } from './button'
import Markdown from './markdown';
import {Inventory, Documentation} from './inventory';
import { useGetGameInfoQuery, useLoadLevelQuery } from '../state/api';
import { changedSelection, codeEdited, selectCode, selectSelections, progressSlice, selectCompleted, helpEdited, selectHelp } from '../state/progress';
import { changedSelection, codeEdited, selectCode, selectSelections, progressSlice, selectCompleted, helpEdited, selectHelp, selectDifficulty } from '../state/progress';
import { DualEditor } from './infoview/main'
import { DeletedHint, DeletedHints, Hints } from './hints';
import { DeletedChatContext, InputModeContext, MonacoEditorContext, ProofContext, ProofStep, SelectionContext } from './infoview/context';
@ -389,6 +389,9 @@ function LevelAppBar({isLoading, levelId, worldId, levelTitle, toggleImpressum})
const gameId = React.useContext(GameIdContext)
const gameInfo = useGetGameInfoQuery({game: gameId})
const difficulty = useSelector(selectDifficulty(gameId))
const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
const { commandLineMode, setCommandLineMode } = React.useContext(InputModeContext)
return <div className="app-bar" style={isLoading ? {display: "none"} : null} >
@ -414,7 +417,8 @@ function LevelAppBar({isLoading, levelId, worldId, levelTitle, toggleImpressum})
</Button>
</>}
{levelId < gameInfo.data?.worldSize[worldId] &&
<Button inverted="true" to={`/${gameId}/world/${worldId}/level/${levelId + 1}`} title="next level">
<Button inverted="true" to={`/${gameId}/world/${worldId}/level/${levelId + 1}`} title="next level"
disabled={difficulty >= 2 && !completed}>
Next&nbsp;<FontAwesomeIcon icon={faArrowRight} />
</Button>
}

@ -20,7 +20,9 @@
}
.welcome-text {
padding: 20px;
padding-left: 20px;
padding-right: 20px;
padding-bottom: 20px;
}
i {
@ -91,6 +93,10 @@ svg .level:hover .level-title {
opacity: 1;
}
svg .disabled {
cursor: default;
}
/******************/
/* Privacy Button */
/******************/

@ -4,17 +4,17 @@ import { Link } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import Split from 'react-split'
import { Box, Typography, CircularProgress } from '@mui/material';
import { Box, Typography, CircularProgress, Slider } from '@mui/material';
import cytoscape, { LayoutOptions } from 'cytoscape'
import klay from 'cytoscape-klay';
import './welcome.css'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faGlobe, faHome, faCircleInfo, faArrowRight, faArrowLeft, faShield, faRotateLeft } from '@fortawesome/free-solid-svg-icons'
import { GameIdContext } from '../app';
import { selectCompleted } from '../state/progress';
import { selectCompleted, selectDifficulty } from '../state/progress';
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery } from '../state/api';
import Markdown from './markdown';
import WorldSelectionMenu from './world_selection_menu';
import WorldSelectionMenu, { WelcomeMenu } from './world_selection_menu';
import {PrivacyPolicy} from './privacy_policy';
import { Button } from './button';
import { Documentation, Inventory } from './inventory';
@ -31,12 +31,17 @@ const padding = 2000 // padding of the graphic (on a different scale)
function LevelIcon({ worldId, levelId, position, completed, available }) {
const gameId = React.useContext(GameIdContext)
const difficulty = useSelector(selectDifficulty(gameId))
const x = s * position.x + Math.sin(levelId * 2 * Math.PI / N) * (R + 1.2*r + 2.4*r*Math.floor((levelId - 1)/N))
const y = s * position.y - Math.cos(levelId * 2 * Math.PI / N) * (R + 1.2*r + 2.4*r*Math.floor((levelId - 1)/N))
let levelDisabled = (difficulty >= 2 && !(available || completed))
// TODO: relative positioning?
return (
<Link to={`/${gameId}/world/${worldId}/level/${levelId}`} className="level">
<Link to={levelDisabled ? '' : `/${gameId}/world/${worldId}/level/${levelId}`}
className={`level${levelDisabled ? ' disabled' : ''}`}>
<circle fill={completed ? "#139e13" : available? "#1976d2" : "#999"} cx={x} cy={y} r={r} />
<foreignObject className="level-title-wrapper" x={x} y={y}
width={1.42*r} height={1.42*r} transform={"translate("+ -.71*r +","+ -.71*r +")"}>
@ -58,6 +63,8 @@ function Welcome() {
const inventory = useLoadInventoryOverviewQuery({game: gameId})
const difficulty = useSelector(selectDifficulty(gameId))
// 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.
const [inventoryDoc, setInventoryDoc] = useState<{name: string, type: string}>(null)
@ -137,9 +144,13 @@ function Welcome() {
nextLevel = 0
}
let worldDisabled = (difficulty >= 2 && !(worldUnlocked || worldCompleted))
// Draw the worlds
svgElements.push(
<Link key={`world${worldId}`} to={`/${gameId}/world/${worldId}/level/${nextLevel}`}>
<Link key={`world${worldId}`}
to={worldDisabled ? '' : `/${gameId}/world/${worldId}/level/${nextLevel}`}
className={worldDisabled ? 'disabled' : ''}>
<circle className="world-circle" cx={s*position.x} cy={s*position.y} r={R}
fill={worldCompleted ? "green" : worldUnlocked ? "#1976d2": "#999"}/>
<foreignObject className="world-title-wrapper" x={s*position.x} y={s*position.y}
@ -164,9 +175,7 @@ function Welcome() {
<Split className="welcome" minSize={200} sizes={[40, 35, 25]}>
<div className="column">
<Typography variant="body1" component="div" className="welcome-text">
<Button inverted="false" title="back to games selection" to="/">
<FontAwesomeIcon icon={faArrowLeft} />&nbsp;<FontAwesomeIcon icon={faGlobe} />
</Button>
<WelcomeMenu />
<Markdown>{gameInfo.data?.introduction}</Markdown>
</Typography>
</div>

@ -9,3 +9,34 @@
margin-right: .4em;
margin-bottom: .2em;
}
.world-selection-menu .slider-wrap {
display: inline-block;
width: 100%;
/* min-width: 16em; */
padding-left: 3em;
padding-right: 3em;
margin-left: auto;
margin-right: auto;
}
/* Test for mobile `title`s */
@media (pointer: coarse), (hover: none) {
[title] {
position: relative;
display: inline-flex;
justify-content: center;
}
[title]:focus::after {
content: attr(title);
position: absolute;
top: 90%;
color: #000;
background-color: #fff;
border: 1px solid;
width: fit-content;
padding: 3px;
}
}

@ -4,14 +4,15 @@
import * as React from 'react'
import { useStore, useSelector } from 'react-redux';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faDownload, faUpload, faEraser } from '@fortawesome/free-solid-svg-icons'
import { faDownload, faUpload, faEraser, faGlobe, faHome, faArrowLeft } from '@fortawesome/free-solid-svg-icons'
import './world_selection_menu.css'
import { Button } from './button'
import { GameIdContext } from '../app';
import { useAppDispatch, useAppSelector } from '../hooks';
import { deleteProgress, selectProgress, loadProgress, GameProgressState } from '../state/progress';
import { deleteProgress, selectProgress, loadProgress, GameProgressState, selectDifficulty, changedDifficulty } from '../state/progress';
import { Slider } from '@mui/material';
/** Only to specify the types for `downloadFile` */
interface downloadFileParam {
@ -35,12 +36,35 @@ const downloadFile = ({ data, fileName, fileType } : downloadFileParam) => {
a.remove()
}
// <div><Button inverted="false" title="back to games selection" to="/">
// <FontAwesomeIcon icon={faArrowLeft} />&nbsp;<FontAwesomeIcon icon={faGlobe} />
// </Button>
// <Slider min={0} max={2}></Slider>
/** The menu that is shown next to the world selection graph */
export function WelcomeMenu() {
function label(x : number) {
return x == 0 ? 'Easy' : x == 1 ? 'Explorer' : 'Strict'
}
return <nav className="world-selection-menu">
<Button inverted="false" title="back to games selection" to="/">
<FontAwesomeIcon icon={faArrowLeft} />&nbsp;<FontAwesomeIcon icon={faGlobe} />
</Button>
</nav>
}
/** The menu that is shown next to the world selection graph */
function WorldSelectionMenu() {
export function WorldSelectionMenu() {
const [file, setFile] = React.useState<File>();
const gameId = React.useContext(GameIdContext)
const store = useStore()
const difficulty = useSelector(selectDifficulty(gameId))
/* state variables to toggle the pop-up menus */
const [eraseMenu, setEraseMenu] = React.useState(false);
@ -92,10 +116,34 @@ function WorldSelectionMenu() {
eraseProgress()
}
function label(x : number) {
return x == 0 ? 'Playground' : x == 1 ? 'Explorer' : 'Strict'
}
return <nav className="world-selection-menu">
<Button onClick={downloadProgress} title="Download game progress" to=""><FontAwesomeIcon icon={faDownload} /></Button>
<Button title="Load game progress from JSON" onClick={openUploadMenu} to=""><FontAwesomeIcon icon={faUpload} /></Button>
<Button title="Clear game progress" to="" onClick={openEraseMenu}><FontAwesomeIcon icon={faEraser} /></Button>
<div className="slider-wrap">
<Slider
title="Difficulties:&#10;- strict: 🔐 levels, 🔐 tactics&#10;- explorer: 🔓 levels, 🔐 tactics&#10;- playground: 🔓 levels, 🔓 tactics"
min={0} max={2}
aria-label="Mode"
defaultValue={difficulty}
marks={[
{value: 0, label: 'playground'},
{value: 1, label: 'explorer'},
{value: 2, label: 'strict'}
]}
valueLabelFormat={label}
getAriaValueText={label}
valueLabelDisplay="auto"
onChange={(ev, val: number) => {
dispatch(changedDifficulty({game: gameId, difficulty: val}))
}}
></Slider>
</div>
{eraseMenu?
<div className="modal-wrapper">
<div className="modal-backdrop" onClick={closeEraseMenu} />

@ -24,9 +24,13 @@ interface WorldProgressState {
export interface GameProgressState {
inventory: string[],
// Difficulty: the default is 2.
difficulty: number,
data: WorldProgressState
}
const DEFAULT_DIFFICULTY = 2
/** The progress made on all lean4-games */
interface ProgressState {
games: {[game: string]: GameProgressState}
@ -40,7 +44,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]) {
state.games[action.payload.game] = {inventory: [], data: {}}
state.games[action.payload.game] = {inventory: [], data: {}, difficulty: DEFAULT_DIFFICULTY}
}
}
@ -83,7 +87,7 @@ export const progressSlice = createSlice({
},
/** delete all progress for this game */
deleteProgress(state: ProgressState, action: PayloadAction<{game: string}>) {
state.games[action.payload.game] = {inventory: [], data: {}}
state.games[action.payload.game] = {inventory: [], data: {}, difficulty: DEFAULT_DIFFICULTY}
},
/** delete progress for this level */
deleteLevelProgress(state: ProgressState, action: PayloadAction<{game: string, world: string, level: number}>) {
@ -100,6 +104,11 @@ export const progressSlice = createSlice({
addGameProgress(state, action)
state.games[action.payload.game].inventory = action.payload.inventory
},
/** set the difficulty */
changedDifficulty(state: ProgressState, action: PayloadAction<{game: string, difficulty: number}>) {
addGameProgress(state, action)
state.games[action.payload.game].difficulty = action.payload.difficulty
},
}
})
@ -156,6 +165,14 @@ export function selectProgress(game: string) {
}
}
/** return progress for the current game if it exists */
export function selectDifficulty(game: string) {
return (state) => {
return state.progress.games[game].difficulty ?? DEFAULT_DIFFICULTY
}
}
/** Export actions to modify the progress */
export const { changedSelection, codeEdited, levelCompleted, deleteProgress,
deleteLevelProgress, loadProgress, helpEdited, changedInventory } = progressSlice.actions
deleteLevelProgress, loadProgress, helpEdited, changedInventory,
changedDifficulty } = progressSlice.actions

Loading…
Cancel
Save