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 { useStore } from "@/store";
|
||||||
import { LainAnimation, NodeID, Position, Rotation, Scale } from "@/types";
|
import { LainAnimation, NodeID, Position, Rotation, Scale } from "@/types";
|
||||||
import useNodeTexture from "@/hooks/useNodeTexture";
|
import useNodeTexture from "@/hooks/useNodeTexture";
|
||||||
import { getNode, isNodeViewed, translatePositionByAngle } from "@/utils/node";
|
import {
|
||||||
import nodePositionsJson from "@/json/node_positions.json";
|
getNode,
|
||||||
|
getNodeWorldPosition,
|
||||||
|
getNodeWorldRotation,
|
||||||
|
isNodeViewed,
|
||||||
|
translatePositionByAngle,
|
||||||
|
} from "@/utils/node";
|
||||||
import { DoubleSide, UniformsLib, UniformsUtils } from "three";
|
import { DoubleSide, UniformsLib, UniformsUtils } from "three";
|
||||||
import vertex from "@/shaders/node.vert";
|
import vertex from "@/shaders/node.vert";
|
||||||
import fragment from "@/shaders/node.frag";
|
import fragment from "@/shaders/node.frag";
|
||||||
|
@ -20,6 +25,7 @@ import NodeExplosion from "./NodeExplosion";
|
||||||
import NodeRip from "./NodeRip";
|
import NodeRip from "./NodeRip";
|
||||||
import sleep from "@/utils/sleep";
|
import sleep from "@/utils/sleep";
|
||||||
import { getRotationForSegment } from "@/utils/site";
|
import { getRotationForSegment } from "@/utils/site";
|
||||||
|
import StaticNode from "./StaticNode";
|
||||||
|
|
||||||
type NodeProps = {
|
type NodeProps = {
|
||||||
id: NodeID;
|
id: NodeID;
|
||||||
|
@ -40,8 +46,8 @@ const Node = (props: NodeProps) => {
|
||||||
[gameProgress, props.id]
|
[gameProgress, props.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
const worldPosition = nodePositionsJson[position].position as Position;
|
const worldPosition = getNodeWorldPosition(position);
|
||||||
const rotation = nodePositionsJson[position].rotation as Rotation;
|
const rotation = getNodeWorldRotation(position);
|
||||||
|
|
||||||
const { activeTexture, viewedTexture, normalTexture } = useNodeTexture(type);
|
const { activeTexture, viewedTexture, normalTexture } = useNodeTexture(type);
|
||||||
|
|
||||||
|
@ -294,19 +300,7 @@ const Node = (props: NodeProps) => {
|
||||||
</group>
|
</group>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<a.mesh
|
<StaticNode id={props.id} />
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
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 React, { useEffect, useMemo } from "react";
|
||||||
import { a, useSpring } from "@react-spring/three";
|
import { a, useSpring } from "@react-spring/three";
|
||||||
import { useStore } from "@/store";
|
import { useStore } from "@/store";
|
||||||
import Levels from "./Levels";
|
|
||||||
import { FlattenedSiteLayout, MainSubscene, NodeID } from "@/types";
|
import { FlattenedSiteLayout, MainSubscene, NodeID } from "@/types";
|
||||||
import { getLevelY } from "@/utils/site";
|
import { getLevelY } from "@/utils/site";
|
||||||
import { getRotationForSegment } from "@/utils/site";
|
import { getRotationForSegment } from "@/utils/site";
|
||||||
|
import Rings from "./Rings";
|
||||||
|
import StaticLevelNodes from "./StaticLevelNodes";
|
||||||
|
import LevelNodes from "./LevelNodes";
|
||||||
|
|
||||||
type SiteProps = {
|
type SiteProps = {
|
||||||
introFinished: boolean;
|
introFinished: boolean;
|
||||||
|
@ -97,7 +99,7 @@ const Site = (props: SiteProps) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const siteLayout = useStore((state) => state.siteLayouts[state.site]);
|
const siteLayout = useStore((state) => state.siteLayouts[state.site]);
|
||||||
|
const site = useStore((state) => state.site);
|
||||||
const layout: FlattenedSiteLayout = useMemo(() => {
|
const layout: FlattenedSiteLayout = useMemo(() => {
|
||||||
return siteLayout.map((level) =>
|
return siteLayout.map((level) =>
|
||||||
level.flat().filter((e): e is NodeID => e !== null)
|
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={tiltState.tilt}>
|
||||||
<a.group rotation-x={rotationSpring.x}>
|
<a.group rotation-x={rotationSpring.x}>
|
||||||
<a.group rotation-y={rotationSpring.y} position-y={positionSpring.y}>
|
<a.group rotation-y={rotationSpring.y} position-y={positionSpring.y}>
|
||||||
<Levels
|
<StaticLevelNodes flattenedLayout={layout} site={site} />
|
||||||
flattenedLayout={layout}
|
<LevelNodes flattenedLayout={layout} site={site} />
|
||||||
activateAllLevels={props.introFinished}
|
<Rings activateAllRings={props.introFinished} site={site} />
|
||||||
/>
|
|
||||||
</a.group>
|
</a.group>
|
||||||
</a.group>
|
</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,
|
NodeRow,
|
||||||
NodeID,
|
NodeID,
|
||||||
Position,
|
Position,
|
||||||
|
Rotation,
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
import { State } from "@/types";
|
import { State } from "@/types";
|
||||||
import nodeHudsJson from "@/json/node_huds.json";
|
|
||||||
import { getLayout, getLevelLimit } from "./site";
|
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 => {
|
export const getNode = (id: NodeID): NodeData => {
|
||||||
return nodesJson[id] as NodeData;
|
return nodesJson[id] as NodeData;
|
||||||
|
@ -411,3 +413,11 @@ export const translatePositionByAngle = (
|
||||||
export const isAudioOnly = (media: string) => {
|
export const isAudioOnly = (media: string) => {
|
||||||
return media.includes("XA");
|
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