diff --git a/public/index.html b/public/index.html index beed347..e38b61b 100644 --- a/public/index.html +++ b/public/index.html @@ -5,18 +5,12 @@ - -
diff --git a/src/App.tsx b/src/App.tsx index 8cbccd0..4ae346d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,7 +14,6 @@ import ChangeDiscScene from "./scenes/ChangeDiscScene"; import EndScene from "./scenes/EndScene"; import IdleMediaScene from "./scenes/IdleMediaScene"; import KeyPressHandler from "./components/KeyPressHandler"; -import Preloader from "./components/Preloader"; const App = () => { const currentScene = useStore((state) => state.currentScene); @@ -35,6 +34,7 @@ const App = () => { tak: , change_disc: , end: , + null: <>, }), [] ); diff --git a/src/__tests__/save-state.test.ts b/src/__tests__/save-state.test.ts new file mode 100644 index 0000000..fbebb12 --- /dev/null +++ b/src/__tests__/save-state.test.ts @@ -0,0 +1,9 @@ +import { getCurrentUserState } from "../store"; + +it("Checks if setting state on localStorage works", () => { + const spy = jest.spyOn(Storage.prototype, "setItem"); + const saveState = JSON.stringify(getCurrentUserState()); + localStorage.setItem("lainSaveState", saveState); + expect(spy).toHaveBeenCalledTimes(1); + expect(localStorage.getItem("lainSaveState")).toEqual(saveState); +}); diff --git a/src/components/KeyPressHandler.tsx b/src/components/KeyPressHandler.tsx index 90bc5e8..ee24cca 100644 --- a/src/components/KeyPressHandler.tsx +++ b/src/components/KeyPressHandler.tsx @@ -8,7 +8,7 @@ import { playAudio, useStore, } from "../store"; -import { getKeyCodeAssociation } from "../utils/getKey"; +import { getKeyCodeAssociation } from "../utils/parseUserInput"; import handleMediaSceneKeyPress from "../core/scene-keypress-handlers/handleMediaSceneKeyPress"; import handleSsknSceneKeyPress from "../core/scene-keypress-handlers/handleSsknSceneKeyPress"; import handleMainSceneKeyPress from "../core/scene-keypress-handlers/handleMainSceneKeyPress"; @@ -18,7 +18,7 @@ import { getRandomIdleLainAnim } from "../helpers/idle-helpers"; import * as audio from "../static/audio/sfx"; import handleEndSceneKeyPress from "../core/scene-keypress-handlers/handleEndSceneKeyPress"; import handleEvent from "../core/handleEvent"; -import {GameEvent} from "../types/types"; +import { GameEvent } from "../types/types"; const KeyPressHandler = () => { const scene = useStore((state) => state.currentScene); @@ -75,9 +75,9 @@ const KeyPressHandler = () => { const now = Date.now(); if ( - keyPress - // now > timeSinceLastKeyPress.current + inputCooldown && - // inputCooldown !== -1 + keyPress && + now > timeSinceLastKeyPress.current + inputCooldown && + inputCooldown !== -1 ) { if (scene === "main") { lainIdleCounter.current = now; diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx index de1ec6f..7756f71 100644 --- a/src/components/Loading.tsx +++ b/src/components/Loading.tsx @@ -21,14 +21,26 @@ const Loading = memo(() => { return ( <> - - + + - - + + - - + + ); diff --git a/src/components/MainScene/LevelSelection.tsx b/src/components/MainScene/LevelSelection.tsx index 08be15d..91191ab 100644 --- a/src/components/MainScene/LevelSelection.tsx +++ b/src/components/MainScene/LevelSelection.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from "react"; +import React, { useEffect, useMemo, useRef } from "react"; import level_selection_font from "../../static/sprite/select_level_font.png"; import verticalHud from "../../static/sprite/select_level_hud_vertical.png"; import horizontalHud from "../../static/sprite/select_level_hud_horizontal.png"; diff --git a/src/components/MainScene/Site/Site.tsx b/src/components/MainScene/Site/Site.tsx index 1d19e5b..4c3b6fc 100644 --- a/src/components/MainScene/Site/Site.tsx +++ b/src/components/MainScene/Site/Site.tsx @@ -1,6 +1,6 @@ -import React, {Suspense, useEffect, useMemo} from "react"; -import {a, useSpring} from "@react-spring/three"; -import {useStore} from "../../../store"; +import React, { Suspense, useEffect, useMemo } from "react"; +import { a, useSpring } from "@react-spring/three"; +import { useStore } from "../../../store"; import ActiveLevelNodes from "./ActiveLevelNodes"; import Rings from "./Rings"; import NodeAnimations from "./NodeAnimations"; @@ -8,7 +8,7 @@ import InactiveLevelNodes from "./InactiveLevelNodes"; import site_a from "../../../resources/site_a.json"; import site_b from "../../../resources/site_b.json"; import level_y_values from "../../../resources/level_y_values.json"; -import {filterInvisibleNodes} from "../../../helpers/node-helpers"; +import { filterInvisibleNodes } from "../../../helpers/node-helpers"; import Loading from "../../Loading"; type SiteProps = { @@ -67,7 +67,7 @@ const Site = (props: SiteProps) => { ); return ( - }> + diff --git a/src/components/MainScene/Starfield/IntroStar.tsx b/src/components/MainScene/Starfield/IntroStar.tsx new file mode 100644 index 0000000..45b3f1a --- /dev/null +++ b/src/components/MainScene/Starfield/IntroStar.tsx @@ -0,0 +1,79 @@ +import React, { useMemo, useRef } from "react"; +import { a } from "@react-spring/three"; +import * as THREE from "three"; +import { useFrame } from "react-three-fiber"; + +type IntroStarProps = { + position: number[]; + color: string; +}; + +const IntroStar = (props: IntroStarProps) => { + const uniforms = useMemo( + () => ({ + color1: { + value: new THREE.Color("white"), + }, + color2: { + value: new THREE.Color(props.color), + }, + }), + [props.color] + ); + + const vertexShader = ` + varying vec2 vUv; + + void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `; + + const fragmentShader = ` + uniform vec3 color1; + uniform vec3 color2; + uniform float alpha; + + varying vec2 vUv; + + void main() { + float alpha = smoothstep(0.0, 1.0, vUv.y); + float colorMix = smoothstep(1.0, 2.0, 1.8); + + gl_FragColor = vec4(mix(color1, color2, colorMix), alpha) * 0.8; + } + `; + + const starRef = useRef(); + + const amp = useRef(Math.random() / 10); + + useFrame(() => { + if (starRef.current && starRef.current.visible) { + starRef.current.position.y += 0.25 + amp.current; + if (starRef.current.position.y > 40) starRef.current.visible = false; + } + }); + + return ( + + + + + ); +}; + +export default IntroStar; diff --git a/src/components/MainScene/Starfield/Star.tsx b/src/components/MainScene/Starfield/Star.tsx index 1a18d7c..d6d3dea 100644 --- a/src/components/MainScene/Starfield/Star.tsx +++ b/src/components/MainScene/Starfield/Star.tsx @@ -1,26 +1,27 @@ -import React, { useRef } from "react"; +import React, { useMemo, useRef } from "react"; import { a } from "@react-spring/three"; import * as THREE from "three"; import { useFrame } from "react-three-fiber"; +import lerp from "../../../utils/lerp"; type StarProps = { position: number[]; color: string; - introStar?: boolean; - shouldIntro?: boolean; + shouldIntro: boolean; }; const Star = (props: StarProps) => { - const uniformConstructor = (col: string) => { - return { + const uniforms = useMemo( + () => ({ color1: { value: new THREE.Color("white"), }, color2: { - value: new THREE.Color(col), + value: new THREE.Color(props.color), }, - }; - }; + }), + [props.color] + ); const vertexShader = ` varying vec2 vUv; @@ -54,17 +55,11 @@ const Star = (props: StarProps) => { useFrame(() => { if (starRef.current) { - if (props.introStar) { - starRef.current.position.y += 0.25 + amp.current; - } else { - if (starRef.current.position.y > 4) { - starRef.current.position.y = props.position[1]; - } - starRef.current.position.y += 0.01 + amp.current + introAmpRef.current; - if (introAmpRef.current > 0) { - introAmpRef.current -= 0.004; - } + if (starRef.current.position.y > 4) { + starRef.current.position.y = props.position[1]; } + starRef.current.position.y += 0.01 + amp.current + introAmpRef.current; + introAmpRef.current = lerp(introAmpRef.current, 0, 0.01); } }); @@ -82,7 +77,7 @@ const Star = (props: StarProps) => { vertexShader={vertexShader} transparent={true} depthWrite={false} - uniforms={uniformConstructor(props.color)} + uniforms={uniforms} /> ); diff --git a/src/components/MainScene/Starfield/Starfield.tsx b/src/components/MainScene/Starfield/Starfield.tsx index e4ca7ff..8b5d140 100644 --- a/src/components/MainScene/Starfield/Starfield.tsx +++ b/src/components/MainScene/Starfield/Starfield.tsx @@ -1,5 +1,6 @@ -import React, { memo, useEffect, useMemo, useState } from "react"; +import React, { memo, useEffect, useMemo, useRef, useState } from "react"; import Star from "./Star"; +import IntroStar from "./IntroStar"; type StarfieldProps = { shouldIntro: boolean; @@ -34,17 +35,11 @@ const Starfield = memo((props: StarfieldProps) => { ].map((x) => Array.from({ length: x }, () => [ lcgInstance() / 1000000050, - lcgInstance() / 100000099 - 15, + lcgInstance() / 100000059 + 5, lcgInstance() / 1000000050, ]) ); - const [introVisible, setIntroVisible] = useState(true); - - useEffect(() => { - setTimeout(() => setIntroVisible(false), 3200); - }, []); - return ( <> @@ -101,20 +96,18 @@ const Starfield = memo((props: StarfieldProps) => { ))} - {introVisible && props.shouldIntro ? ( + {props.shouldIntro && ( {posesBlueFromBottom.map((poses, idx) => ( - + ))} {posesWhiteFromBottom.map((poses, idx) => ( - + ))} {posesCyanFromBottom.map((poses, idx) => ( - + ))} - ) : ( - <> )} ); diff --git a/src/components/TextRenderer/BigLetter.tsx b/src/components/TextRenderer/BigLetter.tsx index 8fd7345..9d62457 100644 --- a/src/components/TextRenderer/BigLetter.tsx +++ b/src/components/TextRenderer/BigLetter.tsx @@ -80,7 +80,7 @@ const BigLetter = memo((props: { letter: string; letterIdx: number }) => { const subscene = useStore((state) => state.mainSubscene); const scene = useStore((state) => state.currentScene); - const prevData = usePrevious({ scene, subscene }); + const prevData = usePrevious({ scene, subscene, activeNode }); const [lastMediaLeftComponent, setLastMediaLeftComponent] = useState("play"); const [shrinkState, set] = useSpring(() => ({ @@ -91,7 +91,13 @@ const BigLetter = memo((props: { letter: string; letterIdx: number }) => { useEffect(() => { if ( subscene === "pause" || - (subscene === "site" && prevData?.subscene === "pause") + (subscene === "site" && prevData?.subscene === "pause") || + (activeNode === prevData?.activeNode && + !( + subscene === "level_selection" || + color === "orange" || + scene === "media" + )) ) return; if (scene === "main" && prevData?.scene === "main") { @@ -123,6 +129,7 @@ const BigLetter = memo((props: { letter: string; letterIdx: number }) => { lastMediaLeftComponent, prevData?.scene, prevData?.subscene, + prevData?.activeNode, ]); return ( diff --git a/src/core/eventTemplates.ts b/src/core/eventTemplates.ts index 3848e79..59674c2 100644 --- a/src/core/eventTemplates.ts +++ b/src/core/eventTemplates.ts @@ -9,14 +9,23 @@ import { import { playMediaElement, resetMediaElement } from "../helpers/media-helpers"; import { ActiveSite, - EndComponent, GameProgress, GameScene, + EndComponent, + GameScene, LeftMediaComponent, - MediaComponent, MediaSide, NodeData, + MediaComponent, + MediaSide, + NodeData, PromptComponent, RightMediaComponent, SiteSaveState, - SsknComponent + SsknComponent, + UserSaveState, } from "../types/types"; +import { saveUserProgress, useStore } from "../store"; + +const setNodeViewed = useStore.getState().setNodeViewed; +const resetMediaScene = useStore.getState().resetMediaScene; +const loadUserSaveState = useStore.getState().loadUserSaveState; export const siteMoveHorizontal = (calculatedState: { lainMoveAnimation: string; @@ -332,28 +341,64 @@ export const exitPrompt = { audio: [{ sfx: [audio.sound28] }], }; -// todo actually save -export const saveGame = () => ({ +export const saveGame = (calculatedState: { + userSaveState: UserSaveState; +}) => ({ state: [ { mutation: { saveSuccessful: true, inputCooldown: 1200 } }, { - mutation: { saveSuccessful: undefined }, + mutation: { + saveSuccessful: undefined, + promptVisible: false, + activePromptComponent: "no", + }, delay: 1200, }, ], audio: [{ sfx: [audio.sound28] }], + effects: [() => saveUserProgress(calculatedState.userSaveState)], }); -// todo actually load -export const loadGame = () => ({ +export const loadGameFail = { + state: [ + { + mutation: { + loadSuccessful: false, + inputCooldown: 1200, + }, + }, + { mutation: { loadSuccessful: undefined }, delay: 1200 }, + ], + audio: [{ sfx: [audio.sound28] }], +}; + +export const loadGame = (calculatedState: { + userSaveState: UserSaveState; +}) => ({ state: [ { mutation: { loadSuccessful: true, inputCooldown: 1200 } }, { - mutation: { loadSuccessful: undefined }, + mutation: { + loadSuccessful: undefined, + currentScene: "null", + mainSubscene: "site", + lainMoveState: "standing", + promptVisible: false, + activePromptComponent: "no", + activePauseComponent: "change", + }, delay: 1200, }, + { + mutation: { currentScene: "main", intro: true }, + delay: 1300, + }, ], audio: [{ sfx: [audio.sound28] }], + effects: [ + () => + setTimeout(() => loadUserSaveState(calculatedState.userSaveState), 1200), + ], }); export const changeSite = (calculatedState: { @@ -366,6 +411,7 @@ export const changeSite = (calculatedState: { state: [ { mutation: { + intro: true, currentScene: "change_disc", lainMoveState: "standing", promptVisible: false, @@ -418,14 +464,28 @@ export const changeMediaSide = (calculatedState: { ], }); -export const playMedia = { +export const playMedia = (calculatedState: { activeNode: NodeData }) => ({ state: [{ mutation: { mediaPercentageElapsed: 0, inputCooldown: 500 } }], - effects: [playMediaElement], -}; + effects: [ + playMediaElement, + () => + setNodeViewed(calculatedState.activeNode.node_name, { + is_viewed: 1, + is_visible: 1, + }), + ], +}); export const exitMedia = { - state: [{ mutation: { currentScene: "main", inputCooldown: -1 } }], - effects: [resetMediaElement], + state: [ + { + mutation: { + currentScene: "main", + inputCooldown: -1, + }, + }, + ], + effects: [resetMediaElement, resetMediaScene], }; export const changeRightMediaComponent = (calculatedState: { @@ -455,7 +515,7 @@ export const wordNotFound = { }, ], audio: [{ sfx: [audio.sound30] }], - effects: [resetMediaElement], + effects: [resetMediaElement, resetMediaScene], }; export const hideWordNotFound = { @@ -480,7 +540,7 @@ export const selectWord = (calculatedState: { }, ], audio: [{ sfx: [audio.sound29] }], - effects: [resetMediaElement], + effects: [resetMediaElement, resetMediaScene], }); export const changeSsknComponent = (calculatedState: { @@ -496,19 +556,23 @@ export const changeSsknComponent = (calculatedState: { ], }); -export const upgradeSskn = (calculatedState: { - gameProgress: GameProgress; -}) => ({ +export const upgradeSskn = (calculatedState: { activeNode: NodeData }) => ({ state: [ { mutation: { - gameProgress: calculatedState.gameProgress, ssknLoading: true, inputCooldown: -1, }, }, { mutation: { currentScene: "main" }, delay: 6000 }, ], + effects: [ + () => + setNodeViewed(calculatedState.activeNode.node_name, { + is_viewed: 1, + is_visible: 0, + }), + ], }); export const exitSskn = { @@ -538,20 +602,11 @@ export const changeEndComponent = (calculatedState: { audio: [{ sfx: [audio.sound1] }], }); -export const endGame = { +export const endGame = (calculatedState: { userSaveState: UserSaveState }) => ({ state: [{ mutation: { currentScene: "boot", inputCooldown: -1 } }], audio: [{ sfx: [audio.sound0] }], -}; - -// todo this is probably buggy -export const continueGameAfterEnd = { - state: [ - { - mutation: { currentScene: "change_disc", intro: true, inputCooldown: -1 }, - }, - ], - audio: [{ sfx: [audio.sound0] }], -}; + effects: [() => saveUserProgress(calculatedState.userSaveState)], +}); export const changeMainMenuComponent = (calculatedState: { activeMainMenuComponent: "authorize_user" | "load_data"; diff --git a/src/core/scene-keypress-handlers/handleBootSceneKeyPress.ts b/src/core/scene-keypress-handlers/handleBootSceneKeyPress.ts index ead3974..aac5578 100644 --- a/src/core/scene-keypress-handlers/handleBootSceneKeyPress.ts +++ b/src/core/scene-keypress-handlers/handleBootSceneKeyPress.ts @@ -9,12 +9,13 @@ import { exitUserAuthorization, failUpdatePlayerName, loadGame, + loadGameFail, removePlayerNameLastChar, startNewGame, updateAuthorizeUserLetterIdx, updatePlayerName, } from "../eventTemplates"; -import {BootSceneContext, GameEvent} from "../../types/types"; +import { BootSceneContext, GameEvent } from "../../types/types"; const handleBootSceneKeyPress = ( bootSceneContext: BootSceneContext @@ -40,7 +41,13 @@ const handleBootSceneKeyPress = ( case "no": return exitLoadData; case "yes": - return loadGame(); + const stateToLoad = localStorage.getItem("lainSaveState"); + + if (stateToLoad) + return loadGame({ + userSaveState: JSON.parse(stateToLoad), + }); + else return loadGameFail; } } } else { diff --git a/src/core/scene-keypress-handlers/handleEndSceneKeyPress.ts b/src/core/scene-keypress-handlers/handleEndSceneKeyPress.ts index 27da1b0..2f591ef 100644 --- a/src/core/scene-keypress-handlers/handleEndSceneKeyPress.ts +++ b/src/core/scene-keypress-handlers/handleEndSceneKeyPress.ts @@ -1,14 +1,19 @@ -import { - changeEndComponent, - continueGameAfterEnd, - endGame, -} from "../eventTemplates"; -import {EndSceneContext, GameEvent} from "../../types/types"; +import { changeEndComponent, changeSite, endGame } from "../eventTemplates"; +import { EndSceneContext, GameEvent } from "../../types/types"; +import { getCurrentUserState } from "../../store"; const handleEndSceneKeyPress = ( endSceneContext: EndSceneContext ): GameEvent | undefined => { - const { keyPress, selectionVisible, activeEndComponent } = endSceneContext; + const { + keyPress, + selectionVisible, + activeEndComponent, + siteSaveState, + activeNode, + activeLevel, + siteRot, + } = endSceneContext; if (selectionVisible) { switch (keyPress) { @@ -19,9 +24,26 @@ const handleEndSceneKeyPress = ( case "CIRCLE": switch (activeEndComponent) { case "end": - return endGame; + return endGame({ userSaveState: getCurrentUserState() }); case "continue": - return continueGameAfterEnd; + const siteToLoad = "a"; + const stateToLoad = siteSaveState[siteToLoad]; + + const newSiteSaveState = { + ...siteSaveState, + b: { + activeNode: activeNode, + siteRot: [0, siteRot[1], 0], + activeLevel: activeLevel.toString().padStart(2, "0"), + }, + }; + return changeSite({ + newActiveSite: siteToLoad, + newActiveNode: stateToLoad.activeNode, + newSiteRot: stateToLoad.siteRot, + newActiveLevel: stateToLoad.activeLevel, + newSiteSaveState: newSiteSaveState, + }); } } } diff --git a/src/core/scene-keypress-handlers/handleMainSceneKeyPress.ts b/src/core/scene-keypress-handlers/handleMainSceneKeyPress.ts index 6a1abdd..f490f44 100644 --- a/src/core/scene-keypress-handlers/handleMainSceneKeyPress.ts +++ b/src/core/scene-keypress-handlers/handleMainSceneKeyPress.ts @@ -21,6 +21,7 @@ import { knockNode, knockNodeAndFall, loadGame, + loadGameFail, pauseGame, ripNode, saveGame, @@ -31,7 +32,8 @@ import { siteMoveVertical, throwNode, } from "../eventTemplates"; -import {GameEvent, MainSceneContext} from "../../types/types"; +import { GameEvent, MainSceneContext } from "../../types/types"; +import { getCurrentUserState } from "../../store"; const handleMainSceneKeyPress = ( mainSceneContext: MainSceneContext @@ -65,7 +67,7 @@ const handleMainSceneKeyPress = ( return exitPrompt; case "yes": switch (activePauseComponent) { - case "change": + case "change": { const siteToLoad = activeSite === "a" ? "b" : "a"; const stateToLoad = siteSaveState[siteToLoad]; @@ -84,10 +86,18 @@ const handleMainSceneKeyPress = ( newActiveLevel: stateToLoad.activeLevel, newSiteSaveState: newSiteSaveState, }); + } case "save": - return saveGame(); - case "load": - return loadGame(); + return saveGame({ userSaveState: getCurrentUserState() }); + case "load": { + const stateToLoad = localStorage.getItem("lainSaveState"); + + if (stateToLoad) + return loadGame({ + userSaveState: JSON.parse(stateToLoad), + }); + else return loadGameFail; + } } } } @@ -139,6 +149,13 @@ const handleMainSceneKeyPress = ( case "DOWN": { const direction = keyPress.toLowerCase(); + const upperLimit = activeSite === "a" ? 22 : 13; + if ( + (direction === "up" && level === upperLimit) || + (direction === "down" && level === 1) + ) + return; + const nodeData = findNode( activeNode, direction, @@ -198,9 +215,9 @@ const handleMainSceneKeyPress = ( } else { return eventAnimation({ currentScene: "media" }); } - case 8: - return eventAnimation({ currentScene: "gate" }); case 7: + return eventAnimation({ currentScene: "sskn" }); + case 8: return eventAnimation({ currentScene: "gate" }); case 9: return eventAnimation({ currentScene: "polytan" }); @@ -231,11 +248,17 @@ const handleMainSceneKeyPress = ( const direction = selectedLevel > level ? "up" : "down"; - // todo implement this row idx without mutating activenode - const rowIdx = direction === "up" ? 2 : 0; + const newStartingPoint = { + ...activeNode, + matrixIndices: { + matrixIdx: activeNode.matrixIndices!.matrixIdx, + rowIdx: direction === "up" ? 2 : 0, + colIdx: 0, + }, + }; const nodeData = findNode( - activeNode, + newStartingPoint, direction, selectedLevel, activeSite, diff --git a/src/core/scene-keypress-handlers/handleMediaSceneKeyPress.ts b/src/core/scene-keypress-handlers/handleMediaSceneKeyPress.ts index 47d09de..8702cbf 100644 --- a/src/core/scene-keypress-handlers/handleMediaSceneKeyPress.ts +++ b/src/core/scene-keypress-handlers/handleMediaSceneKeyPress.ts @@ -9,7 +9,11 @@ import { wordNotFound, } from "../eventTemplates"; import { isNodeVisible } from "../../helpers/node-helpers"; -import {GameEvent, MediaSceneContext, RightMediaComponent} from "../../types/types"; +import { + GameEvent, + MediaSceneContext, + RightMediaComponent, +} from "../../types/types"; const handleMediaSceneKeyPress = ( mediaSceneContext: MediaSceneContext @@ -47,7 +51,7 @@ const handleMediaSceneKeyPress = ( case "CIRCLE": switch (activeMediaComponent) { case "play": - return playMedia; + return playMedia({ activeNode: activeNode }); case "exit": return exitMedia; } diff --git a/src/core/scene-keypress-handlers/handleSsknSceneKeyPress.ts b/src/core/scene-keypress-handlers/handleSsknSceneKeyPress.ts index 02d0de1..25182ca 100644 --- a/src/core/scene-keypress-handlers/handleSsknSceneKeyPress.ts +++ b/src/core/scene-keypress-handlers/handleSsknSceneKeyPress.ts @@ -1,5 +1,5 @@ import { changeSsknComponent, exitSskn, upgradeSskn } from "../eventTemplates"; -import {GameEvent, SsknSceneContext} from "../../types/types"; +import { GameEvent, SsknSceneContext } from "../../types/types"; const handleSsknSceneKeyPress = ( ssknSceneContext: SsknSceneContext @@ -20,16 +20,7 @@ const handleSsknSceneKeyPress = ( case "CIRCLE": switch (activeSsknComponent) { case "ok": - const newGameProgress = { - ...gameProgress, - [activeNode.node_name]: { - is_viewed: 1, - is_visible: 0, - }, - sskn_level: gameProgress.sskn_level + 1, - }; - - return upgradeSskn({ gameProgress: newGameProgress }); + return upgradeSskn({ activeNode: activeNode }); case "cancel": return exitSskn; } diff --git a/src/helpers/node-helpers.ts b/src/helpers/node-helpers.ts index f775e39..0a7bd31 100644 --- a/src/helpers/node-helpers.ts +++ b/src/helpers/node-helpers.ts @@ -173,7 +173,7 @@ const move = (direction: string, [matrix, level]: [number, number]) => { }; export const findNode = ( - activeNode: NodeData, + startingPoint: NodeData, direction: string, level: number, activeSite: ActiveSite, @@ -192,11 +192,10 @@ export const findNode = ( down: [nextPos_down, ([, c]: [number, number]) => nextPos_down([-1, c])], }; - if (activeNode.matrixIndices) { + if (startingPoint.matrixIndices) { const nextPos = funcs[direction]; - const nodeId = activeNode.id; - let { matrixIdx, colIdx, rowIdx } = { ...activeNode.matrixIndices }; + let { matrixIdx, colIdx, rowIdx } = { ...startingPoint.matrixIndices }; const initialMatrixIdx = matrixIdx; @@ -228,6 +227,7 @@ export const findNode = ( [matrixIdx, level] = move(direction, [matrixIdx, level]); } + const nodeId = startingPoint.id; if (nodeId === "") [matrixIdx] = move(direction, [initialMatrixIdx, level]); if (direction === "up" || direction === "down" || nodeId === "") { diff --git a/src/scenes/BootScene.tsx b/src/scenes/BootScene.tsx index 1df104f..29735f5 100644 --- a/src/scenes/BootScene.tsx +++ b/src/scenes/BootScene.tsx @@ -12,10 +12,13 @@ const BootScene = () => { const [accelaVisible, setAccelaVisible] = useState(true); const [mainMenuVisible, setMainMenuVisible] = useState(false); + const setInputCooldown = useStore((state) => state.setInputCooldown); + useEffect(() => { setTimeout(() => setAccelaVisible(false), 2000); setTimeout(() => setMainMenuVisible(true), 6200); - }, []); + setTimeout(() => setInputCooldown(0), 500); + }, [setInputCooldown]); return ( diff --git a/src/scenes/GateScene.tsx b/src/scenes/GateScene.tsx index 0e844ae..518fad1 100644 --- a/src/scenes/GateScene.tsx +++ b/src/scenes/GateScene.tsx @@ -12,6 +12,7 @@ const GateScene = () => { const activeNodeName = useStore((state) => state.activeNode.node_name); const setNodeViewed = useStore((state) => state.setNodeViewed); + const setInputCooldown = useStore((state) => state.setInputCooldown); useEffect(() => { incrementGateLvl(); @@ -20,7 +21,8 @@ const GateScene = () => { is_visible: 0, }); setTimeout(() => setIntroAnim(false), 2500); - }, [activeNodeName, incrementGateLvl, setNodeViewed]); + setTimeout(() => setInputCooldown(0), 3500); + }, [activeNodeName, incrementGateLvl, setInputCooldown, setNodeViewed]); return ( diff --git a/src/scenes/MainScene.tsx b/src/scenes/MainScene.tsx index b4d1a1a..35104ea 100644 --- a/src/scenes/MainScene.tsx +++ b/src/scenes/MainScene.tsx @@ -50,8 +50,11 @@ const MainScene = () => { setStarfieldIntro(false); setLainIntroAnim(false); setIntroFinished(false); + setInputCooldown(-1); + } else { + setInputCooldown(0); } - }, [intro]); + }, [intro, setInputCooldown]); const [starfieldIntro, setStarfieldIntro] = useState(false); const [lainIntroAnim, setLainIntroAnim] = useState(false); @@ -80,18 +83,6 @@ const MainScene = () => { introWrapperRef.current.rotation.x -= 0.008; } - // introWrapperRef.current.position.z = THREE.MathUtils.lerp( - // introWrapperRef.current.position.z, - // intro ? 0 : -10, - // 0.01 - // ); - // - // introWrapperRef.current.rotation.x = THREE.MathUtils.lerp( - // introWrapperRef.current.rotation.x, - // intro ? 0 : Math.PI / 2, - // 0.01 - // ); - if ( !introFinished && !( diff --git a/src/scenes/MediaScene.tsx b/src/scenes/MediaScene.tsx index c7d448e..2a46a2a 100644 --- a/src/scenes/MediaScene.tsx +++ b/src/scenes/MediaScene.tsx @@ -19,6 +19,9 @@ const MediaScene = () => { const activeNode = useStore((state) => state.activeNode); const setScene = useStore((state) => state.setScene); + const incrementFinalVideoViewCount = useStore( + (state) => state.incrementFinalVideoViewCount + ); useEffect(() => { document.getElementsByTagName("canvas")[0].className = @@ -30,9 +33,16 @@ const MediaScene = () => { }, []); useEffect(() => { - if (percentageElapsed === 100 && activeNode.triggers_final_video) + if (percentageElapsed === 100 && activeNode.triggers_final_video) { setScene("end"); - }, [activeNode.triggers_final_video, percentageElapsed, setScene]); + incrementFinalVideoViewCount(); + } + }, [ + activeNode.triggers_final_video, + incrementFinalVideoViewCount, + percentageElapsed, + setScene, + ]); useEffect(() => { const mediaElement = document.getElementById("media") as HTMLMediaElement; diff --git a/src/scenes/SsknScene.tsx b/src/scenes/SsknScene.tsx index eb71455..a2e7c01 100644 --- a/src/scenes/SsknScene.tsx +++ b/src/scenes/SsknScene.tsx @@ -1,14 +1,23 @@ -import React from "react"; +import React, { useEffect } from "react"; import SsknIcon from "../components/SsknScene/SsknIcon"; import SsknBackground from "../components/SsknScene/SsknBackground"; import SsknHUD from "../components/SsknScene/SsknHUD"; +import { useStore } from "../store"; -const SsknScene = () => ( - <> - - - - -); +const SsknScene = () => { + const setInputCooldown = useStore((state) => state.setInputCooldown); + + useEffect(() => { + setTimeout(() => setInputCooldown(0), 500); + }, [setInputCooldown]); + + return ( + <> + + + + + ); +}; export default SsknScene; diff --git a/src/store.ts b/src/store.ts index a920a1c..0d832bc 100644 --- a/src/store.ts +++ b/src/store.ts @@ -6,28 +6,30 @@ import game_progress from "./resources/initial_progress.json"; import { getNodeById } from "./helpers/node-helpers"; import site_a from "./resources/site_a.json"; import { - ActiveSite, - BootSceneContext, - BootSubscene, - EndComponent, - EndSceneContext, - GameProgress, - GameScene, - LeftMediaComponent, - MainMenuComponent, - MainSceneContext, - MainSubscene, - MediaComponent, - MediaSceneContext, - MediaSide, - NodeAttributes, NodeData, - PauseComponent, - PolytanBodyParts, - PromptComponent, - RightMediaComponent, - SiteSaveState, - SsknComponent, - SsknSceneContext, + ActiveSite, + BootSceneContext, + BootSubscene, + EndComponent, + EndSceneContext, + GameProgress, + GameScene, + LeftMediaComponent, + MainMenuComponent, + MainSceneContext, + MainSubscene, + MediaComponent, + MediaSceneContext, + MediaSide, + NodeAttributes, + NodeData, + PauseComponent, + PolytanBodyParts, + PromptComponent, + RightMediaComponent, + SiteSaveState, + SsknComponent, + SsknSceneContext, + UserSaveState, } from "./types/types"; type State = { @@ -137,8 +139,8 @@ export const useStore = create( // nodes activeNode: { - ...site_a["04"]["0422"], - matrixIndices: { matrixIdx: 7, rowIdx: 0, colIdx: 0 }, + ...site_a["04"]["0414"], + matrixIndices: { matrixIdx: 7, rowIdx: 1, colIdx: 0 }, }, activeNodePos: [0, 0, 0], activeNodeRot: [0, 0, 0], @@ -233,19 +235,19 @@ export const useStore = create( siteSaveState: { a: { activeNode: { - ...getNodeById("0422", "a"), - matrixIndices: { matrixIdx: 7, rowIdx: 0, colIdx: 0 }, + ...getNodeById("0408", "a"), + matrixIndices: { matrixIdx: 7, rowIdx: 1, colIdx: 0 }, }, siteRot: [0, 0, 0], activeLevel: "04", }, b: { activeNode: { - ...getNodeById("0414", "b"), - matrixIndices: { matrixIdx: 7, rowIdx: 1, colIdx: 0 }, + ...getNodeById("0105", "b"), + matrixIndices: { matrixIdx: 6, rowIdx: 2, colIdx: 0 }, }, - siteRot: [0, 0, 0], - activeLevel: "04", + siteRot: [0, 0 - Math.PI / 4, 0], + activeLevel: "01", }, }, @@ -268,10 +270,28 @@ export const useStore = create( nodeName: string, to: { is_viewed: number; is_visible: number } ) => + set((state) => { + const nodes = { ...state.gameProgress.nodes, [nodeName]: to }; + return { + gameProgress: { + ...state.gameProgress, + nodes: nodes, + }, + }; + }), + + resetMediaScene: () => + set(() => ({ + activeMediaComponent: "play", + currentMediaSide: "left", + mediaWordPosStateIdx: 1, + })), + + incrementFinalVideoViewCount: () => set((state) => ({ gameProgress: { ...state.gameProgress, - [nodeName]: to, + final_video_viewcount: state.gameProgress.final_video_viewcount + 1, }, })), @@ -305,6 +325,15 @@ export const useStore = create( gate_level: state.gameProgress.gate_level + 1, }, })), + loadUserSaveState: (userState: UserSaveState) => + set(() => ({ + siteSaveState: userState.siteSaveState, + activeNode: userState.activeNode, + siteRot: userState.siteRot, + activeLevel: userState.activeLevel, + activeSite: userState.activeSite, + gameProgress: userState.gameProgress, + })), }) ) ); @@ -383,9 +412,29 @@ export const getEndSceneContext = (keyPress: string): EndSceneContext => { keyPress: keyPress, activeEndComponent: state.activeEndComponent, selectionVisible: state.endSceneSelectionVisible, + siteSaveState: state.siteSaveState, + activeNode: state.activeNode, + siteRot: state.siteRot, + activeLevel: state.activeLevel, }; }; +export const getCurrentUserState = (): UserSaveState => { + const state = useStore.getState(); + + return { + siteSaveState: state.siteSaveState, + activeNode: state.activeNode, + siteRot: [0, state.siteRot[1], 0], + activeLevel: state.activeLevel, + activeSite: state.activeSite, + gameProgress: state.gameProgress, + }; +}; + +export const saveUserProgress = (state: UserSaveState) => + localStorage.setItem("lainSaveState", JSON.stringify(state)); + export const playAudio = (audio: HTMLAudioElement) => { audio.currentTime = 0; audio.currentTime = 0; diff --git a/src/types/types.ts b/src/types/types.ts index 38c9952..62f19c2 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -19,7 +19,8 @@ export type GameScene = | "gate" | "boot" | "change_disc" - | "end"; + | "end" + | "null"; export type MainSubscene = "site" | "pause" | "level_selection"; @@ -143,6 +144,10 @@ export type EndSceneContext = { keyPress: string; activeEndComponent: EndComponent; selectionVisible: boolean; + siteSaveState: SiteSaveState; + activeNode: NodeData; + siteRot: number[]; + activeLevel: string; }; export type Level = { @@ -188,3 +193,12 @@ export type HUDData = { initial_position: number[]; }; }; + +export type UserSaveState = { + siteSaveState: SiteSaveState; + activeNode: NodeData; + siteRot: number[]; + activeLevel: string; + activeSite: ActiveSite; + gameProgress: GameProgress; +}; diff --git a/src/utils/getKey.ts b/src/utils/parseUserInput.ts similarity index 100% rename from src/utils/getKey.ts rename to src/utils/parseUserInput.ts diff --git a/src/utils/test-utils.tsx b/src/utils/test-utils.tsx deleted file mode 100644 index 79e3fb8..0000000 --- a/src/utils/test-utils.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, { FC, ReactElement, Suspense } from "react"; -import { render } from "@testing-library/react"; - -const SuspenseProvider: FC = ({ children }) => { - return {children}; -}; - -const customRender = (ui: ReactElement) => - render(ui, { wrapper: SuspenseProvider }); - -export * from "@testing-library/react"; - -export { customRender as render };