mirror of
https://github.com/ad044/lainTSX.git
synced 2024-10-22 23:19:06 +00:00
separated active and inactive nodes
This commit is contained in:
parent
ef4428a809
commit
9f283912e9
8 changed files with 265 additions and 101 deletions
59
src/components/canvas/objects/MainScene/LevelNodes.tsx
Normal file
59
src/components/canvas/objects/MainScene/LevelNodes.tsx
Normal 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);
|
|
@ -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);
|
|
@ -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} />
|
||||
);
|
||||
};
|
||||
|
||||
|
|
42
src/components/canvas/objects/MainScene/Rings.tsx
Normal file
42
src/components/canvas/objects/MainScene/Rings.tsx
Normal 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);
|
|
@ -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>
|
||||
|
|
52
src/components/canvas/objects/MainScene/StaticLevelNodes.tsx
Normal file
52
src/components/canvas/objects/MainScene/StaticLevelNodes.tsx
Normal 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);
|
83
src/components/canvas/objects/MainScene/StaticNode.tsx
Normal file
83
src/components/canvas/objects/MainScene/StaticNode.tsx
Normal 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);
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue