Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/ideas/creatures/Goose/idea.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"id": "f3c1ae49-de5e-4702-a80b-b3f5124c84a9",
"name": "Goose",
"predecessor": "beed4009-baa2-4ffa-aec4-225cd36e1fbf",
"purpose": "exist. create Goose. give the goose some id, beliefs, and bug fixes. init pos fixes.",
"schema": [
{
"name": "position",
"type": "position"
},
{
"name": "name",
"type": "string"
},
{
"name": "alwaysFollow",
"type": "boolean"
}
],
"npm_dependencies": {},
"data_url": null
}
54 changes: 54 additions & 0 deletions src/ideas/creatures/Goose/ideas/FollowBone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Bone, Group, Quaternion, Vector3 } from "three";
import { ReactNode, useMemo, useRef } from "react";
import { useLimitedFrame } from "spacesvr";
import { GroupProps } from "@react-three/fiber";

type FollowBoneProps = {
bone: Bone;
children: ReactNode | ReactNode[];
} & GroupProps;

/**
* Given a bone, this modifier will make its children's transform follow the bone
* @param props
* @constructor
*/
export default function FollowBone(props: FollowBoneProps) {
const { bone, children, ...rest } = props;

const refGroup = useRef<Group>(null);
const group = useRef<Group>(null);

const parent_pos = useMemo(() => new Vector3(), []);
const parent_quat = useMemo(() => new Quaternion(), []);

const bone_pos = useMemo(() => new Vector3(), []);
const bone_quat = useMemo(() => new Quaternion(), []);

useLimitedFrame(40, () => {
if (!group.current || !refGroup.current || !bone) return;
refGroup.current.getWorldPosition(parent_pos);
refGroup.current.getWorldQuaternion(parent_quat);
bone.getWorldPosition(bone_pos);
bone.getWorldQuaternion(bone_quat);

// get diff between bone pos and parent pos
bone_pos.sub(parent_pos);

// apply the parent's roation since it will be applied to the group .. or something idk, but this is v important!!
bone_pos.applyQuaternion(parent_quat.invert());

group.current.position.copy(bone_pos);
group.current.quaternion.copy(parent_quat.multiply(bone_quat));
});

return (
<group ref={refGroup}>
<group {...rest}>
<group ref={group} name="follow-bone">
{children}
</group>
</group>
</group>
);
}
46 changes: 46 additions & 0 deletions src/ideas/creatures/Goose/ideas/GooseAudio.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { PositionalAudio } from "@react-three/drei";
import { PositionalAudio as PositionalAudioImpl } from "three";
import { useRef } from "react";
import { useLimitedFrame } from "spacesvr";

const CONTENT_LIBRARY = "https://d27rt3a60hh1lx.cloudfront.net/content/goose";
const FILE_1 = `${CONTENT_LIBRARY}/footstep_carpet_003.mp3`;
const FILE_2 = `${CONTENT_LIBRARY}/footstep_carpet_004.mp3`;

type GooseAudioProps = {
walking?: boolean;
};

export default function GooseAudio(props: GooseAudioProps) {
const { walking = false } = props;

const audio1 = useRef<PositionalAudioImpl>();
const audio2 = useRef<PositionalAudioImpl>();

useLimitedFrame(3.5, () => {
if (Math.random() > 0.9 || !audio1.current || !audio2.current || !walking) {
return;
}

const audio = Math.random() > 0.5 ? audio1.current : audio2.current;
audio.setVolume(Math.random() * 0.1 + 0.05);
audio.play();
});

return (
<group name="goose-audio">
<PositionalAudio
ref={audio1}
url={FILE_1}
loop={false}
autoplay={false}
/>
<PositionalAudio
ref={audio2}
url={FILE_2}
loop={false}
autoplay={false}
/>
</group>
);
}
39 changes: 39 additions & 0 deletions src/ideas/creatures/Goose/ideas/Nametag/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { GroupProps } from "@react-three/fiber";
import { Image } from "spacesvr";
import { Text } from "@react-three/drei";

type NametagProps = {
name: string;
} & GroupProps;

const FONT_FILE =
"https://d27rt3a60hh1lx.cloudfront.net/fonts/GrapeNuts-Regular.ttf";

export default function Nametag(props: NametagProps) {
const { name, ...rest } = props;

return (
<group {...rest} name={`nametag-${name}`}>
<mesh name="pin">
<sphereBufferGeometry args={[0.0075, 10, 10]} />
<meshBasicMaterial color="black" />
</mesh>
<group position-y={-0.0525}>
<Image
src="https://d1htv66kutdwsl.cloudfront.net/00e31fc1-f8c5-4710-bed9-b494e5401587/295a087b-62fc-4570-a0a5-a3fb0e9a43d3.ktx2"
size={0.15}
/>
<Text
font={FONT_FILE}
color="black"
position-z={0.01}
fontSize={0.035}
rotation-z={0.09}
position-y={-0.005}
>
{name}
</Text>
</group>
</group>
);
}
50 changes: 50 additions & 0 deletions src/ideas/creatures/Goose/ideas/Pathfinding/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Vector3 } from "three";
import { useFrame } from "@react-three/fiber";
import { useMind } from "../../layers/Mind";
import { useBody } from "../../layers/Body";
import { useLimitedFrame, usePlayer } from "spacesvr";
import { useRef } from "react";

const RELAX = 1.2;

export default function Pathfinding() {
const mind = useMind();
const { pos, setDir } = useBody();
const player = usePlayer();

const nextMoveTime = useRef(0);

useFrame(({ camera }) => {
// move towards target
const dist = pos.distanceTo(mind.target);
if (dist < RELAX) setDir(new Vector3());
else setDir(mind.target.clone().sub(pos));
});

const lastState = useRef(mind.state);
useFrame(() => {
if (lastState.current !== mind.state) {
lastState.current = mind.state;
nextMoveTime.current = 0;
}
});

useLimitedFrame(20, ({ clock, camera }) => {
if (mind.state === "wander" || mind.state === "idle") {
if (clock.getElapsedTime() > nextMoveTime.current) {
const nextDelta = 12 + Math.pow(Math.random(), 0.15) * 30;
nextMoveTime.current = clock.getElapsedTime() + nextDelta;
mind.target.x = (Math.random() * 2 - 1) * 8;
mind.target.z = (Math.random() * 2 - 1) * 8;
}
} else if (mind.state === "follow" || mind.state === "attack") {
if (clock.getElapsedTime() > nextMoveTime.current) {
nextMoveTime.current = clock.getElapsedTime() + 0.3;
mind.target.x = camera.position.x;
mind.target.z = camera.position.z;
}
}
});

return null;
}
10 changes: 10 additions & 0 deletions src/ideas/creatures/Goose/ideas/Pathfinding/logic/move.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Quaternion, Spherical, Vector3 } from "three";

const dummy = new Spherical();
const UP_NORMAL = new Vector3(0, 1, 0);

export const setYRot = (rot: Quaternion, angle: number) =>
rot.setFromAxisAngle(UP_NORMAL, angle);

export const setYRotFromXZ = (rot: Quaternion, x: number, z: number) =>
setYRot(rot, dummy.setFromCartesianCoords(x, 0, z).theta);
57 changes: 57 additions & 0 deletions src/ideas/creatures/Goose/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Suspense, useEffect, useMemo, useRef } from "react";
import GooseModel from "./models/Goose";
import { GroupProps } from "@react-three/fiber";
import Mind from "./layers/Mind";
import Body, { BodyConsumer } from "./layers/Body";
import Pathfinding from "./ideas/Pathfinding";
import { GooseMind } from "./logic/goose";
import GooseAudio from "./ideas/GooseAudio";
import { Group } from "three";
import { useLimitedFrame } from "spacesvr";

type GooseProps = {
name?: string;
alwaysFollow?: boolean;
} & GroupProps;

export default function Goose(props: GooseProps) {
const { name, alwaysFollow, rotation, scale, position, ...rest } = props;

const ref = useRef<Group>(null);

const mind = useMemo(() => new GooseMind(), []);

useEffect(() => {
mind.updateBeliefs({ alwaysFollow });
}, [alwaysFollow]);

useLimitedFrame(4, () => {
if (!ref.current) return;
ref.current.position.set(0, 0, 0);
});

return (
<group
ref={ref}
name={`goose-${name}`}
{...rest}
rotation={[0, 0, 0]}
position={[0, 0, 0]}
scale={1}
>
<Mind mind={mind}>
<Body height={0.5} radius={0.2} speed={1} initPos={position}>
<Pathfinding />
<BodyConsumer>
{(bodyState) => (
<Suspense fallback={null}>
<GooseAudio walking={bodyState.moving} />
<GooseModel walking={bodyState.moving} name={name} />
</Suspense>
)}
</BodyConsumer>
</Body>
</Mind>
</group>
);
}
112 changes: 112 additions & 0 deletions src/ideas/creatures/Goose/layers/Body/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import {
createContext,
ReactNode,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Group, Quaternion, Vector3 } from "three";
import { useCapsuleCollider } from "./logic/collider";
import { setYRotFromXZ } from "../../ideas/Pathfinding/logic/move";
import { useLimitedFrame } from "spacesvr";
import { GroupProps } from "@react-three/fiber";
import { getVecPos, useInitialPosition } from "./logic/initPos";

type BodyState = {
pos: Vector3;
setDir: (dir: Vector3) => void;
moving: boolean;
};
export const BodyContext = createContext({} as BodyState);
export const BodyConsumer = BodyContext.Consumer;
export const useBody = () => useContext(BodyContext);

type PickRename<T, K extends keyof T, R extends PropertyKey> = Omit<T, K> &
{ [P in R]: T[K] };

type BodyProps = {
children: ReactNode | ReactNode[];
speed?: number;
height?: number;
radius?: number;
} & PickRename<GroupProps, "position", "initPos">;

export default function Body(props: BodyProps) {
const { speed = 1, height = 0.9, radius = 0.2, children, initPos } = props;

const SPEED = speed * 1.2;
const group = useRef<Group>(null);
const [moving, setMoving] = useState(false);

// readonly values ================================================================
const pos = useMemo(
() => getVecPos(initPos).add(new Vector3(0, height, 0)),
[]
);
const vel = useMemo(() => new Vector3(), []);
const [, bodyApi] = useCapsuleCollider(pos.toArray(), height, radius);
useEffect(() => {
bodyApi.position.subscribe((p) => pos.fromArray(p));
bodyApi.velocity.subscribe((v) => vel.fromArray(v));
}, []);

useInitialPosition(initPos, bodyApi, height);

// mutable values ================================================================
const targetRot = useMemo(() => new Quaternion(), []);
const dir = useMemo(() => new Vector3(), []);

useLimitedFrame(70, ({ camera }) => {
if (!group.current) return;

// helpers ================================================================
const faceTarget = () => setYRotFromXZ(targetRot, dir.x, dir.z);
const lookAtPlayer = () =>
setYRotFromXZ(
targetRot,
camera.position.x - pos.x,
camera.position.z - pos.z
);

// updates ================================================================
// update pos value
group.current.position.copy(pos);
group.current.position.y -= height;

// slerp towards target rot
group.current.quaternion.slerp(targetRot, 0.17);

// logic ================================================================
// move towards target
if (dir.length() <= 0.1) {
setMoving(false);
bodyApi.velocity.set(0, 0, 0);
} else {
setMoving(true);
faceTarget();
bodyApi.velocity.set(dir.x * SPEED, vel.y, dir.z * SPEED);
}
});

const setDir = (d: Vector3) => {
if (d.length() < 0.5) {
dir.x = 0;
dir.y = 0;
dir.z = 0;
} else {
dir.copy(d).normalize();
}
};

const value = { pos, setDir, moving };

return (
<BodyContext.Provider value={value}>
<group name="body" ref={group}>
{children}
</group>
</BodyContext.Provider>
);
}
Loading