progress saving implemented, bug fixes and improvements

This commit is contained in:
ad044 2021-02-23 21:04:53 +04:00
parent d92e259bcc
commit 0956ae9fba
27 changed files with 455 additions and 199 deletions

View file

@ -5,18 +5,12 @@
<link rel="icon" type="image/png" href="icon.png" /> <link rel="icon" type="image/png" href="icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<style>
body {
background-color: #000;
}
</style>
</head> </head>
<body> <body>
<noscript <noscript
>do you actually believe i could rewrite this in plain html css or >do you actually believe i could rewrite this in plain html css or
smoetihng</noscript smoetihng</noscript
> >
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r119/three.min.js"></script>
<div id="root"></div> <div id="root"></div>
</body> </body>
</html> </html>

View file

@ -14,7 +14,6 @@ import ChangeDiscScene from "./scenes/ChangeDiscScene";
import EndScene from "./scenes/EndScene"; import EndScene from "./scenes/EndScene";
import IdleMediaScene from "./scenes/IdleMediaScene"; import IdleMediaScene from "./scenes/IdleMediaScene";
import KeyPressHandler from "./components/KeyPressHandler"; import KeyPressHandler from "./components/KeyPressHandler";
import Preloader from "./components/Preloader";
const App = () => { const App = () => {
const currentScene = useStore((state) => state.currentScene); const currentScene = useStore((state) => state.currentScene);
@ -35,6 +34,7 @@ const App = () => {
tak: <TaKScene />, tak: <TaKScene />,
change_disc: <ChangeDiscScene />, change_disc: <ChangeDiscScene />,
end: <EndScene />, end: <EndScene />,
null: <></>,
}), }),
[] []
); );

View file

@ -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);
});

View file

@ -8,7 +8,7 @@ import {
playAudio, playAudio,
useStore, useStore,
} from "../store"; } from "../store";
import { getKeyCodeAssociation } from "../utils/getKey"; import { getKeyCodeAssociation } from "../utils/parseUserInput";
import handleMediaSceneKeyPress from "../core/scene-keypress-handlers/handleMediaSceneKeyPress"; import handleMediaSceneKeyPress from "../core/scene-keypress-handlers/handleMediaSceneKeyPress";
import handleSsknSceneKeyPress from "../core/scene-keypress-handlers/handleSsknSceneKeyPress"; import handleSsknSceneKeyPress from "../core/scene-keypress-handlers/handleSsknSceneKeyPress";
import handleMainSceneKeyPress from "../core/scene-keypress-handlers/handleMainSceneKeyPress"; import handleMainSceneKeyPress from "../core/scene-keypress-handlers/handleMainSceneKeyPress";
@ -75,9 +75,9 @@ const KeyPressHandler = () => {
const now = Date.now(); const now = Date.now();
if ( if (
keyPress keyPress &&
// now > timeSinceLastKeyPress.current + inputCooldown && now > timeSinceLastKeyPress.current + inputCooldown &&
// inputCooldown !== -1 inputCooldown !== -1
) { ) {
if (scene === "main") { if (scene === "main") {
lainIdleCounter.current = now; lainIdleCounter.current = now;

View file

@ -21,14 +21,26 @@ const Loading = memo(() => {
return ( return (
<> <>
<sprite scale={[5, 5, 5]}> <sprite scale={[5, 5, 5]} renderOrder={999}>
<spriteMaterial attach="material" color={0x000000} /> <spriteMaterial attach="material" color={0x000000} depthTest={false} />
</sprite> </sprite>
<sprite scale={[0.35, 0.6, 0.35]} position={[0, 0.2, 0]}> <sprite
<spriteMaterial attach="material" map={loadingTex} /> scale={[0.35, 0.6, 0.35]}
position={[0, 0.2, 0]}
renderOrder={1000}
>
<spriteMaterial attach="material" map={loadingTex} depthTest={false} />
</sprite> </sprite>
<sprite scale={[0.4, 0.6, 0.4]} position={[0, -0.5, 0]}> <sprite
<spriteMaterial attach="material" map={lifeInstinctTex} /> scale={[0.4, 0.6, 0.4]}
position={[0, -0.5, 0]}
renderOrder={1000}
>
<spriteMaterial
attach="material"
map={lifeInstinctTex}
depthTest={false}
/>
</sprite> </sprite>
</> </>
); );

View file

@ -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 level_selection_font from "../../static/sprite/select_level_font.png";
import verticalHud from "../../static/sprite/select_level_hud_vertical.png"; import verticalHud from "../../static/sprite/select_level_hud_vertical.png";
import horizontalHud from "../../static/sprite/select_level_hud_horizontal.png"; import horizontalHud from "../../static/sprite/select_level_hud_horizontal.png";

View file

@ -67,7 +67,7 @@ const Site = (props: SiteProps) => {
); );
return ( return (
<Suspense fallback={<Loading />}> <Suspense fallback={null}>
<a.group rotation-x={rotXState.x}> <a.group rotation-x={rotXState.x}>
<a.group rotation-y={rotYState.y} position-y={posState.y}> <a.group rotation-y={rotYState.y} position-y={posState.y}>
<ActiveLevelNodes visibleNodes={visibleNodes} /> <ActiveLevelNodes visibleNodes={visibleNodes} />

View file

@ -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<THREE.Object3D>();
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 (
<mesh
position={props.position as [number, number, number]}
scale={[0.01, 2, 0.01]}
renderOrder={-1}
ref={starRef}
>
<boxBufferGeometry attach="geometry" args={[1, 1, 1]} />
<a.shaderMaterial
attach="material"
fragmentShader={fragmentShader}
vertexShader={vertexShader}
transparent={true}
depthWrite={false}
uniforms={uniforms}
/>
</mesh>
);
};
export default IntroStar;

View file

@ -1,26 +1,27 @@
import React, { useRef } from "react"; import React, { useMemo, useRef } from "react";
import { a } from "@react-spring/three"; import { a } from "@react-spring/three";
import * as THREE from "three"; import * as THREE from "three";
import { useFrame } from "react-three-fiber"; import { useFrame } from "react-three-fiber";
import lerp from "../../../utils/lerp";
type StarProps = { type StarProps = {
position: number[]; position: number[];
color: string; color: string;
introStar?: boolean; shouldIntro: boolean;
shouldIntro?: boolean;
}; };
const Star = (props: StarProps) => { const Star = (props: StarProps) => {
const uniformConstructor = (col: string) => { const uniforms = useMemo(
return { () => ({
color1: { color1: {
value: new THREE.Color("white"), value: new THREE.Color("white"),
}, },
color2: { color2: {
value: new THREE.Color(col), value: new THREE.Color(props.color),
}, },
}; }),
}; [props.color]
);
const vertexShader = ` const vertexShader = `
varying vec2 vUv; varying vec2 vUv;
@ -54,17 +55,11 @@ const Star = (props: StarProps) => {
useFrame(() => { useFrame(() => {
if (starRef.current) { if (starRef.current) {
if (props.introStar) {
starRef.current.position.y += 0.25 + amp.current;
} else {
if (starRef.current.position.y > 4) { if (starRef.current.position.y > 4) {
starRef.current.position.y = props.position[1]; starRef.current.position.y = props.position[1];
} }
starRef.current.position.y += 0.01 + amp.current + introAmpRef.current; starRef.current.position.y += 0.01 + amp.current + introAmpRef.current;
if (introAmpRef.current > 0) { introAmpRef.current = lerp(introAmpRef.current, 0, 0.01);
introAmpRef.current -= 0.004;
}
}
} }
}); });
@ -82,7 +77,7 @@ const Star = (props: StarProps) => {
vertexShader={vertexShader} vertexShader={vertexShader}
transparent={true} transparent={true}
depthWrite={false} depthWrite={false}
uniforms={uniformConstructor(props.color)} uniforms={uniforms}
/> />
</mesh> </mesh>
); );

View file

@ -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 Star from "./Star";
import IntroStar from "./IntroStar";
type StarfieldProps = { type StarfieldProps = {
shouldIntro: boolean; shouldIntro: boolean;
@ -34,17 +35,11 @@ const Starfield = memo((props: StarfieldProps) => {
].map((x) => ].map((x) =>
Array.from({ length: x }, () => [ Array.from({ length: x }, () => [
lcgInstance() / 1000000050, lcgInstance() / 1000000050,
lcgInstance() / 100000099 - 15, lcgInstance() / 100000059 + 5,
lcgInstance() / 1000000050, lcgInstance() / 1000000050,
]) ])
); );
const [introVisible, setIntroVisible] = useState(true);
useEffect(() => {
setTimeout(() => setIntroVisible(false), 3200);
}, []);
return ( return (
<> <>
<group position={[0, -1, 2]} visible={props.mainVisible}> <group position={[0, -1, 2]} visible={props.mainVisible}>
@ -101,20 +96,18 @@ const Starfield = memo((props: StarfieldProps) => {
))} ))}
</group> </group>
</group> </group>
{introVisible && props.shouldIntro ? ( {props.shouldIntro && (
<group position={[-2, -15, -30]} rotation={[Math.PI / 3, 0, 0]}> <group position={[-2, -15, -30]} rotation={[Math.PI / 3, 0, 0]}>
{posesBlueFromBottom.map((poses, idx) => ( {posesBlueFromBottom.map((poses, idx) => (
<Star position={poses} color={"blue"} key={idx} introStar={true} /> <IntroStar position={poses} color={"blue"} key={idx} />
))} ))}
{posesWhiteFromBottom.map((poses, idx) => ( {posesWhiteFromBottom.map((poses, idx) => (
<Star position={poses} color={"white"} key={idx} introStar={true} /> <IntroStar position={poses} color={"white"} key={idx} />
))} ))}
{posesCyanFromBottom.map((poses, idx) => ( {posesCyanFromBottom.map((poses, idx) => (
<Star position={poses} color={"cyan"} key={idx} introStar={true} /> <IntroStar position={poses} color={"cyan"} key={idx} />
))} ))}
</group> </group>
) : (
<></>
)} )}
</> </>
); );

View file

@ -80,7 +80,7 @@ const BigLetter = memo((props: { letter: string; letterIdx: number }) => {
const subscene = useStore((state) => state.mainSubscene); const subscene = useStore((state) => state.mainSubscene);
const scene = useStore((state) => state.currentScene); const scene = useStore((state) => state.currentScene);
const prevData = usePrevious({ scene, subscene }); const prevData = usePrevious({ scene, subscene, activeNode });
const [lastMediaLeftComponent, setLastMediaLeftComponent] = useState("play"); const [lastMediaLeftComponent, setLastMediaLeftComponent] = useState("play");
const [shrinkState, set] = useSpring(() => ({ const [shrinkState, set] = useSpring(() => ({
@ -91,7 +91,13 @@ const BigLetter = memo((props: { letter: string; letterIdx: number }) => {
useEffect(() => { useEffect(() => {
if ( if (
subscene === "pause" || subscene === "pause" ||
(subscene === "site" && prevData?.subscene === "pause") (subscene === "site" && prevData?.subscene === "pause") ||
(activeNode === prevData?.activeNode &&
!(
subscene === "level_selection" ||
color === "orange" ||
scene === "media"
))
) )
return; return;
if (scene === "main" && prevData?.scene === "main") { if (scene === "main" && prevData?.scene === "main") {
@ -123,6 +129,7 @@ const BigLetter = memo((props: { letter: string; letterIdx: number }) => {
lastMediaLeftComponent, lastMediaLeftComponent,
prevData?.scene, prevData?.scene,
prevData?.subscene, prevData?.subscene,
prevData?.activeNode,
]); ]);
return ( return (

View file

@ -9,14 +9,23 @@ import {
import { playMediaElement, resetMediaElement } from "../helpers/media-helpers"; import { playMediaElement, resetMediaElement } from "../helpers/media-helpers";
import { import {
ActiveSite, ActiveSite,
EndComponent, GameProgress, GameScene, EndComponent,
GameScene,
LeftMediaComponent, LeftMediaComponent,
MediaComponent, MediaSide, NodeData, MediaComponent,
MediaSide,
NodeData,
PromptComponent, PromptComponent,
RightMediaComponent, RightMediaComponent,
SiteSaveState, SiteSaveState,
SsknComponent SsknComponent,
UserSaveState,
} from "../types/types"; } 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: { export const siteMoveHorizontal = (calculatedState: {
lainMoveAnimation: string; lainMoveAnimation: string;
@ -332,28 +341,64 @@ export const exitPrompt = {
audio: [{ sfx: [audio.sound28] }], audio: [{ sfx: [audio.sound28] }],
}; };
// todo actually save export const saveGame = (calculatedState: {
export const saveGame = () => ({ userSaveState: UserSaveState;
}) => ({
state: [ state: [
{ mutation: { saveSuccessful: true, inputCooldown: 1200 } }, { mutation: { saveSuccessful: true, inputCooldown: 1200 } },
{ {
mutation: { saveSuccessful: undefined }, mutation: {
saveSuccessful: undefined,
promptVisible: false,
activePromptComponent: "no",
},
delay: 1200, delay: 1200,
}, },
], ],
audio: [{ sfx: [audio.sound28] }], audio: [{ sfx: [audio.sound28] }],
effects: [() => saveUserProgress(calculatedState.userSaveState)],
}); });
// todo actually load export const loadGameFail = {
export const loadGame = () => ({ state: [
{
mutation: {
loadSuccessful: false,
inputCooldown: 1200,
},
},
{ mutation: { loadSuccessful: undefined }, delay: 1200 },
],
audio: [{ sfx: [audio.sound28] }],
};
export const loadGame = (calculatedState: {
userSaveState: UserSaveState;
}) => ({
state: [ state: [
{ mutation: { loadSuccessful: true, inputCooldown: 1200 } }, { 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, delay: 1200,
}, },
{
mutation: { currentScene: "main", intro: true },
delay: 1300,
},
], ],
audio: [{ sfx: [audio.sound28] }], audio: [{ sfx: [audio.sound28] }],
effects: [
() =>
setTimeout(() => loadUserSaveState(calculatedState.userSaveState), 1200),
],
}); });
export const changeSite = (calculatedState: { export const changeSite = (calculatedState: {
@ -366,6 +411,7 @@ export const changeSite = (calculatedState: {
state: [ state: [
{ {
mutation: { mutation: {
intro: true,
currentScene: "change_disc", currentScene: "change_disc",
lainMoveState: "standing", lainMoveState: "standing",
promptVisible: false, 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 } }], state: [{ mutation: { mediaPercentageElapsed: 0, inputCooldown: 500 } }],
effects: [playMediaElement], effects: [
}; playMediaElement,
() =>
setNodeViewed(calculatedState.activeNode.node_name, {
is_viewed: 1,
is_visible: 1,
}),
],
});
export const exitMedia = { export const exitMedia = {
state: [{ mutation: { currentScene: "main", inputCooldown: -1 } }], state: [
effects: [resetMediaElement], {
mutation: {
currentScene: "main",
inputCooldown: -1,
},
},
],
effects: [resetMediaElement, resetMediaScene],
}; };
export const changeRightMediaComponent = (calculatedState: { export const changeRightMediaComponent = (calculatedState: {
@ -455,7 +515,7 @@ export const wordNotFound = {
}, },
], ],
audio: [{ sfx: [audio.sound30] }], audio: [{ sfx: [audio.sound30] }],
effects: [resetMediaElement], effects: [resetMediaElement, resetMediaScene],
}; };
export const hideWordNotFound = { export const hideWordNotFound = {
@ -480,7 +540,7 @@ export const selectWord = (calculatedState: {
}, },
], ],
audio: [{ sfx: [audio.sound29] }], audio: [{ sfx: [audio.sound29] }],
effects: [resetMediaElement], effects: [resetMediaElement, resetMediaScene],
}); });
export const changeSsknComponent = (calculatedState: { export const changeSsknComponent = (calculatedState: {
@ -496,19 +556,23 @@ export const changeSsknComponent = (calculatedState: {
], ],
}); });
export const upgradeSskn = (calculatedState: { export const upgradeSskn = (calculatedState: { activeNode: NodeData }) => ({
gameProgress: GameProgress;
}) => ({
state: [ state: [
{ {
mutation: { mutation: {
gameProgress: calculatedState.gameProgress,
ssknLoading: true, ssknLoading: true,
inputCooldown: -1, inputCooldown: -1,
}, },
}, },
{ mutation: { currentScene: "main" }, delay: 6000 }, { mutation: { currentScene: "main" }, delay: 6000 },
], ],
effects: [
() =>
setNodeViewed(calculatedState.activeNode.node_name, {
is_viewed: 1,
is_visible: 0,
}),
],
}); });
export const exitSskn = { export const exitSskn = {
@ -538,20 +602,11 @@ export const changeEndComponent = (calculatedState: {
audio: [{ sfx: [audio.sound1] }], audio: [{ sfx: [audio.sound1] }],
}); });
export const endGame = { export const endGame = (calculatedState: { userSaveState: UserSaveState }) => ({
state: [{ mutation: { currentScene: "boot", inputCooldown: -1 } }], state: [{ mutation: { currentScene: "boot", inputCooldown: -1 } }],
audio: [{ sfx: [audio.sound0] }], audio: [{ sfx: [audio.sound0] }],
}; effects: [() => saveUserProgress(calculatedState.userSaveState)],
});
// todo this is probably buggy
export const continueGameAfterEnd = {
state: [
{
mutation: { currentScene: "change_disc", intro: true, inputCooldown: -1 },
},
],
audio: [{ sfx: [audio.sound0] }],
};
export const changeMainMenuComponent = (calculatedState: { export const changeMainMenuComponent = (calculatedState: {
activeMainMenuComponent: "authorize_user" | "load_data"; activeMainMenuComponent: "authorize_user" | "load_data";

View file

@ -9,6 +9,7 @@ import {
exitUserAuthorization, exitUserAuthorization,
failUpdatePlayerName, failUpdatePlayerName,
loadGame, loadGame,
loadGameFail,
removePlayerNameLastChar, removePlayerNameLastChar,
startNewGame, startNewGame,
updateAuthorizeUserLetterIdx, updateAuthorizeUserLetterIdx,
@ -40,7 +41,13 @@ const handleBootSceneKeyPress = (
case "no": case "no":
return exitLoadData; return exitLoadData;
case "yes": case "yes":
return loadGame(); const stateToLoad = localStorage.getItem("lainSaveState");
if (stateToLoad)
return loadGame({
userSaveState: JSON.parse(stateToLoad),
});
else return loadGameFail;
} }
} }
} else { } else {

View file

@ -1,14 +1,19 @@
import { import { changeEndComponent, changeSite, endGame } from "../eventTemplates";
changeEndComponent,
continueGameAfterEnd,
endGame,
} from "../eventTemplates";
import { EndSceneContext, GameEvent } from "../../types/types"; import { EndSceneContext, GameEvent } from "../../types/types";
import { getCurrentUserState } from "../../store";
const handleEndSceneKeyPress = ( const handleEndSceneKeyPress = (
endSceneContext: EndSceneContext endSceneContext: EndSceneContext
): GameEvent | undefined => { ): GameEvent | undefined => {
const { keyPress, selectionVisible, activeEndComponent } = endSceneContext; const {
keyPress,
selectionVisible,
activeEndComponent,
siteSaveState,
activeNode,
activeLevel,
siteRot,
} = endSceneContext;
if (selectionVisible) { if (selectionVisible) {
switch (keyPress) { switch (keyPress) {
@ -19,9 +24,26 @@ const handleEndSceneKeyPress = (
case "CIRCLE": case "CIRCLE":
switch (activeEndComponent) { switch (activeEndComponent) {
case "end": case "end":
return endGame; return endGame({ userSaveState: getCurrentUserState() });
case "continue": 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,
});
} }
} }
} }

View file

@ -21,6 +21,7 @@ import {
knockNode, knockNode,
knockNodeAndFall, knockNodeAndFall,
loadGame, loadGame,
loadGameFail,
pauseGame, pauseGame,
ripNode, ripNode,
saveGame, saveGame,
@ -32,6 +33,7 @@ import {
throwNode, throwNode,
} from "../eventTemplates"; } from "../eventTemplates";
import { GameEvent, MainSceneContext } from "../../types/types"; import { GameEvent, MainSceneContext } from "../../types/types";
import { getCurrentUserState } from "../../store";
const handleMainSceneKeyPress = ( const handleMainSceneKeyPress = (
mainSceneContext: MainSceneContext mainSceneContext: MainSceneContext
@ -65,7 +67,7 @@ const handleMainSceneKeyPress = (
return exitPrompt; return exitPrompt;
case "yes": case "yes":
switch (activePauseComponent) { switch (activePauseComponent) {
case "change": case "change": {
const siteToLoad = activeSite === "a" ? "b" : "a"; const siteToLoad = activeSite === "a" ? "b" : "a";
const stateToLoad = siteSaveState[siteToLoad]; const stateToLoad = siteSaveState[siteToLoad];
@ -84,10 +86,18 @@ const handleMainSceneKeyPress = (
newActiveLevel: stateToLoad.activeLevel, newActiveLevel: stateToLoad.activeLevel,
newSiteSaveState: newSiteSaveState, newSiteSaveState: newSiteSaveState,
}); });
}
case "save": case "save":
return saveGame(); return saveGame({ userSaveState: getCurrentUserState() });
case "load": case "load": {
return loadGame(); const stateToLoad = localStorage.getItem("lainSaveState");
if (stateToLoad)
return loadGame({
userSaveState: JSON.parse(stateToLoad),
});
else return loadGameFail;
}
} }
} }
} }
@ -139,6 +149,13 @@ const handleMainSceneKeyPress = (
case "DOWN": { case "DOWN": {
const direction = keyPress.toLowerCase(); const direction = keyPress.toLowerCase();
const upperLimit = activeSite === "a" ? 22 : 13;
if (
(direction === "up" && level === upperLimit) ||
(direction === "down" && level === 1)
)
return;
const nodeData = findNode( const nodeData = findNode(
activeNode, activeNode,
direction, direction,
@ -198,9 +215,9 @@ const handleMainSceneKeyPress = (
} else { } else {
return eventAnimation({ currentScene: "media" }); return eventAnimation({ currentScene: "media" });
} }
case 8:
return eventAnimation({ currentScene: "gate" });
case 7: case 7:
return eventAnimation({ currentScene: "sskn" });
case 8:
return eventAnimation({ currentScene: "gate" }); return eventAnimation({ currentScene: "gate" });
case 9: case 9:
return eventAnimation({ currentScene: "polytan" }); return eventAnimation({ currentScene: "polytan" });
@ -231,11 +248,17 @@ const handleMainSceneKeyPress = (
const direction = selectedLevel > level ? "up" : "down"; const direction = selectedLevel > level ? "up" : "down";
// todo implement this row idx without mutating activenode const newStartingPoint = {
const rowIdx = direction === "up" ? 2 : 0; ...activeNode,
matrixIndices: {
matrixIdx: activeNode.matrixIndices!.matrixIdx,
rowIdx: direction === "up" ? 2 : 0,
colIdx: 0,
},
};
const nodeData = findNode( const nodeData = findNode(
activeNode, newStartingPoint,
direction, direction,
selectedLevel, selectedLevel,
activeSite, activeSite,

View file

@ -9,7 +9,11 @@ import {
wordNotFound, wordNotFound,
} from "../eventTemplates"; } from "../eventTemplates";
import { isNodeVisible } from "../../helpers/node-helpers"; import { isNodeVisible } from "../../helpers/node-helpers";
import {GameEvent, MediaSceneContext, RightMediaComponent} from "../../types/types"; import {
GameEvent,
MediaSceneContext,
RightMediaComponent,
} from "../../types/types";
const handleMediaSceneKeyPress = ( const handleMediaSceneKeyPress = (
mediaSceneContext: MediaSceneContext mediaSceneContext: MediaSceneContext
@ -47,7 +51,7 @@ const handleMediaSceneKeyPress = (
case "CIRCLE": case "CIRCLE":
switch (activeMediaComponent) { switch (activeMediaComponent) {
case "play": case "play":
return playMedia; return playMedia({ activeNode: activeNode });
case "exit": case "exit":
return exitMedia; return exitMedia;
} }

View file

@ -20,16 +20,7 @@ const handleSsknSceneKeyPress = (
case "CIRCLE": case "CIRCLE":
switch (activeSsknComponent) { switch (activeSsknComponent) {
case "ok": case "ok":
const newGameProgress = { return upgradeSskn({ activeNode: activeNode });
...gameProgress,
[activeNode.node_name]: {
is_viewed: 1,
is_visible: 0,
},
sskn_level: gameProgress.sskn_level + 1,
};
return upgradeSskn({ gameProgress: newGameProgress });
case "cancel": case "cancel":
return exitSskn; return exitSskn;
} }

View file

@ -173,7 +173,7 @@ const move = (direction: string, [matrix, level]: [number, number]) => {
}; };
export const findNode = ( export const findNode = (
activeNode: NodeData, startingPoint: NodeData,
direction: string, direction: string,
level: number, level: number,
activeSite: ActiveSite, activeSite: ActiveSite,
@ -192,11 +192,10 @@ export const findNode = (
down: [nextPos_down, ([, c]: [number, number]) => nextPos_down([-1, c])], down: [nextPos_down, ([, c]: [number, number]) => nextPos_down([-1, c])],
}; };
if (activeNode.matrixIndices) { if (startingPoint.matrixIndices) {
const nextPos = funcs[direction]; const nextPos = funcs[direction];
const nodeId = activeNode.id; let { matrixIdx, colIdx, rowIdx } = { ...startingPoint.matrixIndices };
let { matrixIdx, colIdx, rowIdx } = { ...activeNode.matrixIndices };
const initialMatrixIdx = matrixIdx; const initialMatrixIdx = matrixIdx;
@ -228,6 +227,7 @@ export const findNode = (
[matrixIdx, level] = move(direction, [matrixIdx, level]); [matrixIdx, level] = move(direction, [matrixIdx, level]);
} }
const nodeId = startingPoint.id;
if (nodeId === "") [matrixIdx] = move(direction, [initialMatrixIdx, level]); if (nodeId === "") [matrixIdx] = move(direction, [initialMatrixIdx, level]);
if (direction === "up" || direction === "down" || nodeId === "") { if (direction === "up" || direction === "down" || nodeId === "") {

View file

@ -12,10 +12,13 @@ const BootScene = () => {
const [accelaVisible, setAccelaVisible] = useState(true); const [accelaVisible, setAccelaVisible] = useState(true);
const [mainMenuVisible, setMainMenuVisible] = useState(false); const [mainMenuVisible, setMainMenuVisible] = useState(false);
const setInputCooldown = useStore((state) => state.setInputCooldown);
useEffect(() => { useEffect(() => {
setTimeout(() => setAccelaVisible(false), 2000); setTimeout(() => setAccelaVisible(false), 2000);
setTimeout(() => setMainMenuVisible(true), 6200); setTimeout(() => setMainMenuVisible(true), 6200);
}, []); setTimeout(() => setInputCooldown(0), 500);
}, [setInputCooldown]);
return ( return (
<perspectiveCamera position-z={3}> <perspectiveCamera position-z={3}>

View file

@ -12,6 +12,7 @@ const GateScene = () => {
const activeNodeName = useStore((state) => state.activeNode.node_name); const activeNodeName = useStore((state) => state.activeNode.node_name);
const setNodeViewed = useStore((state) => state.setNodeViewed); const setNodeViewed = useStore((state) => state.setNodeViewed);
const setInputCooldown = useStore((state) => state.setInputCooldown);
useEffect(() => { useEffect(() => {
incrementGateLvl(); incrementGateLvl();
@ -20,7 +21,8 @@ const GateScene = () => {
is_visible: 0, is_visible: 0,
}); });
setTimeout(() => setIntroAnim(false), 2500); setTimeout(() => setIntroAnim(false), 2500);
}, [activeNodeName, incrementGateLvl, setNodeViewed]); setTimeout(() => setInputCooldown(0), 3500);
}, [activeNodeName, incrementGateLvl, setInputCooldown, setNodeViewed]);
return ( return (
<perspectiveCamera position-z={3}> <perspectiveCamera position-z={3}>

View file

@ -50,8 +50,11 @@ const MainScene = () => {
setStarfieldIntro(false); setStarfieldIntro(false);
setLainIntroAnim(false); setLainIntroAnim(false);
setIntroFinished(false); setIntroFinished(false);
setInputCooldown(-1);
} else {
setInputCooldown(0);
} }
}, [intro]); }, [intro, setInputCooldown]);
const [starfieldIntro, setStarfieldIntro] = useState(false); const [starfieldIntro, setStarfieldIntro] = useState(false);
const [lainIntroAnim, setLainIntroAnim] = useState(false); const [lainIntroAnim, setLainIntroAnim] = useState(false);
@ -80,18 +83,6 @@ const MainScene = () => {
introWrapperRef.current.rotation.x -= 0.008; 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 ( if (
!introFinished && !introFinished &&
!( !(

View file

@ -19,6 +19,9 @@ const MediaScene = () => {
const activeNode = useStore((state) => state.activeNode); const activeNode = useStore((state) => state.activeNode);
const setScene = useStore((state) => state.setScene); const setScene = useStore((state) => state.setScene);
const incrementFinalVideoViewCount = useStore(
(state) => state.incrementFinalVideoViewCount
);
useEffect(() => { useEffect(() => {
document.getElementsByTagName("canvas")[0].className = document.getElementsByTagName("canvas")[0].className =
@ -30,9 +33,16 @@ const MediaScene = () => {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (percentageElapsed === 100 && activeNode.triggers_final_video) if (percentageElapsed === 100 && activeNode.triggers_final_video) {
setScene("end"); setScene("end");
}, [activeNode.triggers_final_video, percentageElapsed, setScene]); incrementFinalVideoViewCount();
}
}, [
activeNode.triggers_final_video,
incrementFinalVideoViewCount,
percentageElapsed,
setScene,
]);
useEffect(() => { useEffect(() => {
const mediaElement = document.getElementById("media") as HTMLMediaElement; const mediaElement = document.getElementById("media") as HTMLMediaElement;

View file

@ -1,14 +1,23 @@
import React from "react"; import React, { useEffect } from "react";
import SsknIcon from "../components/SsknScene/SsknIcon"; import SsknIcon from "../components/SsknScene/SsknIcon";
import SsknBackground from "../components/SsknScene/SsknBackground"; import SsknBackground from "../components/SsknScene/SsknBackground";
import SsknHUD from "../components/SsknScene/SsknHUD"; 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 (
<> <>
<SsknBackground /> <SsknBackground />
<SsknIcon /> <SsknIcon />
<SsknHUD /> <SsknHUD />
</> </>
); );
};
export default SsknScene; export default SsknScene;

View file

@ -20,7 +20,8 @@ import {
MediaComponent, MediaComponent,
MediaSceneContext, MediaSceneContext,
MediaSide, MediaSide,
NodeAttributes, NodeData, NodeAttributes,
NodeData,
PauseComponent, PauseComponent,
PolytanBodyParts, PolytanBodyParts,
PromptComponent, PromptComponent,
@ -28,6 +29,7 @@ import {
SiteSaveState, SiteSaveState,
SsknComponent, SsknComponent,
SsknSceneContext, SsknSceneContext,
UserSaveState,
} from "./types/types"; } from "./types/types";
type State = { type State = {
@ -137,8 +139,8 @@ export const useStore = create(
// nodes // nodes
activeNode: { activeNode: {
...site_a["04"]["0422"], ...site_a["04"]["0414"],
matrixIndices: { matrixIdx: 7, rowIdx: 0, colIdx: 0 }, matrixIndices: { matrixIdx: 7, rowIdx: 1, colIdx: 0 },
}, },
activeNodePos: [0, 0, 0], activeNodePos: [0, 0, 0],
activeNodeRot: [0, 0, 0], activeNodeRot: [0, 0, 0],
@ -233,19 +235,19 @@ export const useStore = create(
siteSaveState: { siteSaveState: {
a: { a: {
activeNode: { activeNode: {
...getNodeById("0422", "a"), ...getNodeById("0408", "a"),
matrixIndices: { matrixIdx: 7, rowIdx: 0, colIdx: 0 }, matrixIndices: { matrixIdx: 7, rowIdx: 1, colIdx: 0 },
}, },
siteRot: [0, 0, 0], siteRot: [0, 0, 0],
activeLevel: "04", activeLevel: "04",
}, },
b: { b: {
activeNode: { activeNode: {
...getNodeById("0414", "b"), ...getNodeById("0105", "b"),
matrixIndices: { matrixIdx: 7, rowIdx: 1, colIdx: 0 }, matrixIndices: { matrixIdx: 6, rowIdx: 2, colIdx: 0 },
}, },
siteRot: [0, 0, 0], siteRot: [0, 0 - Math.PI / 4, 0],
activeLevel: "04", activeLevel: "01",
}, },
}, },
@ -268,10 +270,28 @@ export const useStore = create(
nodeName: string, nodeName: string,
to: { is_viewed: number; is_visible: number } 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) => ({ set((state) => ({
gameProgress: { gameProgress: {
...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, 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, keyPress: keyPress,
activeEndComponent: state.activeEndComponent, activeEndComponent: state.activeEndComponent,
selectionVisible: state.endSceneSelectionVisible, 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) => { export const playAudio = (audio: HTMLAudioElement) => {
audio.currentTime = 0; audio.currentTime = 0;
audio.currentTime = 0; audio.currentTime = 0;

View file

@ -19,7 +19,8 @@ export type GameScene =
| "gate" | "gate"
| "boot" | "boot"
| "change_disc" | "change_disc"
| "end"; | "end"
| "null";
export type MainSubscene = "site" | "pause" | "level_selection"; export type MainSubscene = "site" | "pause" | "level_selection";
@ -143,6 +144,10 @@ export type EndSceneContext = {
keyPress: string; keyPress: string;
activeEndComponent: EndComponent; activeEndComponent: EndComponent;
selectionVisible: boolean; selectionVisible: boolean;
siteSaveState: SiteSaveState;
activeNode: NodeData;
siteRot: number[];
activeLevel: string;
}; };
export type Level = { export type Level = {
@ -188,3 +193,12 @@ export type HUDData = {
initial_position: number[]; initial_position: number[];
}; };
}; };
export type UserSaveState = {
siteSaveState: SiteSaveState;
activeNode: NodeData;
siteRot: number[];
activeLevel: string;
activeSite: ActiveSite;
gameProgress: GameProgress;
};

View file

@ -1,13 +0,0 @@
import React, { FC, ReactElement, Suspense } from "react";
import { render } from "@testing-library/react";
const SuspenseProvider: FC = ({ children }) => {
return <Suspense fallback={null}>{children}</Suspense>;
};
const customRender = (ui: ReactElement) =>
render(ui, { wrapper: SuspenseProvider });
export * from "@testing-library/react";
export { customRender as render };