mirror of
https://github.com/ad044/lainTSX.git
synced 2024-10-22 15:09:05 +00:00
progress saving implemented, bug fixes and improvements
This commit is contained in:
parent
d92e259bcc
commit
0956ae9fba
27 changed files with 455 additions and 199 deletions
|
@ -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>
|
||||
|
|
|
@ -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: <></>,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
|
9
src/__tests__/save-state.test.ts
Normal file
9
src/__tests__/save-state.test.ts
Normal 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);
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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} />
|
||||
|
|
79
src/components/MainScene/Starfield/IntroStar.tsx
Normal file
79
src/components/MainScene/Starfield/IntroStar.tsx
Normal 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;
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 === "") {
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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 &&
|
||||
!(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
111
src/store.ts
111
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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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 };
|
Loading…
Reference in a new issue