|
|
|
|
@ -18,26 +18,26 @@ import WorldSelectionMenu from './world_selection_menu';
|
|
|
|
|
import {PrivacyPolicy} from './privacy_policy';
|
|
|
|
|
import { Button } from './button';
|
|
|
|
|
import { Documentation, Inventory } from './inventory';
|
|
|
|
|
import { store } from '../state/store';
|
|
|
|
|
|
|
|
|
|
cytoscape.use( klay );
|
|
|
|
|
|
|
|
|
|
const N = 24 // max number of levels per world
|
|
|
|
|
const N = 18 // max number of levels per world
|
|
|
|
|
const R = 800 // radius of a world
|
|
|
|
|
const r = 110 // radius of a level
|
|
|
|
|
const s = 100 // global scale
|
|
|
|
|
const r = 160 // radius of a level
|
|
|
|
|
const s = 120 // global scale
|
|
|
|
|
const padding = 2000 // padding of the graphic (on a different scale)
|
|
|
|
|
|
|
|
|
|
function LevelIcon({ worldId, levelId, position }) {
|
|
|
|
|
function LevelIcon({ worldId, levelId, position, completed, available }) {
|
|
|
|
|
const gameId = React.useContext(GameIdContext)
|
|
|
|
|
const completed = useSelector(selectCompleted(gameId, worldId,levelId))
|
|
|
|
|
|
|
|
|
|
const x = s * position.x + Math.sin(levelId * 2 * Math.PI / N) * (R + 1.2*r + 2*Math.floor((levelId - 1)/N))
|
|
|
|
|
const y = s * position.y - Math.cos(levelId * 2 * Math.PI / N) * (R + 1.2*r + 2*Math.floor((levelId - 1)/N))
|
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
// TODO: relative positioning?
|
|
|
|
|
return (
|
|
|
|
|
<Link to={`/${gameId}/world/${worldId}/level/${levelId}`}>
|
|
|
|
|
<circle fill={completed ? "green" :"#999"} cx={x} cy={y} r={r} />
|
|
|
|
|
<circle fill={completed ? "#139e13" : available? "#1976d2" : "#999"} cx={x} cy={y} r={r} />
|
|
|
|
|
</Link>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
@ -72,35 +72,64 @@ function Welcome() {
|
|
|
|
|
|
|
|
|
|
const svgElements = []
|
|
|
|
|
|
|
|
|
|
// For each `worldId` as index, this contains a list of booleans with indices
|
|
|
|
|
// 0, 1, …, n. Index `0` will be set to `false` if any dependency is not completely solved.
|
|
|
|
|
// Indices `1, …, n` indicate if the corresponding level is completed
|
|
|
|
|
var completed = {}
|
|
|
|
|
|
|
|
|
|
if (gameInfo.data) {
|
|
|
|
|
|
|
|
|
|
// Fill `completed` with the level data.
|
|
|
|
|
for (let worldId in nodes) {
|
|
|
|
|
let position: cytoscape.Position = nodes[worldId].position
|
|
|
|
|
let state = store.getState()
|
|
|
|
|
|
|
|
|
|
completed[worldId] = Array.from({ length: gameInfo.data.worldSize[worldId] + 1 }, (_, i) => {
|
|
|
|
|
// Index `0` might be set to `false` in the loop over the edges
|
|
|
|
|
if (!i) {return true}
|
|
|
|
|
return selectCompleted(gameId, worldId, i)(state)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let i in gameInfo.data.worlds.edges) {
|
|
|
|
|
const edge = gameInfo.data.worlds.edges[i]
|
|
|
|
|
|
|
|
|
|
// If the origin world is not completed, mark the target world as non-playable
|
|
|
|
|
let unlocked = completed[edge[0]].slice(1).every(Boolean)
|
|
|
|
|
if (!unlocked) {completed[edge[1]][0] = false}
|
|
|
|
|
|
|
|
|
|
// Draw the connection edges
|
|
|
|
|
svgElements.push(
|
|
|
|
|
<line key={`pathway${i}`} x1={s*nodes[edge[0]].position.x} y1={s*nodes[edge[0]].position.y}
|
|
|
|
|
x2={s*nodes[edge[1]].position.x} y2={s*nodes[edge[1]].position.y} stroke="#1976d2" strokeWidth={s}/>
|
|
|
|
|
x2={s*nodes[edge[1]].position.x} y2={s*nodes[edge[1]].position.y}
|
|
|
|
|
stroke={unlocked ? "green" : "#bbb"} strokeWidth={s}/>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let id in nodes) {
|
|
|
|
|
let position: cytoscape.Position = nodes[id].position
|
|
|
|
|
|
|
|
|
|
for (let i = 1; i <= gameInfo.data.worldSize[id]; i++) {
|
|
|
|
|
for (let worldId in nodes) {
|
|
|
|
|
// Draw the level bubbles
|
|
|
|
|
let position: cytoscape.Position = nodes[worldId].position
|
|
|
|
|
for (let i = 1; i <= gameInfo.data.worldSize[worldId]; i++) {
|
|
|
|
|
svgElements.push(
|
|
|
|
|
<LevelIcon
|
|
|
|
|
key={`/${gameId}/world/${id}/level/${i}`}
|
|
|
|
|
position={position} worldId={id} levelId={i} />
|
|
|
|
|
key={`/${gameId}/world/${worldId}/level/${i}`}
|
|
|
|
|
position={position} worldId={worldId} levelId={i}
|
|
|
|
|
completed={completed[worldId][i]} available={completed[worldId][i-1]}/>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw the worlds
|
|
|
|
|
let worldUnlocked = completed[worldId][0]
|
|
|
|
|
let worldCompleted = completed[worldId].slice(1).every(Boolean)
|
|
|
|
|
svgElements.push(
|
|
|
|
|
<Link key={`world${id}`} to={`/${gameId}/world/${id}/level/0`}>
|
|
|
|
|
<Link key={`world${worldId}`} to={`/${gameId}/world/${worldId}/level/0`}>
|
|
|
|
|
<circle className="world-circle" cx={s*position.x} cy={s*position.y} r={R}
|
|
|
|
|
fill="#1976d2"/>
|
|
|
|
|
fill={worldCompleted ? "green" : worldUnlocked ? "#1976d2": "#999"}/>
|
|
|
|
|
<foreignObject className="world-title-wrapper" x={s*position.x} y={s*position.y}
|
|
|
|
|
width={1.42*R} height={1.42*R} transform={"translate("+ -.71*R +","+ -.71*R +")"}>
|
|
|
|
|
<div>
|
|
|
|
|
<p className="world-title" style={{fontSize: Math.floor(R/4) + "px"}}>
|
|
|
|
|
{nodes[id].data.title ? nodes[id].data.title : id}
|
|
|
|
|
{nodes[worldId].data.title ? nodes[worldId].data.title : worldId}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</foreignObject>
|
|
|
|
|
|