diff --git a/src/components/GameObjectView.tsx b/src/components/GameObjectView.tsx new file mode 100644 index 0000000..42df29b --- /dev/null +++ b/src/components/GameObjectView.tsx @@ -0,0 +1,17 @@ +import { observer } from "mobx-react-lite"; +import type { ObjectInstance, Runtime } from "../types"; +import { state } from "../state"; +import { ObjectViewInternal } from "./ObjectViewInternal"; + +export type GameObjectViewProps = { + object: Runtime; +} + +export const GameObjectView = observer(function (props: GameObjectViewProps) { + + const objectType = state.world.getObjectTypeById(props.object.typeId); + if (!objectType) + return null; + + return +}); diff --git a/src/components/ObjectEditorView.tsx b/src/components/ObjectEditorView.tsx new file mode 100644 index 0000000..af8dae3 --- /dev/null +++ b/src/components/ObjectEditorView.tsx @@ -0,0 +1,62 @@ +import { observer } from "mobx-react-lite"; +import type { ObjectInstance, R3, Runtime } from "../types"; +import { useRef, type RefObject } from "react"; +import type { Group } from "three"; +import { TransformControls, useHelper } from "@react-three/drei"; +import { BoxHelper } from "three"; +import type { ThreeEvent } from "@react-three/fiber"; +import { state } from "../state"; +import { nextSelectionEditMode } from "../state/worldEditorState"; +import { ObjectViewInternal } from "./ObjectViewInternal"; + +type ObjectEditorViewProps = { + object: Runtime; +} + +export const ObjectEditorView = observer(function ({ object }: ObjectEditorViewProps) { + const groupRef = useRef(null); + + const objectType = state.world.getObjectTypeById(object.typeId); + + const isSelected = state.worldEditor.isEnabled && + state.worldEditor.selectedObjectId === object.id; + const selectionMode = isSelected ? state.worldEditor.selectedObjectMode : undefined; + + useHelper(isSelected ? groupRef : { current: null } as any, BoxHelper, 'white'); + + if (!objectType) + return null; + + function handleClick(e: ThreeEvent) { + if (e.delta > 5) return; + e.stopPropagation(); + state.worldEditor.setSelectedObject(object.id, nextSelectionEditMode(selectionMode)); + } + + function handleTransformEnd() { + const group = groupRef.current; + if (group) + state.worldEditor.setObjectTransform( + object.id, + group.position.toArray(), + group.rotation.toArray().slice(0, 3) as R3, + group.scale.toArray(), + ); + } + + return (<> + {(isSelected && selectionMode !== undefined && groupRef.current) && + } + mode={selectionMode} + onMouseUp={handleTransformEnd} + /> + } + + ); +}); diff --git a/src/components/ObjectView.tsx b/src/components/ObjectView.tsx deleted file mode 100644 index cd042a0..0000000 --- a/src/components/ObjectView.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { observer } from "mobx-react-lite"; -import type { ObjectType, ObjectInstance } from "../types"; -import { useRef, useMemo, type RefObject } from "react"; -import type { Group } from "three"; -import { Instance, Instances, TransformControls, useHelper } from "@react-three/drei"; -import { BoxHelper } from "three"; -import type { ThreeEvent } from "@react-three/fiber"; -import { state } from "../state"; -import { nextSelectionEditMode } from "../state/worldEditorState"; -import type { R3 } from "../types/3d"; -import { TrimeshCollider } from "@react-three/rapier"; -import { SyncRigidBody } from "./SyncRigidBody"; -import { runInAction } from "mobx"; -import { getObjectVoxelGroups } from "../utils/graphics/voxelGroup"; - -type ObjectViewProps = { - object: ObjectInstance; -} - -function buildTrimesh(objectType: ObjectType, voxelTypes: typeof state.world.data.voxelTypes): [Float32Array, Uint32Array] | null { - const collidable = objectType.voxels.filter( - v => voxelTypes[v.typeId]?.collidable !== false - ); - if (!collidable.length) return null; - - const n = collidable.length; - const verts = new Float32Array(n * 8 * 3); - const idxs = new Uint32Array(n * 36); - - for (let i = 0; i < n; i++) { - const p = collidable[i].position; - const vb = i * 8; - - // 8 corners of unit box at position p (no +0.5 — group transform handles offset) - verts.set([ - p[0], p[1], p[2], - p[0] + 1, p[1], p[2], - p[0], p[1] + 1, p[2], - p[0] + 1, p[1] + 1, p[2], - p[0], p[1], p[2] + 1, - p[0] + 1, p[1], p[2] + 1, - p[0], p[1] + 1, p[2] + 1, - p[0] + 1, p[1] + 1, p[2] + 1, - ], vb * 3); - - // 12 triangles (CCW outward normals) - idxs.set([ - vb, vb + 2, vb + 3, vb, vb + 3, vb + 1, // -Z - vb + 4, vb + 5, vb + 7, vb + 4, vb + 7, vb + 6, // +Z - vb, vb + 1, vb + 5, vb, vb + 5, vb + 4, // -Y - vb + 2, vb + 6, vb + 7, vb + 2, vb + 7, vb + 3, // +Y - vb, vb + 4, vb + 6, vb, vb + 6, vb + 2, // -X - vb + 1, vb + 3, vb + 7, vb + 1, vb + 7, vb + 5, // +X - ], i * 36); - } - - return [verts, idxs]; -} - -export const ObjectView = observer(function ({ object }: ObjectViewProps) { - - const objectType = state.world.getObjectTypeById(object.typeId); - - const groupRef = useRef(null); - - const isSelected = state.worldEditor.isEnabled && - state.worldEditor.selectedObjectId === object.id; - - useHelper(isSelected ? groupRef : { current: null } as any, BoxHelper, 'white'); - - // Must be before early return to satisfy hooks rules - const trimeshArgs = useMemo( - () => objectType - ? buildTrimesh(objectType, state.world.data.voxelTypes) - : null, - // eslint-disable-next-line react-hooks/exhaustive-deps - [objectType?.id] - ); - - if (!objectType) - return null; - - const selectionMode = isSelected - ? state.worldEditor.selectedObjectMode - : undefined; - - function handleClick(e: ThreeEvent) { - if (!state.worldEditor.isEnabled) - return; - if (e.delta > 5) - return; - e.stopPropagation(); - state.worldEditor.setSelectedObject(object.id, nextSelectionEditMode(selectionMode)); - }; - - function handleTransformEnd() { - const group = groupRef.current; - if (group) - state.worldEditor.setObjectTransform( - object.id, - group.position.toArray(), - group.rotation.toArray().slice(0, 3) as R3, - group.scale.toArray(), - ); - }; - - return (<> - { - (isSelected && (selectionMode !== undefined) && groupRef.current) && - } - mode={selectionMode} - onMouseUp={handleTransformEnd} - /> - } - { - // console.log(`changed object ${object.id} ${JSON.stringify(data)}`); - runInAction(() => { - const obj = state.game?.scene.objects[object.id]; - if (obj) { - obj.position = data.position; - obj.rotation = data.rotation; - obj.linearVelocity = data.linearVelocity; - obj.radialVelocity = data.radialVelocity; - } - }); - }} - > - - {getObjectVoxelGroups(objectType, state.world.data.voxelTypes).map((vg) => - - - - {vg.positions.map((pos, i) => )} - - )} - {trimeshArgs && } - - - ); -}); diff --git a/src/components/ObjectViewInternal.tsx b/src/components/ObjectViewInternal.tsx new file mode 100644 index 0000000..1b44d8c --- /dev/null +++ b/src/components/ObjectViewInternal.tsx @@ -0,0 +1,65 @@ +import { observer } from "mobx-react-lite"; +import type { ObjectType, RuntimeObjectInstance } from "../types"; +import { forwardRef, useMemo } from "react"; +import type { Group } from "three"; +import { Instance, Instances } from "@react-three/drei"; +import type { ThreeEvent } from "@react-three/fiber"; +import { state } from "../state"; +import { TrimeshCollider } from "@react-three/rapier"; +import { SyncRigidBody } from "./SyncRigidBody"; +import { runInAction } from "mobx"; +import { buildObjectTrimesh } from "../utils/graphics/mesh"; + +type ObjectViewInternalProps = { + object: Omit; + objectType: ObjectType; + onClick?: (e: ThreeEvent) => void; +} + +export const ObjectViewInternal = observer(forwardRef( + function ({ object, objectType, onClick }, ref) { + const trimeshArgs = useMemo( + () => buildObjectTrimesh(objectType, state.world.data.voxelTypes), + // eslint-disable-next-line react-hooks/exhaustive-deps + [objectType.id] + ); + + return ( + { + runInAction(() => { + const obj = state.game?.scene.objects[object.id]; + if (obj) { + obj.position = data.position; + obj.rotation = data.rotation; + obj.linearVelocity = data.linearVelocity; + obj.radialVelocity = data.radialVelocity; + } + }); + }} + > + + { + object.cache.voxelGroups.map((vg) => + + + + {vg.positions.map((pos, i) => )} + + ) + } + {trimeshArgs && } + + + ); + } +)); diff --git a/src/components/SceneView.tsx b/src/components/SceneView.tsx index 9b696f7..8ad814a 100644 --- a/src/components/SceneView.tsx +++ b/src/components/SceneView.tsx @@ -1,13 +1,13 @@ import { observer } from "mobx-react-lite"; -import type { Scene } from "../types"; +import type { RuntimeScene } from "../types"; import { CharacterView } from "./CharacterView"; -import { ObjectView } from "./ObjectView"; +import { GameObjectView } from "./GameObjectView"; import { useFrame } from "@react-three/fiber"; import { useRapier } from "@react-three/rapier"; -import { state } from "../state"; +import { ObjectEditorView } from "./ObjectEditorView"; type SceneViewProps = { - scene: Scene, + scene: RuntimeScene, editMode?: boolean; } @@ -28,8 +28,13 @@ export const SceneView = observer(function (props: SceneViewProps) { return (<> - {Object.values(props.scene.objects).map((obj) => - )} + { + Object.values(props.scene.objects).map((obj) => ( + props.editMode + ? + : + )) + } {/* {props.editMode && } */} {} ); diff --git a/src/components/SyncRigidBody.tsx b/src/components/SyncRigidBody.tsx index ade0e54..0d5c48f 100644 --- a/src/components/SyncRigidBody.tsx +++ b/src/components/SyncRigidBody.tsx @@ -2,7 +2,7 @@ import { RigidBody, type RigidBodyProps, type RapierRigidBody } from "@react-thr import { useFrame } from "@react-three/fiber"; import { useRef } from "react"; import { Euler, Quaternion } from "three"; -import type { R3, V3 } from "../types/3d"; +import type { R3, V3 } from "../types"; export type SyncRigidBodyData = { position: V3; diff --git a/src/components/tools/CameraSync.tsx b/src/components/tools/CameraSync.tsx index 34781cd..70df28a 100644 --- a/src/components/tools/CameraSync.tsx +++ b/src/components/tools/CameraSync.tsx @@ -1,5 +1,5 @@ import { useThree } from "@react-three/fiber"; -import type { Pos3 } from "../../types/3d"; +import type { Pos3 } from "../../types"; import { useLayoutEffect } from "react"; import { observer } from "mobx-react-lite"; diff --git a/src/model/defaultVoxelTypes.ts b/src/model/defaultVoxelTypes.ts index bde74cd..5f9f7e1 100644 --- a/src/model/defaultVoxelTypes.ts +++ b/src/model/defaultVoxelTypes.ts @@ -1,4 +1,4 @@ -import type { VoxelType } from "../types/voxel"; +import type { VoxelType } from "../types"; const stone: VoxelType = { id: 'stone', diff --git a/src/model/gameFactory.ts b/src/model/gameFactory.ts index 53b30bb..1fd92f2 100644 --- a/src/model/gameFactory.ts +++ b/src/model/gameFactory.ts @@ -1,13 +1,16 @@ -import type { Game, RuntimeScene, World } from "../types"; +import type { Game, RuntimeGameScene, RuntimeScene, World } from "../types"; import { clone } from "../utils"; +import { populateRuntimeScene } from "../utils/runtime"; export class GameFactory { public static create(world: World): Game { + const scene = populateRuntimeScene(clone(world.initialScene), world); + return { paused: false, time: 0, - scene: clone(world.initialScene) as RuntimeScene, + scene: GameFactory.initGameScene(scene), } } @@ -21,4 +24,8 @@ export class GameFactory { public static save(game: Game): void { localStorage.setItem("game", JSON.stringify(game)); } + + private static initGameScene(scene: RuntimeScene): RuntimeGameScene { + return scene as RuntimeGameScene; + } } diff --git a/src/model/objectPrefabs/terrain.ts b/src/model/objectPrefabs/terrain.ts index c550fdc..5223364 100644 --- a/src/model/objectPrefabs/terrain.ts +++ b/src/model/objectPrefabs/terrain.ts @@ -1,4 +1,4 @@ -import type { Voxel } from "../../types/voxel"; +import type { Voxel } from "../../types"; export function terrainXZ(width: number, length: number): Voxel[] { diff --git a/src/model/objectPrefabs/wolf.ts b/src/model/objectPrefabs/wolf.ts index 65a5fa9..7ffffc3 100644 --- a/src/model/objectPrefabs/wolf.ts +++ b/src/model/objectPrefabs/wolf.ts @@ -1,4 +1,4 @@ -import type { Voxel } from "../../types/voxel"; +import type { Voxel } from "../../types"; const G = '#808080'; // gray fur const L = '#d0c8b8'; // light beige-gray (snout / face) diff --git a/src/model/worldFactory.ts b/src/model/worldFactory.ts index 5f54e83..a6f7413 100644 --- a/src/model/worldFactory.ts +++ b/src/model/worldFactory.ts @@ -1,10 +1,11 @@ -import type { World } from "../types"; +import { type RuntimeWorld, type World } from "../types"; +import { depopulateRuntimeWorld, populateRuntimeWorld } from "../utils/runtime"; import { DEFAULT_VOXEL_TYPES } from "./defaultVoxelTypes"; export class WorldFactory { - public static create(): World { - return { + public static create(): RuntimeWorld { + return populateRuntimeWorld({ objectTypes: {}, voxelTypes: DEFAULT_VOXEL_TYPES, initialScene: { @@ -23,17 +24,17 @@ export class WorldFactory { position: [0, 5, 10], look: [0, 0, 0], }, - } + }); } - public static load(): World | undefined { + public static load(): RuntimeWorld | undefined { const json = localStorage.getItem("world"); if (json) { - return JSON.parse(json) as World; + return populateRuntimeWorld(JSON.parse(json) as World); } } - public static save(world: World): void { - localStorage.setItem("world", JSON.stringify(world)); + public static save(world: RuntimeWorld): void { + localStorage.setItem("world", JSON.stringify(depopulateRuntimeWorld(world))); } } diff --git a/src/state/gameState.ts b/src/state/gameState.ts index 5893126..03b522c 100644 --- a/src/state/gameState.ts +++ b/src/state/gameState.ts @@ -1,7 +1,6 @@ import { makeAutoObservable, reaction, toJS } from "mobx"; import type { WorldState } from "./worldState"; -import type { Game, RuntimeScene } from "../types"; -import type { Pos3, V3, R3 } from "../types/3d"; +import type { Game, Pos3, V3, R3, RuntimeGameScene } from "../types"; import type { CameraProps } from "@react-three/fiber"; import { GameFactory } from "../model/gameFactory"; @@ -34,7 +33,7 @@ export class GameState { return this.data.paused; } - public get scene(): RuntimeScene { + public get scene(): RuntimeGameScene { return this.data.scene; } diff --git a/src/state/worldEditorState.ts b/src/state/worldEditorState.ts index e4b480c..ef8e0aa 100644 --- a/src/state/worldEditorState.ts +++ b/src/state/worldEditorState.ts @@ -1,10 +1,10 @@ import { makeAutoObservable, runInAction } from "mobx"; import type { WorldState } from "./worldState"; -import type { ObjectInstance, Scene } from "../types"; +import { type ObjectInstance, type Pos3, type R3, type V3, type RuntimeScene } from "../types"; import { createObjectInstance } from "../utils/object"; import { randomId } from "../utils"; -import type { Pos3, R3, V3 } from "../types/3d"; import { state } from "./rootState"; +import { populateRuntimeObject } from "../utils/runtime"; export const SelectionEditModeEnum = [ 'translate', @@ -51,7 +51,7 @@ export class WorldEditorState { this.selectedObjectMode = undefined; } - public get scene(): Scene { + public get scene(): RuntimeScene { return this.world.data.initialScene; } @@ -82,7 +82,7 @@ export class WorldEditorState { typeId: string, pos: { x: number; y: number; z: number; }, ): ObjectInstance { - const obj = createObjectInstance(randomId(), typeId); + const obj = populateRuntimeObject(createObjectInstance(randomId(), typeId), this.world.data); obj.position = [pos.x, pos.y, pos.z]; obj.rotation = [0, 0, 0]; diff --git a/src/state/worldState.ts b/src/state/worldState.ts index 1fbaa1d..004ac45 100644 --- a/src/state/worldState.ts +++ b/src/state/worldState.ts @@ -1,14 +1,14 @@ import { makeAutoObservable, reaction, toJS } from "mobx"; import { WorldFactory } from "../model/worldFactory"; -import type { ObjectInstance, ObjectType, World } from "../types"; -import type { VoxelType } from "../types/voxel"; +import { type ObjectInstance, type ObjectType, type World, type VoxelType, type RuntimeWorld } from "../types"; import { state } from "./rootState"; import { DEFAULT_VOXEL_TYPES } from "../model/defaultVoxelTypes"; import { wolf } from "../model/objectPrefabs/wolf"; import { terrainXZ } from "../model/objectPrefabs/terrain"; +import { populateRuntimeWorld } from "../utils/runtime"; export class WorldState { - public data: World = WorldFactory.create(); + public data: RuntimeWorld = WorldFactory.create(); private startAutoSave() { return reaction(() => toJS(this.data), (data) => this.saveData(data), { delay: 500 }); @@ -39,7 +39,7 @@ export class WorldState { public loadMock() { console.log('Mocking world...'); - const objects = Array(0).fill(0) + const objects = Array(3).fill(0) .map((_, idx) => ({ id: `obj${idx}`, typeId: 'wolf', @@ -52,7 +52,7 @@ export class WorldState { objects.map((obj) => [obj.id, obj]), ) as Record - this.data = { + this.data = populateRuntimeWorld({ objectTypes: { wolf: { id: 'wolf', @@ -93,7 +93,8 @@ export class WorldState { gameRules: { gravity: true, }, - }; + }); + console.log(objects); state.worldEditor.resetSelectedObject(); } diff --git a/src/types/character.ts b/src/types/character.ts deleted file mode 100644 index ab9c92d..0000000 --- a/src/types/character.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Pos3 } from "./3d"; -import type { RuntimeObjectData } from "./object"; - -export type Character = { - transform: Pos3, -} - -export type RuntimeCharacter = Character & RuntimeObjectData; diff --git a/src/types/game.ts b/src/types/game.ts deleted file mode 100644 index e48f5d1..0000000 --- a/src/types/game.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { RuntimeScene } from "./scene"; - -export type Game = { - paused: boolean; - time: number; - scene: RuntimeScene; -} diff --git a/src/types/index.ts b/src/types/index.ts index 098f42a..eafa3a5 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,9 +1,3 @@ -export * from './object'; -export * from './scene'; -export * from './world'; -export * from './gameRules'; -export * from './game'; -export * from './character'; - - - +export * from './3d'; +export * from './model'; +export * from './runtime'; diff --git a/src/types/model/character.ts b/src/types/model/character.ts new file mode 100644 index 0000000..820861f --- /dev/null +++ b/src/types/model/character.ts @@ -0,0 +1,8 @@ +import type { Pos3 } from "../3d"; +import type { GameObjectData } from "./object"; + +export type Character = { + transform: Pos3, +} + +export type GameCharacter = Character & GameObjectData; diff --git a/src/types/model/game.ts b/src/types/model/game.ts new file mode 100644 index 0000000..dd9f5d3 --- /dev/null +++ b/src/types/model/game.ts @@ -0,0 +1,7 @@ +import type { RuntimeGameScene } from "./scene"; + +export type Game = { + paused: boolean; + time: number; + scene: RuntimeGameScene; +} diff --git a/src/types/gameRules.ts b/src/types/model/gameRules.ts similarity index 100% rename from src/types/gameRules.ts rename to src/types/model/gameRules.ts diff --git a/src/types/model/index.ts b/src/types/model/index.ts new file mode 100644 index 0000000..a0f4417 --- /dev/null +++ b/src/types/model/index.ts @@ -0,0 +1,8 @@ +export * from './object'; +export * from './scene'; +export * from './world'; +export * from './gameRules'; +export * from './game'; +export * from './character'; +export * from './voxel'; +export * from './runtime'; diff --git a/src/types/object.ts b/src/types/model/object.ts similarity index 52% rename from src/types/object.ts rename to src/types/model/object.ts index cf8fb52..4d5b313 100644 --- a/src/types/object.ts +++ b/src/types/model/object.ts @@ -1,4 +1,5 @@ -import type { R3, V3 } from "./3d"; +import type { R3, V3 } from "../3d"; +import type { Runtime } from "./runtime"; import type { Voxel } from "./voxel"; export type ObjectType = { @@ -17,9 +18,12 @@ export type ObjectInstance = { scale: V3; } -export type RuntimeObjectData = { +export type GameObjectData = { linearVelocity: V3; radialVelocity: R3; } -export type RuntimeObjectInstance = ObjectInstance & RuntimeObjectData; +export type GameObjectInstance = ObjectInstance & GameObjectData; + +export type RuntimeObjectInstance = Runtime; +export type RuntimeGameObjectInstance = Runtime; diff --git a/src/types/model/runtime.ts b/src/types/model/runtime.ts new file mode 100644 index 0000000..c37461c --- /dev/null +++ b/src/types/model/runtime.ts @@ -0,0 +1,10 @@ +import type { VoxelGroup } from "../runtime"; +import type { ObjectInstance } from "./object"; + +export type ObjectInstanceRuntimeData = { + cache: { + voxelGroups: VoxelGroup[]; + }; +} + +export type Runtime = T & ObjectInstanceRuntimeData; diff --git a/src/types/model/scene.ts b/src/types/model/scene.ts new file mode 100644 index 0000000..98ae942 --- /dev/null +++ b/src/types/model/scene.ts @@ -0,0 +1,22 @@ +import type { Character, GameCharacter } from "./character"; +import type { GameObjectInstance, ObjectInstance, RuntimeGameObjectInstance, RuntimeObjectInstance } from "./object"; + +export type Scene = { + character: Character; + objects: Record; +} + +export type RuntimeScene = { + character: Character; + objects: Record; +} + +export type GameScene = { + character: GameCharacter; + objects: Record; +} + +export type RuntimeGameScene = { + character: GameCharacter; + objects: Record; +} diff --git a/src/types/voxel.ts b/src/types/model/voxel.ts similarity index 87% rename from src/types/voxel.ts rename to src/types/model/voxel.ts index fc22092..9d40a05 100644 --- a/src/types/voxel.ts +++ b/src/types/model/voxel.ts @@ -1,4 +1,4 @@ -import type { V3 } from "./3d"; +import type { V3 } from "../3d"; export type VoxelType = { id: string; diff --git a/src/types/world.ts b/src/types/model/world.ts similarity index 66% rename from src/types/world.ts rename to src/types/model/world.ts index c3736a9..4e187ac 100644 --- a/src/types/world.ts +++ b/src/types/model/world.ts @@ -1,7 +1,7 @@ import type { GameRules } from "./gameRules"; import type { ObjectType } from "./object"; -import type { Scene } from "./scene"; -import type { Pos3 } from "./3d"; +import type { RuntimeScene, Scene } from "./scene"; +import type { Pos3 } from "../3d"; import type { VoxelType } from "./voxel"; export type World = { @@ -11,3 +11,7 @@ export type World = { initialScene: Scene; gameRules: GameRules; } + +export type RuntimeWorld = World & { + initialScene: RuntimeScene; +} diff --git a/src/types/runtime/index.ts b/src/types/runtime/index.ts new file mode 100644 index 0000000..2aaaab3 --- /dev/null +++ b/src/types/runtime/index.ts @@ -0,0 +1 @@ +export * from './voxelGroup'; diff --git a/src/types/runtime/voxelGroup.ts b/src/types/runtime/voxelGroup.ts new file mode 100644 index 0000000..cabbc36 --- /dev/null +++ b/src/types/runtime/voxelGroup.ts @@ -0,0 +1,9 @@ +import type { V3 } from "../3d"; + +export type VoxelGroup = { + id: string; + color: string; + opacity: number; + positions: V3[]; +}; + diff --git a/src/types/scene.ts b/src/types/scene.ts deleted file mode 100644 index 9e74291..0000000 --- a/src/types/scene.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Character, RuntimeCharacter } from "./character"; -import type { ObjectInstance, RuntimeObjectInstance } from "./object"; - -export type Scene = { - character: Character; - objects: Record; -} - -export type RuntimeScene = { - character: RuntimeCharacter; - objects: Record; -} diff --git a/src/utils/graphics/mesh.ts b/src/utils/graphics/mesh.ts new file mode 100644 index 0000000..f868343 --- /dev/null +++ b/src/utils/graphics/mesh.ts @@ -0,0 +1,41 @@ +import type { ObjectType, VoxelType } from "../../types"; + +export function buildObjectTrimesh(objectType: ObjectType, voxelTypes: Record): [Float32Array, Uint32Array] | null { + const collidable = objectType.voxels.filter((v) => voxelTypes[v.typeId]?.collidable !== false); + + if (!collidable.length) + return null; + + const n = collidable.length; + const verts = new Float32Array(n * 8 * 3); + const idxs = new Uint32Array(n * 36); + + for (let i = 0; i < n; i++) { + const p = collidable[i].position; + const vb = i * 8; + + // 8 corners of unit box at position p (no +0.5 — group transform handles offset) + verts.set([ + p[0], p[1], p[2], + p[0] + 1, p[1], p[2], + p[0], p[1] + 1, p[2], + p[0] + 1, p[1] + 1, p[2], + p[0], p[1], p[2] + 1, + p[0] + 1, p[1], p[2] + 1, + p[0], p[1] + 1, p[2] + 1, + p[0] + 1, p[1] + 1, p[2] + 1, + ], vb * 3); + + // 12 triangles (CCW outward normals) + idxs.set([ + vb, vb + 2, vb + 3, vb, vb + 3, vb + 1, // -Z + vb + 4, vb + 5, vb + 7, vb + 4, vb + 7, vb + 6, // +Z + vb, vb + 1, vb + 5, vb, vb + 5, vb + 4, // -Y + vb + 2, vb + 6, vb + 7, vb + 2, vb + 7, vb + 3, // +Y + vb, vb + 4, vb + 6, vb, vb + 6, vb + 2, // -X + vb + 1, vb + 3, vb + 7, vb + 1, vb + 7, vb + 5, // +X + ], i * 36); + } + + return [verts, idxs]; +} \ No newline at end of file diff --git a/src/utils/graphics/voxelGroup.ts b/src/utils/graphics/voxelGroup.ts index 6f04d33..7736567 100644 --- a/src/utils/graphics/voxelGroup.ts +++ b/src/utils/graphics/voxelGroup.ts @@ -1,12 +1,4 @@ -import type { ObjectType } from "../../types"; -import type { VoxelType } from "../../types/voxel"; - -export type VoxelGroup = { - id: string; - color: string; - opacity: number; - positions: [number, number, number][]; -}; +import type { ObjectType, VoxelGroup, VoxelType } from "../../types"; export function getObjectVoxelGroups( object: ObjectType, diff --git a/src/utils/object.ts b/src/utils/object.ts index b6c7482..2bc3dfb 100644 --- a/src/utils/object.ts +++ b/src/utils/object.ts @@ -4,7 +4,10 @@ export function createObjectInstance(id: string, typeId: string): ObjectInstance return { id, typeId: typeId, + physics: false, + gravityScale: 1, position: [0, 0, 0], rotation: [0, 0, 0], + scale: [1, 1, 1], }; } diff --git a/src/utils/runtime/index.ts b/src/utils/runtime/index.ts new file mode 100644 index 0000000..53f97dd --- /dev/null +++ b/src/utils/runtime/index.ts @@ -0,0 +1,3 @@ +export * from './world'; +export * from './scene'; +export * from './object'; diff --git a/src/utils/runtime/object.ts b/src/utils/runtime/object.ts new file mode 100644 index 0000000..8f48946 --- /dev/null +++ b/src/utils/runtime/object.ts @@ -0,0 +1,19 @@ +import { getObjectVoxelGroups } from "../graphics/voxelGroup"; +import type { ObjectInstance, RuntimeObjectInstance, World } from "../../types"; + +export function populateRuntimeObject(object: ObjectInstance, world: World): RuntimeObjectInstance { + const objectType = world.objectTypes[object.typeId]; + + return { + ...object, + cache: { + voxelGroups: getObjectVoxelGroups(objectType, world.voxelTypes), + }, + }; + +} + +export function depopulateRuntimeObject(object: RuntimeObjectInstance, _world: World): ObjectInstance { + const { cache, ...result } = object; + return result; +} diff --git a/src/utils/runtime/scene.ts b/src/utils/runtime/scene.ts new file mode 100644 index 0000000..11f59a1 --- /dev/null +++ b/src/utils/runtime/scene.ts @@ -0,0 +1,16 @@ +import type { RuntimeScene, Scene, World } from "../../types"; +import { depopulateRuntimeObject, populateRuntimeObject } from "./object"; + +export function populateRuntimeScene(scene: Scene, world: World): RuntimeScene { + return { + ...scene, + objects: Object.fromEntries(Object.entries(scene.objects).map(([id, obj]) => [id, populateRuntimeObject(obj, world)])), + } +} + +export function depopulateRuntimeScene(scene: RuntimeScene, world: World): Scene { + return { + ...scene, + objects: Object.fromEntries(Object.entries(scene.objects).map(([id, obj]) => [id, depopulateRuntimeObject(obj, world)])), + } +} diff --git a/src/utils/runtime/world.ts b/src/utils/runtime/world.ts new file mode 100644 index 0000000..f509a99 --- /dev/null +++ b/src/utils/runtime/world.ts @@ -0,0 +1,16 @@ +import type { RuntimeWorld, World } from "../../types"; +import { depopulateRuntimeScene, populateRuntimeScene } from "./scene"; + +export function populateRuntimeWorld(world: World): RuntimeWorld { + return { + ...world, + initialScene: populateRuntimeScene(world.initialScene, world), + }; +} + +export function depopulateRuntimeWorld(world: RuntimeWorld): World { + return { + ...world, + initialScene: depopulateRuntimeScene(world.initialScene, world), + }; +}