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" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<style>
body {
background-color: #000;
}
</style>
</head>
<body>
<noscript
>do you actually believe i could rewrite this in plain html css or
smoetihng</noscript
>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r119/three.min.js"></script>
<div id="root"></div>
</body>
</html>

View file

@ -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: <TaKScene />,
change_disc: <ChangeDiscScene />,
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,
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;

View file

@ -21,14 +21,26 @@ const Loading = memo(() => {
return (
<>
<sprite scale={[5, 5, 5]}>
<spriteMaterial attach="material" color={0x000000} />
<sprite scale={[5, 5, 5]} renderOrder={999}>
<spriteMaterial attach="material" color={0x000000} depthTest={false} />
</sprite>
<sprite scale={[0.35, 0.6, 0.35]} position={[0, 0.2, 0]}>
<spriteMaterial attach="material" map={loadingTex} />
<sprite
scale={[0.35, 0.6, 0.35]}
position={[0, 0.2, 0]}
renderOrder={1000}
>
<spriteMaterial attach="material" map={loadingTex} depthTest={false} />
</sprite>
<sprite scale={[0.4, 0.6, 0.4]} position={[0, -0.5, 0]}>
<spriteMaterial attach="material" map={lifeInstinctTex} />
<sprite
scale={[0.4, 0.6, 0.4]}
position={[0, -0.5, 0]}
renderOrder={1000}
>
<spriteMaterial
attach="material"
map={lifeInstinctTex}
depthTest={false}
/>
</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 verticalHud from "../../static/sprite/select_level_hud_vertical.png";
import horizontalHud from "../../static/sprite/select_level_hud_horizontal.png";

View file

@ -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 (
<Suspense fallback={<Loading />}>
<Suspense fallback={null}>
<a.group rotation-x={rotXState.x}>
<a.group rotation-y={rotYState.y} position-y={posState.y}>
<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 * 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}
/>
</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 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 (
<>
<group position={[0, -1, 2]} visible={props.mainVisible}>
@ -101,20 +96,18 @@ const Starfield = memo((props: StarfieldProps) => {
))}
</group>
</group>
{introVisible && props.shouldIntro ? (
{props.shouldIntro && (
<group position={[-2, -15, -30]} rotation={[Math.PI / 3, 0, 0]}>
{posesBlueFromBottom.map((poses, idx) => (
<Star position={poses} color={"blue"} key={idx} introStar={true} />
<IntroStar position={poses} color={"blue"} key={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) => (
<Star position={poses} color={"cyan"} key={idx} introStar={true} />
<IntroStar position={poses} color={"cyan"} key={idx} />
))}
</group>
) : (
<></>
)}
</>
);

View file

@ -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 (

View file

@ -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";

View file

@ -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 {

View file

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

View file

@ -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,

View file

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

View file

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

View file

@ -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 === "") {

View file

@ -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 (
<perspectiveCamera position-z={3}>

View file

@ -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 (
<perspectiveCamera position-z={3}>

View file

@ -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 &&
!(

View file

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

View file

@ -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 = () => (
<>
<SsknBackground />
<SsknIcon />
<SsknHUD />
</>
);
const SsknScene = () => {
const setInputCooldown = useStore((state) => state.setInputCooldown);
useEffect(() => {
setTimeout(() => setInputCooldown(0), 500);
}, [setInputCooldown]);
return (
<>
<SsknBackground />
<SsknIcon />
<SsknHUD />
</>
);
};
export default SsknScene;

View file

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

View file

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

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