separated active and inactive nodes

This commit is contained in:
ad044 2022-07-26 17:50:16 +04:00
parent ef4428a809
commit 9f283912e9
8 changed files with 265 additions and 101 deletions

View file

@ -0,0 +1,59 @@
import usePrevious from "@/hooks/usePrevious";
import { useStore } from "@/store";
import {
FlattenedSiteLayout,
GameSite,
MainSubscene,
NodeID,
Position,
} from "@/types";
import { getLevelY } from "@/utils/site";
import React, { memo, useEffect, useState } from "react";
import Node from "./Node";
type LevelNodesProps = {
flattenedLayout: FlattenedSiteLayout;
site: GameSite;
};
const LevelNodes = (props: LevelNodesProps) => {
const currentLevel = useStore((state) => state.level);
const paused = useStore((state) => state.mainSubscene === MainSubscene.Pause);
const currentNode = useStore((state) => state.node);
const prevData = usePrevious({ level: currentLevel });
const [nodes, setNodes] = useState<NodeID[]>(
props.flattenedLayout[currentLevel]
);
const [pos, setPos] = useState<Position>([0, getLevelY(currentLevel), 0]);
useEffect(() => {
if (prevData?.level !== currentLevel && prevData?.level !== undefined) {
if (Math.abs(prevData.level - currentLevel) === 1) {
// if only changed one level
setNodes(props.flattenedLayout[currentLevel]);
setPos([0, getLevelY(currentLevel), 0]);
} else {
// if changed from level selection
setTimeout(() => {
setNodes(props.flattenedLayout[currentLevel]);
setPos([0, getLevelY(currentLevel), 0]);
}, 1500);
}
}
}, [currentLevel, prevData?.level, props.flattenedLayout, props.site]);
return (
<group position={pos}>
{nodes.map((nodeId) => (
<Node
id={nodeId}
key={nodeId}
active={nodeId === currentNode?.id && !paused}
/>
))}
</group>
);
};
export default memo(LevelNodes);

View file

@ -1,77 +0,0 @@
import React, { createRef, memo, useEffect, useMemo } from "react";
import PurpleRing from "./PurpleRing";
import GrayRing from "./GrayRing";
import CyanCrystal from "./CyanCrystal";
import { useStore } from "@/store";
import { getLevelLimit, getLevelY } from "@/utils/site";
import { FlattenedSiteLayout, MainSubscene } from "@/types";
import Node from "./Node";
type LevelsProps = {
flattenedLayout: FlattenedSiteLayout;
activateAllLevels: boolean;
};
const Levels = (props: LevelsProps) => {
const level = useStore((state) => state.level);
const site = useStore((state) => state.site);
const node = useStore((state) => state.node);
const isPaused = useStore(
(state) => state.mainSubscene === MainSubscene.Pause
);
const refs = useMemo(
() =>
Array.from({ length: getLevelLimit(site) + 1 }, () =>
createRef<THREE.Group>()
),
[site]
);
useEffect(() => {
if (props.activateAllLevels) {
refs.forEach((ref) => {
if (ref.current) {
ref.current.visible = true;
}
});
}
}, [props.activateAllLevels, refs]);
useEffect(() => {
if (!props.activateAllLevels) {
const start = Math.max(0, level - 2);
const end = Math.min(getLevelLimit(site), level + 2);
refs.forEach((ref, idx) => {
if (ref.current && (idx <= start || idx >= end)) {
ref.current.visible = false;
}
});
}
}, [site, level, props.activateAllLevels, refs]);
return (
<>
{refs.map(
(ref, level) =>
level > 0 && (
<group position={[0, getLevelY(level), 0]} key={level} ref={ref}>
{props.flattenedLayout[level].map((nodeId) => (
<Node
key={nodeId}
id={nodeId}
active={node !== null && node.id === nodeId && !isPaused}
/>
))}
<PurpleRing level={level} site={site} />
<GrayRing />
<CyanCrystal />
</group>
)
)}
</>
);
};
export default memo(Levels);

View file

@ -11,8 +11,13 @@ import { a, useSpring } from "@react-spring/three";
import { useStore } from "@/store";
import { LainAnimation, NodeID, Position, Rotation, Scale } from "@/types";
import useNodeTexture from "@/hooks/useNodeTexture";
import { getNode, isNodeViewed, translatePositionByAngle } from "@/utils/node";
import nodePositionsJson from "@/json/node_positions.json";
import {
getNode,
getNodeWorldPosition,
getNodeWorldRotation,
isNodeViewed,
translatePositionByAngle,
} from "@/utils/node";
import { DoubleSide, UniformsLib, UniformsUtils } from "three";
import vertex from "@/shaders/node.vert";
import fragment from "@/shaders/node.frag";
@ -20,6 +25,7 @@ import NodeExplosion from "./NodeExplosion";
import NodeRip from "./NodeRip";
import sleep from "@/utils/sleep";
import { getRotationForSegment } from "@/utils/site";
import StaticNode from "./StaticNode";
type NodeProps = {
id: NodeID;
@ -40,8 +46,8 @@ const Node = (props: NodeProps) => {
[gameProgress, props.id]
);
const worldPosition = nodePositionsJson[position].position as Position;
const rotation = nodePositionsJson[position].rotation as Rotation;
const worldPosition = getNodeWorldPosition(position);
const rotation = getNodeWorldRotation(position);
const { activeTexture, viewedTexture, normalTexture } = useNodeTexture(type);
@ -294,19 +300,7 @@ const Node = (props: NodeProps) => {
</group>
</>
) : (
<a.mesh
position={worldPosition}
rotation={rotation}
scale={[0.36, 0.18, 0.36]}
renderOrder={1}
>
<planeBufferGeometry attach="geometry" />
<meshStandardMaterial
map={isViewed ? viewedTexture : normalTexture}
side={DoubleSide}
transparent={true}
/>
</a.mesh>
<StaticNode id={props.id} />
);
};

View file

@ -0,0 +1,42 @@
import React, { memo, useMemo } from "react";
import PurpleRing from "./PurpleRing";
import GrayRing from "./GrayRing";
import CyanCrystal from "./CyanCrystal";
import { useStore } from "@/store";
import { getLevelLimit, getLevelY } from "@/utils/site";
import range from "@/utils/range";
import { GameSite } from "@/types";
type RingsProps = {
activateAllRings: boolean;
site: GameSite;
};
const Rings = (props: RingsProps) => {
const level = useStore((state) => state.level);
const visibleLevels: number[] = useMemo(() => {
if (props.activateAllRings) {
return range(1, getLevelLimit(props.site) + 1);
} else {
const start = Math.max(0, level - 2);
const end = Math.min(getLevelLimit(props.site) + 1, level + 2);
return range(start, end);
}
}, [level, props.activateAllRings, props.site]);
return (
<>
{visibleLevels.map((level: number) => (
<group position={[0, getLevelY(level), 0]} key={level}>
<PurpleRing level={level} site={props.site} />
<GrayRing />
<CyanCrystal />
</group>
))}
</>
);
};
export default memo(Rings);

View file

@ -1,10 +1,12 @@
import React, { useEffect, useMemo } from "react";
import { a, useSpring } from "@react-spring/three";
import { useStore } from "@/store";
import Levels from "./Levels";
import { FlattenedSiteLayout, MainSubscene, NodeID } from "@/types";
import { getLevelY } from "@/utils/site";
import { getRotationForSegment } from "@/utils/site";
import Rings from "./Rings";
import StaticLevelNodes from "./StaticLevelNodes";
import LevelNodes from "./LevelNodes";
type SiteProps = {
introFinished: boolean;
@ -97,7 +99,7 @@ const Site = (props: SiteProps) => {
);
const siteLayout = useStore((state) => state.siteLayouts[state.site]);
const site = useStore((state) => state.site);
const layout: FlattenedSiteLayout = useMemo(() => {
return siteLayout.map((level) =>
level.flat().filter((e): e is NodeID => e !== null)
@ -108,10 +110,9 @@ const Site = (props: SiteProps) => {
<a.group rotation-x={tiltState.tilt}>
<a.group rotation-x={rotationSpring.x}>
<a.group rotation-y={rotationSpring.y} position-y={positionSpring.y}>
<Levels
flattenedLayout={layout}
activateAllLevels={props.introFinished}
/>
<StaticLevelNodes flattenedLayout={layout} site={site} />
<LevelNodes flattenedLayout={layout} site={site} />
<Rings activateAllRings={props.introFinished} site={site} />
</a.group>
</a.group>
</a.group>

View file

@ -0,0 +1,52 @@
import usePrevious from "@/hooks/usePrevious";
import { useStore } from "@/store";
import { FlattenedSiteLayout, GameSite } from "@/types";
import range from "@/utils/range";
import { getLevelLimit, getLevelY } from "@/utils/site";
import React, { memo, useEffect, useState } from "react";
import StaticNode from "./StaticNode";
type StaticLevelNodesProps = {
flattenedLayout: FlattenedSiteLayout;
site: GameSite;
};
const StaticLevelNodes = (props: StaticLevelNodesProps) => {
const currentLevel = useStore((state) => state.level);
const prevData = usePrevious({ level: currentLevel });
const [visibleLevels, setVisibleLevels] = useState<number[]>(
range(
Math.max(currentLevel - 3, 0),
Math.min(currentLevel + 3, getLevelLimit(props.site))
)
);
useEffect(() => {
if (prevData?.level !== currentLevel && prevData?.level !== undefined) {
const start = Math.max(currentLevel - 3, 1);
const end = Math.min(currentLevel + 3, getLevelLimit(props.site));
if (Math.abs(prevData.level - currentLevel) === 1) {
// if only changed one level
setVisibleLevels(range(start, end));
} else {
// if changed from level selection
setTimeout(() => setVisibleLevels(range(start, end)), 1500);
}
}
}, [currentLevel, prevData?.level, props.site]);
return (
<>
{visibleLevels.map((level) => (
<group position={[0, getLevelY(level), 0]} key={level}>
{props.flattenedLayout[level].map((nodeId) => (
<StaticNode id={nodeId} key={nodeId} />
))}
</group>
))}
</>
);
};
export default memo(StaticLevelNodes);

View file

@ -0,0 +1,83 @@
import useNodeTexture from "@/hooks/useNodeTexture";
import { useStore } from "@/store";
import { LainAnimation, NodeID } from "@/types";
import {
getNode,
getNodeWorldPosition,
getNodeWorldRotation,
isNodeViewed,
} from "@/utils/node";
import { memo, useEffect, useMemo, useRef } from "react";
import { DoubleSide } from "three";
type StaticNodeProps = {
id: NodeID;
};
const StaticNode = (props: StaticNodeProps) => {
const currentNode = useStore((state) => state.node);
const node = useMemo(() => getNode(props.id), [props.id]);
const ref = useRef<THREE.Mesh>(null);
useEffect(
() =>
useStore.subscribe(
(state) => state.lainAnimation,
(lainAnimation) => {
switch (lainAnimation) {
case LainAnimation.ThrowNode:
case LainAnimation.RipNode:
case LainAnimation.TouchNodeAndGetScared:
case LainAnimation.KnockAndFall:
case LainAnimation.Knock:
if (
ref.current &&
props.id === currentNode?.id &&
ref.current.visible
) {
ref.current.visible = false;
}
break;
default:
if (ref.current && !ref.current.visible) {
ref.current.visible = true;
}
break;
}
}
),
[currentNode?.id, props.id]
);
const { position, type } = node;
const worldPosition = getNodeWorldPosition(position);
const rotation = getNodeWorldRotation(position);
const gameProgress = useStore((state) => state.gameProgress);
const { viewedTexture, normalTexture } = useNodeTexture(type);
const isViewed = useMemo(
() => isNodeViewed(props.id, gameProgress),
[gameProgress, props.id]
);
return (
<mesh
ref={ref}
position={worldPosition}
rotation={rotation}
scale={[0.36, 0.18, 0.36]}
renderOrder={1}
>
<planeBufferGeometry attach="geometry" />
<meshStandardMaterial
map={isViewed ? viewedTexture : normalTexture}
side={DoubleSide}
transparent={true}
/>
</mesh>
);
};
export default memo(StaticNode);

View file

@ -11,10 +11,12 @@ import {
NodeRow,
NodeID,
Position,
Rotation,
} from "@/types";
import { State } from "@/types";
import nodeHudsJson from "@/json/node_huds.json";
import { getLayout, getLevelLimit } from "./site";
import nodeHudsJson from "@/json/node_huds.json";
import nodePositionsJson from "@/json/node_positions.json";
export const getNode = (id: NodeID): NodeData => {
return nodesJson[id] as NodeData;
@ -411,3 +413,11 @@ export const translatePositionByAngle = (
export const isAudioOnly = (media: string) => {
return media.includes("XA");
};
export const getNodeWorldPosition = (position: number) => {
return nodePositionsJson[position].position as Position;
};
export const getNodeWorldRotation = (position: number) => {
return nodePositionsJson[position].rotation as Rotation;
};